自定义ViewGroup实现左滑效果

2017-07-16 11:13:30来源:CSDN作者:u014372527人点击

相信很多人见过也写过这样的控件,我也参照网上的例子,自己模仿着写了一个,主要的目的是为了梳理下自定义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:

点击打开链接









微信扫一扫

第七城市微信公众平台