自定义ViewGroup实现左滑效果

2017-07-16 by 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:

点击打开链接









第七城市

栏目导航(关闭)