ViewDragHelper是Google2013年IO大会提出来用于解决界面控件拖拽移动的问题(位于v4兼容包下),最近在做QQ侧滑菜单那样的效果,用到了ViewDragHelper,做个笔记记录下。
结合简单的demo一点一点介绍ViewDragHelper,首先创建一个类DragLayout,继承自LinearLayout[LinearLayout继承自VIewGroup的嘛],提供相应的构造器,接着需要创建ViewDragHelper对象,发现new不出来它,打开源码发现它的构造器是私有的,继续查看源码,发现它提供了两个静态的工厂方法,如下:
/** * Factory method to create a new ViewDragHelper. * * @param forParent Parent view to monitor * @param cb Callback to provide information and receive events * @return a new ViewDragHelper instance */ public static ViewDragHelper create(ViewGroup forParent, Callback cb) { return new ViewDragHelper(forParent.getContext(), forParent, cb); }
/** * Factory method to create a new ViewDragHelper. * * @param forParent Parent view to monitor * @param sensitivity Multiplier for how sensitive the helper should be about detecting * the start of a drag. Larger values are more sensitive. 1.0f is normal. * @param cb Callback to provide information and receive events * @return a new ViewDragHelper instance */ public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) { final ViewDragHelper helper = create(forParent, cb); helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity)); return helper; }
|
一个提供了两个参数,一个提供了三个参数,唯一不同的是多了一个灵敏度,并且这个灵敏度的默认值是1.0f,我这里使用两个参数的静态工厂方法创建ViewDragHelper。它的第一个参数是父View,在我的代码里,也就是DragLayout,传入this即可,最后一个参数是回调接口,实现这个回调接口即可。
到这里,我的代码如下:
public class DragLayout extends LinearLayout {
private ViewDragHelper dragHelper;
public DragLayout(Context context) { this(context, null); }
public DragLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); }
public DragLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); dragHelper = ViewDragHelper.create(this, mCallback); }
/** * 该接口默认实现了public boolean tryCaptureView(View child, int pointerId)方法 * 当然还需要实现很多其他的方法 */ private ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() { /** * 根据返回结果决定当前child是否可以拖拽 * @param child 当前被拖拽的view * @param pointerId 区分多点触摸的id * @return */ @Override public boolean tryCaptureView(View child, int pointerId) { Log.e("Test","child view:" + child.toString()); return true; } };
|
看到回调接口默认实现了public boolean tryCaptureView(View child, int pointerId)
方法,第一个参数是当前被拖拽的view,第二个参数是区分多点触摸的id,暂且不管第二个参数,这个方法根据返回结果决定当前child是否可以拖拽,我先返回true,让当前所有的child都可以被拖动,在拖动的时候,打印view对象的值。
此时在MainActivity的布局中使用DragLayout,并添加两个子view(颜色区分),如下:
<me.chenfuduo.myviewdraghelperusage.drag.DragLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:orientation="vertical" tools:context=".MainActivity">
<LinearLayout android:layout_gravity="center_horizontal" android:background="#009688" android:layout_width="200dp" android:layout_height="200dp"></LinearLayout>
<LinearLayout android:layout_marginTop="20dp" android:layout_gravity="center_horizontal" android:background="#FF4081" android:layout_width="200dp" android:layout_height="200dp"></LinearLayout>
</me.chenfuduo.myviewdraghelperusage.drag.DragLayout>
|
ok,此时运行,去拖动view,发现并没有打印view对象的值,因为我们还缺少一步没有去做。
接下来,我们需要把触摸事件交给ViewDragHelper去处理,回忆在自定义控件里,我们重写的几个方法:
public boolean dispatchTouchEvent(MotionEvent ev) public boolean onInterceptTouchEvent(MotionEvent ev) public boolean onTouchEvent(MotionEvent event)
|
我们这里不需要事件分发的方法,重写后两个即可。首先是public boolean onInterceptTouchEvent(MotionEvent ev)
,这个方法将事件拦截下来,相当于把自定义控件的事件交给ViewDragHelper去处理。
/** * 将事件拦截下来,相当于把自定义控件的事件交给ViewDragHelper去处理 * @param ev * @return */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return dragHelper.shouldInterceptTouchEvent(ev); }
|
接着,将拦截下来的事件进行处理,onTouchEvent
返回true,持续接收事件.
/** * 将拦截下来的事件做处理 * @param event * @return */ @Override public boolean onTouchEvent(MotionEvent event) { try { dragHelper.processTouchEvent(event); } catch (Exception e) { e.printStackTrace(); } return true; }
|
此时运行程序,发现已经打印值。(不管向哪里拖动,view都会回到左边的位置)
06-29 08:39:02.224 22132-22132/me.chenfuduo.myviewdraghelperusage E/Test﹕ child view:android.widget.LinearLayout@53585d10 06-29 08:39:03.876 22132-22132/me.chenfuduo.myviewdraghelperusage E/Test﹕ child view:android.widget.LinearLayout@53585d10 06-29 08:39:05.324 22132-22132/me.chenfuduo.myviewdraghelperusage E/Test﹕ child view:android.widget.LinearLayout@53585d10 06-29 08:39:08.196 22132-22132/me.chenfuduo.myviewdraghelperusage E/Test﹕ child view:android.widget.LinearLayout@53585f18
|
ok,下面就来控制view拖拽的距离。为了实现控制两个view拖拽距离的控制,得先找到刚刚在MainActivity的布局中定义的两个LinearLayout,怎么找到这两个LinearLayout?(没有id啊)
重写下面的方法onFinishInflate()
,它在xml文件被填充结束后调用,这个时候它的所有的子孩子都被添加了。
@Override protected void onFinishInflate() { super.onFinishInflate(); blueView = (ViewGroup) getChildAt(0); pinkView = (ViewGroup) getChildAt(1); }
|
我在xml文件中,每个LinearLayout都有一个背景颜色,第一个是蓝色的,第二个是粉红色的。这样就得到了两个view.
重写callback中的方法clampViewPositionHorizontal(View child, int left, int dx)
和clampViewPositionVertical(View child, int top, int dy)
/** * 根据建议值修正将要移动到的位置(水平) * @param child * @param left * @param dx * @return */ @Override public int clampViewPositionHorizontal(View child, int left, int dx) { super.clampViewPositionHorizontal(child, left, dx); Log.e("Test","left:" + left + " dx:" + dx); return left; }
@Override public int clampViewPositionVertical(View child, int top, int dy) { super.clampViewPositionVertical(child, top, dy); Log.e("Test","top:" + top + " dy:" + dy); return top; }
|
实现的效果图如下:
1
它们分别负责:根据建议值修正将要移动到的位置(水平或者垂直)。当然这里可能会移除超出屏幕的边界。这个时候修改public boolean tryCaptureView(View child, int pointerId)
,让其不是总是返回true,如下:
@Override public boolean tryCaptureView(View child, int pointerId) { Log.e("Test", "child view:" + child.toString()); return child == blueView; }
|
此时我们看到红色的view能打印出view对象的值,但是不能移动,只有蓝色的可以移动。
总结上面的,使用ViewDragerHelper可以分为三个步骤:
- 初始化 使用静态工厂方法创建ViewDragHelper对象
- 接收触摸事件
- 重写Callback的方法
后面会写一个QQ侧滑面板的综合案例,本篇的代码和后面的一起上传。
晚饭后看到这样的一篇文章,说是FrameLayout的效率比LinearLayout和RelativeLayout的效率高。
- RelativeLayout会让子View调用两次onMeasure,LinearLayout在有weight时,也会调用子View两次onMeasure。
- RelativeLayout的子View如果高度和RelativeLayout不同,则会引发效率问题,当子View很复杂时,这个问题会更加严重。如果可以,使用padding代替margin。
- 在不响应层级深度的情况下,使用Linearlayout和FrameLayout而不是RelativeLayout(上帝是公平的,灵活性导致了效率上的问题)。
下面是View的measure()方法的源码:
/** * <p> * This is called to find out how big a view should be. The parent * supplies constraint information in the width and height parameters. * </p> * * <p> * The actual measurement work of a view is performed in * {@link #onMeasure(int, int)}, called by this method. Therefore, only * {@link #onMeasure(int, int)} can and must be overridden by subclasses. * </p> * * * @param widthMeasureSpec Horizontal space requirements as imposed by the * parent * @param heightMeasureSpec Vertical space requirements as imposed by the * parent * * @see #onMeasure(int, int) */ public final void measure(int widthMeasureSpec, int heightMeasureSpec) { boolean optical = isLayoutModeOptical(this); if (optical != isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets(); int oWidth = insets.left + insets.right; int oHeight = insets.top + insets.bottom; widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth); heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight); }
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL; if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT; final boolean isExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY; final boolean matchingSize = isExactly && getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec) && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec); if (forceLayout || !matchingSize && (widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec)) {
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key); if (cacheIndex < 0 || sIgnoreMeasureCache) { onMeasure(widthMeasureSpec, heightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } else { long value = mMeasureCache.valueAt(cacheIndex); setMeasuredDimensionRaw((int) (value >> 32), (int) value); mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; }
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) { throw new IllegalStateException("onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); }
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; }
mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 | (long) mMeasuredHeight & 0xffffffffL); }
|
注意到这里的方法是final的
也就是无法被继承改变了,在这里调用了onMeasure(),而每个View的子类也在onMeasure中真正完成了测量,而ViewGroup们也在onMeasure中完成了对子View的测量。
注意到有widthMeasureSpec
和heightMeasureSpec
这2个变量。这两个变量描述的是父View对子View的大小的要求。xxMeasureSpec(widthMeasureSpec
和heightMeasureSpec
)是int类型,它的高16位是mode,低16位是size;用MeasureSpec.getMode(),MeasureSpec.getSize()可以计算出mode和size.
mode分为3种:MeasureSpec.EXACTLY
,MeasureSpec.AT_MOST
,MeasureSpec.UNSPECIFIED
.主要看AT_MOST(最多这么大)和EXACTLY(确切这么大);下面是ViewGroup.getChildMeasureSpec()方法。作用是根据父View的Spec生成子View的Spec,传入的spec是父View的spec,padding是父View的padding,childDemension对应的是xml中我们常写的layout_width,layout_height.
/** * Does the hard part of measureChildren: figuring out the MeasureSpec to * pass to a particular child. This method figures out the right MeasureSpec * for one dimension (height or width) of one child view. * * The goal is to combine information from our MeasureSpec with the * LayoutParams of the child to get the best possible results. For example, * if the this view knows its size (because its MeasureSpec has a mode of * EXACTLY), and the child has indicated in its LayoutParams that it wants * to be the same size as the parent, the parent should ask the child to * layout given an exact size. * * @param spec The requirements for this view * @param padding The padding of this view for the current dimension and * margins, if applicable * @param childDimension How big the child wants to be in the current * dimension * @return a MeasureSpec integer for the child */ 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) { case MeasureSpec.EXACTLY: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } break;
case MeasureSpec.AT_MOST: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } break;
case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = 0; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = 0; resultMode = MeasureSpec.UNSPECIFIED; } break; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
|
比如说父View是100px80px,padding是10px,模式是EXACTLY,而子View是martchParent的,那子view的measure方法传入的参数为:80px60px,而mode是EXACTLY的.
View.measure()方法中的一个优化:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { ... mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; }
|
如果我们或者我们的子View没有要求强制刷新,而父View给子View的传入值也没有变化(也就是说子View的位置没变化),就不会做无谓的测量。View的measure流程基本就是这样。下面来看一下RelativeLayout的onMeasure()中干了点啥吧.
- 首先RelativeLayout中由于有依赖关系,而且依赖关系可能和xml中view的顺序并不相同,在确定每个人的位置的时候,就需要先给所有的子View排序一下。
- 又因为RelativeLayout允许A,B 2个子View,横向上B依赖A,纵向上A依赖B。所以需要横向纵向各排序依次。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mDirtyHierarchy) { mDirtyHierarchy = false; sortChildren(); }
int myWidth = -1; int myHeight = -1;
int width = 0; int height = 0;
final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); final int widthSize = MeasureSpec.getSize(widthMeasureSpec); final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode != MeasureSpec.UNSPECIFIED) { myWidth = widthSize; }
if (heightMode != MeasureSpec.UNSPECIFIED) { myHeight = heightSize; }
if (widthMode == MeasureSpec.EXACTLY) { width = myWidth; }
if (heightMode == MeasureSpec.EXACTLY) { height = myHeight; }
mHasBaselineAlignedChild = false;
View ignore = null; int gravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; final boolean horizontalGravity = gravity != Gravity.START && gravity != 0; gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; final boolean verticalGravity = gravity != Gravity.TOP && gravity != 0;
int left = Integer.MAX_VALUE; int top = Integer.MAX_VALUE; int right = Integer.MIN_VALUE; int bottom = Integer.MIN_VALUE;
boolean offsetHorizontalAxis = false; boolean offsetVerticalAxis = false;
if ((horizontalGravity || verticalGravity) && mIgnoreGravity != View.NO_ID) { ignore = findViewById(mIgnoreGravity); }
final boolean isWrapContentWidth = widthMode != MeasureSpec.EXACTLY; final boolean isWrapContentHeight = heightMode != MeasureSpec.EXACTLY;
final int layoutDirection = getLayoutDirection(); if (isLayoutRtl() && myWidth == -1) { myWidth = DEFAULT_WIDTH; }
View[] views = mSortedHorizontalChildren; int count = views.length;
for (int i = 0; i < count; i++) { View child = views[i]; if (child.getVisibility() != GONE) { LayoutParams params = (LayoutParams) child.getLayoutParams(); int[] rules = params.getRules(layoutDirection);
applyHorizontalSizeRules(params, myWidth, rules); measureChildHorizontal(child, params, myWidth, myHeight);
if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) { offsetHorizontalAxis = true; } } }
views = mSortedVerticalChildren; count = views.length; final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
for (int i = 0; i < count; i++) { View child = views[i]; if (child.getVisibility() != GONE) { LayoutParams params = (LayoutParams) child.getLayoutParams(); applyVerticalSizeRules(params, myHeight); measureChild(child, params, myWidth, myHeight); if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) { offsetVerticalAxis = true; }
if (isWrapContentWidth) { if (isLayoutRtl()) { if (targetSdkVersion < Build.VERSION_CODES.KITKAT) { width = Math.max(width, myWidth - params.mLeft); } else { width = Math.max(width, myWidth - params.mLeft - params.leftMargin); } } else { if (targetSdkVersion < Build.VERSION_CODES.KITKAT) { width = Math.max(width, params.mRight); } else { width = Math.max(width, params.mRight + params.rightMargin); } } }
if (isWrapContentHeight) { if (targetSdkVersion < Build.VERSION_CODES.KITKAT) { height = Math.max(height, params.mBottom); } else { height = Math.max(height, params.mBottom + params.bottomMargin); } }
if (child != ignore || verticalGravity) { left = Math.min(left, params.mLeft - params.leftMargin); top = Math.min(top, params.mTop - params.topMargin); }
if (child != ignore || horizontalGravity) { right = Math.max(right, params.mRight + params.rightMargin); bottom = Math.max(bottom, params.mBottom + params.bottomMargin); } } }
if (mHasBaselineAlignedChild) { for (int i = 0; i < count; i++) { View child = getChildAt(i); if (child.getVisibility() != GONE) { LayoutParams params = (LayoutParams) child.getLayoutParams(); alignBaseline(child, params);
if (child != ignore || verticalGravity) { left = Math.min(left, params.mLeft - params.leftMargin); top = Math.min(top, params.mTop - params.topMargin); }
if (child != ignore || horizontalGravity) { right = Math.max(right, params.mRight + params.rightMargin); bottom = Math.max(bottom, params.mBottom + params.bottomMargin); } } } }
if (isWrapContentWidth) { width += mPaddingRight;
if (mLayoutParams != null && mLayoutParams.width >= 0) { width = Math.max(width, mLayoutParams.width); }
width = Math.max(width, getSuggestedMinimumWidth()); width = resolveSize(width, widthMeasureSpec);
if (offsetHorizontalAxis) { for (int i = 0; i < count; i++) { View child = getChildAt(i); if (child.getVisibility() != GONE) { LayoutParams params = (LayoutParams) child.getLayoutParams(); final int[] rules = params.getRules(layoutDirection); if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_HORIZONTAL] != 0) { centerHorizontal(child, params, width); } else if (rules[ALIGN_PARENT_RIGHT] != 0) { final int childWidth = child.getMeasuredWidth(); params.mLeft = width - mPaddingRight - childWidth; params.mRight = params.mLeft + childWidth; } } } } }
if (isWrapContentHeight) { height += mPaddingBottom;
if (mLayoutParams != null && mLayoutParams.height >= 0) { height = Math.max(height, mLayoutParams.height); }
height = Math.max(height, getSuggestedMinimumHeight()); height = resolveSize(height, heightMeasureSpec);
if (offsetVerticalAxis) { for (int i = 0; i < count; i++) { View child = getChildAt(i); if (child.getVisibility() != GONE) { LayoutParams params = (LayoutParams) child.getLayoutParams(); final int[] rules = params.getRules(layoutDirection); if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_VERTICAL] != 0) { centerVertical(child, params, height); } else if (rules[ALIGN_PARENT_BOTTOM] != 0) { final int childHeight = child.getMeasuredHeight(); params.mTop = height - mPaddingBottom - childHeight; params.mBottom = params.mTop + childHeight; } } } } }
if (horizontalGravity || verticalGravity) { final Rect selfBounds = mSelfBounds; selfBounds.set(mPaddingLeft, mPaddingTop, width - mPaddingRight, height - mPaddingBottom);
final Rect contentBounds = mContentBounds; Gravity.apply(mGravity, right - left, bottom - top, selfBounds, contentBounds, layoutDirection);
final int horizontalOffset = contentBounds.left - left; final int verticalOffset = contentBounds.top - top; if (horizontalOffset != 0 || verticalOffset != 0) { for (int i = 0; i < count; i++) { View child = getChildAt(i); if (child.getVisibility() != GONE && child != ignore) { LayoutParams params = (LayoutParams) child.getLayoutParams(); if (horizontalGravity) { params.mLeft += horizontalOffset; params.mRight += horizontalOffset; } if (verticalGravity) { params.mTop += verticalOffset; params.mBottom += verticalOffset; } } } } }
if (isLayoutRtl()) { final int offsetWidth = myWidth - width; for (int i = 0; i < count; i++) { View child = getChildAt(i); if (child.getVisibility() != GONE) { LayoutParams params = (LayoutParams) child.getLayoutParams(); params.mLeft -= offsetWidth; params.mRight -= offsetWidth; } }
}
setMeasuredDimension(width, height); }
|
发现RelativeLayout会根据2次排列的结果对子View各做一次measure。而在做横向的测量时,纵向的测量结果尚未完成,只好暂时使用myHeight传入子View系统。这样必然会导致子View的高度和RelativeLayout的高度不同时,第3点中所说的优化会失效,在View系统足够复杂时,效率问题就会出现。
在我的代码里,我改了下DragLayout的继承者,使其继承自FrameLayout,并修改了布局。
实现的效果图如下: