自定义ViewGroup实现左滑效果

相信很多人见过也写过这样的控件,我也参照网上的例子,自己模仿着写了一个,主要的目的是为了梳理下自定义ViewGroup的方法跟流程。在这里,做个记录,也提供给大家做个了解,如果有写的不好的地方,希望能够及时给我指正。

效果图,这里我就不贴了,就是大家常见的那种左滑的效果。但是,我这里,并没有把左滑放在列表里面,因为我在列表里面,触摸其他地方,我还不知道怎么把之前的那个左滑的View给关闭。当然,网上有比较好的方案,看了好几个,不是我想要的,所以,如果大家有好的思路,可以自己去实现下,我这里就不做任何的讲解,我实在是还没有想到一个好的思路。所以,这里只是说明一下,怎样构造一个可以左滑的ViewGroup。

大家都知道,自定义ViewGroup的流程,主要就是onMeasure()和onLayout()两个方法以及事件处理这些方法,这里,onDraw()方法没有用到,所以就 不多说了。

OK,那就先从onMeasure()开始。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        Log.i("SwipeLayout","onMeasure");
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setClickable(true);

        boolean measureMatchParent=MeasureSpec.getMode(widthMeasureSpec)!=MeasureSpec.EXACTLY
                ||MeasureSpec.getMode(heightMeasureSpec)!=MeasureSpec.EXACTLY;
        int maxWidth=0,maxHeight=0;
        for (int i=0;i<getChildCount();i++){
            View child=getChildAt(i);
            if(child.getVisibility()!=GONE){
                measureChildWithMargins(child,widthMeasureSpec,0,heightMeasureSpec,0);
                MarginLayoutParams layoutParams= (MarginLayoutParams) child.getLayoutParams();
                //拿到最大宽度跟最大高度,来决定父控件的宽高
                maxWidth=Math.max(maxWidth,layoutParams.leftMargin+
                        child.getMeasuredWidth()+layoutParams.rightMargin);
                maxHeight=Math.max(maxHeight,layoutParams.topMargin+
                        child.getMeasuredHeight()+layoutParams.bottomMargin);

                //如果父控件是wrap_content的情况下,这个时候,子控件如果是match_parent,
                // 那么需要重新计算下子控件的宽高
                if(measureMatchParent) {
                    if (layoutParams.width==MeasureSpec.EXACTLY ||
                            layoutParams.height==MeasureSpec.EXACTLY){
                        //这里先加入到一个集合中,下面计算
                        mChildMatchParents.add(child);
                    }
                }
            }
        }
        //考虑下背景的宽高
        maxHeight=Math.max(maxHeight,getSuggestedMinimumHeight());
        maxWidth=Math.max(maxWidth,getSuggestedMinimumWidth());

        setMeasuredDimension(resolveSizeAndState(maxWidth,widthMeasureSpec,0)
                    ,resolveSizeAndState(maxHeight,heightMeasureSpec,0));


        for (int i=0;i<mChildMatchParents.size();i++){
            View child=mChildMatchParents.get(i);
            int childWidthSpec;
            MarginLayoutParams layoutParams= (MarginLayoutParams) child.getLayoutParams();
            if(layoutParams.width== LayoutParams.MATCH_PARENT){
                int width=Math.max(0,getMeasuredWidth()-
                        layoutParams.leftMargin-layoutParams.rightMargin);
                childWidthSpec=MeasureSpec.makeMeasureSpec(width,MeasureSpec.EXACTLY);
            }else{
                childWidthSpec=MeasureSpec.makeMeasureSpec(layoutParams.leftMargin+
                        layoutParams.width+layoutParams.rightMargin,MeasureSpec.EXACTLY);
            }

            int childHeightSpec;
            if(layoutParams.height==LayoutParams.MATCH_PARENT){
                int height=Math.max(0,getMeasuredHeight()-
                        layoutParams.topMargin-layoutParams.bottomMargin);
                childHeightSpec=MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY);
            }else{
                childHeightSpec=MeasureSpec.makeMeasureSpec(layoutParams.topMargin+
                        layoutParams.height+layoutParams.bottomMargin,MeasureSpec.EXACTLY);
            }
            child.measure(childWidthSpec,childHeightSpec);
        }
    }
这里是onMeasure的过程。代码里面写了一些注释。这里再详细做下说明。

boolean measureMatchParent=MeasureSpec.getMode(widthMeasureSpec)!=MeasureSpec.EXACTLY
                ||MeasureSpec.getMode(heightMeasureSpec)!=MeasureSpec.EXACTLY;

先来说下这一段代码,这里就是说,如果父控件的宽度或者高度是wrap_content 并且子View 是match_parent的情况下,我们需要对这些子View进行重新测量,当然了,有人可能会问父控件都是wrap_content了,怎么再对这些match_parent的子View进行测量呢?这里我的做法是对那些能够测量出来的子View,取它们的最大宽度跟最大高度给到父控件,这样,就直接把父控件的宽高定好了。然后那些match_parent的子View是不是就能够拿到宽高了呢。曾今我这里测量的时候有个疑问,就是子View是wrap_content的话是怎么测量的,因为 widthMeasureSpec跟 heightMeasureSpec 是用来测量父控件的。我这里用到的是measureChildWidthMargins 这个方法,我们就从这个方法入手,看看源码是怎么来解决这样的事情的。

大家都知道View的宽高有3中模式EXACTLY、AT_MOST、UNSPECFIED。

EXACTLY 表示的是match_parent或者是固定宽高。

AT_MOST 表示的是wrap_content

UNSPECFIED 表示的是未指定大小,就是子View想要多大就给多大了,这种情况很少用到,反正我是没有用过这个。

OK,了解了这个,我们直接看源码吧。

protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

这个是measureChildMargins方法,看到里面调用了getChildMeasureSpec这个方法,我们继续看。

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
这里的代码是重点,主要解决的问题是测量子View的问题。我们可以看到里面的switch语句主要做的事情是这样的,这里是父控件的MeasureSpec,分为三种模式,就是我们刚刚讲过的那三种。

EXACTYL 这种情况下,由于用的是MarginLayoutParams,所以我们可以轻松的拿到子View的宽高是match_parent还是wrap_content或者是固定的大小。然后就是知道子View的MeasureSpec 是什么了。这里我们可以看到是没有UNSPECFIED这种模式的。

WRAP_CONTENT 跟上面的差不多。就是固有的一些逻辑判断,大家通过代码应该也能看出来,就不多说了

UNSPECFIED 这个也不用多说了,就是一些正常的逻辑判断,相信大家也能够看得懂。

这样就可以拿到子View的MeasureSpec了。我们在回到measureChildWidthMargins这个方法,它最后调用了child.measure方法,用来测量的。这样就完成了整个的测量过程。

还有一点需要注意的是,我们这里用到的是MarginLayoutParams,我们需要重写一个方法,如下:

 @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(),attrs);
    }
不重写的话,会报一个layoutparams转换错误。这里就为大家揭秘下,这个方法是具体是干嘛的。

大家应该都知道LayoutInflate.inflate(),这个方法,干嘛用的呢,是用来解析布局文件的,我们会在加载一个布局的时候用到。那么我告诉你,系统在解析你的布局文件的时候也是通过这个方法。这个方法里面的代码还算多的,我贴一段主要的代码。

final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }
看到没有,这里会通过generateLayoutParams这个方法拿到它的layoutparams,所以我们通过重写这个方面就可以将layoutparams变成MarginLayoutParams了,就是使用margin相关的东西。这里主要是为了适配能够在这个左滑的ViewGroup里面能够写margin。

是不是很明了。ok, 继续啊,到了onLayout()。

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mContentView=getChildAt(0);
        mRightView=getChildAt(1);
        MarginLayoutParams cParams=null;
        if(mContentView!=null){
            cParams= (MarginLayoutParams) mContentView.getLayoutParams();
            int cl=l+cParams.leftMargin;
            int ct=t+cParams.topMargin;
            int cr=cl+mContentView.getMeasuredWidth();
            int cb=ct+mContentView.getMeasuredHeight();
            mContentView.layout(cl,ct,cr,cb);
        }
        if(mRightView!=null){
            MarginLayoutParams rParams= (MarginLayoutParams) mRightView.getLayoutParams();
            int rl=mContentView.getRight()+cParams.rightMargin+rParams.leftMargin;
            int rt=t+rParams.topMargin;
            int rr=rl+mRightView.getMeasuredWidth();
            int rb=rt+mRightView.getMeasuredHeight();
            mRightView.layout(rl,rt,rr,rb);
        }
    }
这里我为了简单起见,就直接默认写死了两个子View。这里需要注意下。 测量好了,摆放就很简单了,就是摆放在自己想要的地方就好了,没啥说的。

然后就是事件处理了,我这里重写了dispatchOnTouchEvent这个方法,当然也可以是onTouchEvent。

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastPoint.set(ev.getRawX(),ev.getRawY());
                firstPoint.set(ev.getRawX(),ev.getRawY());
                break;
            case MotionEvent.ACTION_MOVE:
                float delx=ev.getRawX()-lastPoint.x;
                float dely=ev.getRawY()-lastPoint.y;
                if(Math.abs(delx)>Math.abs(dely) && Math.abs(delx)>mTouchSlop){//
                    scrollBy(-(int) delx,0);
                    if(getScrollX()>=0){
                        if(getScrollX()>=mRightView.getMeasuredWidth()){
                            scrollTo(mRightView.getMeasuredWidth(),0);
                        }
                    }else{
                        if(getScrollX()<mContentView.getLeft()){
                            scrollTo(0,0);
                        }
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                float smoothX=ev.getRawX()-firstPoint.x-mTouchSlop;
                if(smoothX>=0 && getScrollX()>mContentView.getLeft()){
                    smoothClose();
                }else if(smoothX<0 && getScrollX()<mRightView.getMeasuredWidth()){
                    smoothExpand();
                }
                break;
        }
        lastPoint.set(ev.getRawX(),ev.getRawY());
        return super.dispatchTouchEvent(ev);
    }
这里呢其实,就是一些逻辑判断,简单说下展开跟关闭两个动画。其实这里可以用scroller来写。看个人爱好了。

private ValueAnimator mExpandAnim,mCloseAnim;
    public void smoothExpand(){

        if(mExpandAnim==null){
            mExpandAnim=ValueAnimator.ofInt(getScrollX(),mRightView.getMeasuredWidth());
        }
        //每次动画之前先取消所有的动画
        cancelAnim();
        mExpandAnim.setInterpolator(new LinearInterpolator());
        mExpandAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int value= (int) animation.getAnimatedValue();
                scrollTo(value,0);
            }
        });
        mExpandAnim.setDuration(500);
        mExpandAnim.start();
    }
    public void smoothClose(){

        if(mCloseAnim==null){
            mCloseAnim=ValueAnimator.ofInt(getScrollX(),0);
        }
        cancelAnim();
        mCloseAnim.setInterpolator(new LinearInterpolator());
        mCloseAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int value= (int) animation.getAnimatedValue();
                Log.i("SwipeLayout","value="+value);
                scrollTo(value,0);
            }
        });
        mCloseAnim.setDuration(500);
        mCloseAnim.start();
    }
    private void cancelAnim(){
        if(mExpandAnim!=null){
            mExpandAnim.cancel();
        }
        if(mCloseAnim!=null){
            mCloseAnim.cancel();
        }
    }
其实就是,看你的ACTION_UP跟ACTION_CANCEL在什么时候触发,来控制动画的距离。

此次分析就是以上,希望能够帮到大家。


Thanks:

点击打开链接









  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值