本文将为您提供关于Android利用ViewDragHelper轻松实现拼图游戏的示例的详细介绍,我们还将为您解释android拼图游戏的相关知识,同时,我们还将为您提供关于AndroidViewDr
本文将为您提供关于Android利用ViewDragHelper轻松实现拼图游戏的示例的详细介绍,我们还将为您解释android 拼图游戏的相关知识,同时,我们还将为您提供关于Android ViewDragHelper 及移动处理总结、Android ViewDragHelper 实现 QQ 侧滑边栏、Android ViewDragHelper 源码解析、Android ViewDragHelper 的简单使用的实用信息。
本文目录一览:- Android利用ViewDragHelper轻松实现拼图游戏的示例(android 拼图游戏)
- Android ViewDragHelper 及移动处理总结
- Android ViewDragHelper 实现 QQ 侧滑边栏
- Android ViewDragHelper 源码解析
- Android ViewDragHelper 的简单使用
Android利用ViewDragHelper轻松实现拼图游戏的示例(android 拼图游戏)
前言
最近一段时间看了一些介绍ViewDragHelper的博客,感觉这是一个处理手势滑动的神奇,看完以后就想做点东西练练手,于是就做了这个Android拼图小游戏。
先上个效果图
源码 https://github.com/kevin-mob/Puzzle
ViewDragHelper
其实ViewDragHelper并不是第一个用于分析手势处理的类,gesturedetector也是,但是在和拖动相关的手势分析方面gesturedetector只能说是勉为其难。
关于ViewDragHelper有如下几点:
ViewDragHelper.Callback是连接ViewDragHelper与view之间的桥梁(这个view一般是指拥子view的容器即parentView);
ViewDragHelper的实例是通过静态工厂方法创建的;
你能够指定拖动的方向;
ViewDragHelper可以检测到是否触及到边缘;
ViewDragHelper并不是直接作用于要被拖动的View,而是使其控制的视图容器中的子View可以被拖动,如果要指定某个子view的行为,需要在Callback中想办法;
ViewDragHelper的本质其实是分析onInterceptTouchEvent和onTouchEvent的MotionEvent参数,然后根据分析的结果去改变一个容器中被拖动子View的位置( 通过offsetTopAndBottom(int offset)和offsetLeftAndRight(int offset)方法 ),他能在触摸的时候判断当前拖动的是哪个子View;
虽然ViewDragHelper的实例方法 ViewDragHelper create(ViewGroup forParent,Callback cb) 可以指定一个被ViewDragHelper处理拖动事件的对象 。
实现思路
- 自定义PuzzleLayout继承自RelativeLayout。
- 将PuzzleLayout的onInterceptTouchEvent和onTouchEvent交给ViewDragHelper来处理。
- 将拼图Bitmap按九宫格切割,生成ImageView添加到PuzzleLayout并进行排列。
- 创建ImageView的对应数据模型。
- ViewDragHelper.Callback控制滑动边界的实现。
- 打乱ImageView的摆放位置。
下面介绍一下以上5步的具体实现细节。
第一步: 创建一个PuzzleLayout继承自RelativeLayout。
public class PuzzleLayout extends RelativeLayout { public PuzzleLayout(Context context) { super(context); } public PuzzleLayout(Context context,AttributeSet attrs) { super(context,attrs); } public PuzzleLayout(Context context,AttributeSet attrs,int defStyleAttr) { } }
第二步:将PuzzleLayout的onInterceptTouchEvent和onTouchEvent交给ViewDragHelper来处理。
这里我们会用到ViewDragHelper这个处理手势滑动的神器。
在使用之前我们先简单的了解一下它的相关函数。
/** * 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)
上面这个是创建一个ViewDragHelper的静态函数,根据注释我们可以了解到:
- 第一个参数是当前的ViewGroup。
- 第二个参数是检测拖动开始的灵敏度,1.0f为正常值。
- 第三个参数Callback,是ViewDragHelper给ViewGroup的回调。
这里我们主要来看看Callback这个参数,Callback会在手指触摸当前ViewGroup的过程中不断返回解析到的相关事件和状态,并获取ViewGroup返回给ViewDragHelper的状态,来决定接下来的操作是否需要执行,从而达到了在ViewGroup中管理和控制ViewDragHelper的目的。
Callback的方法很多,这里主要介绍本文用到的几个方法
public abstract boolean tryCaptureView(View child,int pointerId)
尝试捕获当前手指触摸到的子view, 返回true 允许捕获,false不捕获。
public int clampViewPositionHorizontal(View child,int left,int dx)
控制childView在水平方向的滑动,主要用来限定childView滑动的左右边界。
public int clampViewPositionVertical(View child,int top,int dy)
控制childView在垂直方向的滑动,主要用来限定childView滑动的上下边界。
public void onViewReleased(View releasedChild,float xvel,float yvel)
当手指从childView上离开时回调。
有了以上这些函数,我们的拼图游戏大致就可以做出来了,通过ViewDragHelper.create()来创建一个ViewDragHelper,通过Callback中tryCaptureView来控制当前触摸的子view是否可以滑动,clampViewPositionHorizontal、clampViewPositionVertical来控制水平方向和垂直方向的移动边界,具体的方法实现会在后面讲到。
public class PuzzleLayout extends RelativeLayout { private ViewDragHelper viewDragHelper; public PuzzleLayout(Context context) { super(context); init(); } public PuzzleLayout(Context context,AttributeSet attrs) { super(context,attrs); init(); } public PuzzleLayout(Context context,int defStyleAttr) { super(context,attrs,defStyleAttr); init(); } private void init() { getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { mHeight = getHeight(); mWidth = getWidth(); getViewTreeObserver().removeOnPreDrawListener(this); if(mDrawableId != 0 && mSquareRootNum != 0){ createChildren(); } return false; } }); viewDragHelper = ViewDragHelper.create(this,1.0f,new ViewDragHelper.Callback() { @Override public boolean tryCaptureView(View child,int pointerId) { return true; } @Override public int clampViewPositionHorizontal(View child,int dx) { return left; } @Override public int clampViewPositionVertical(View child,int dy) { return top; } @Override public void onViewReleased(View releasedChild,float yvel) { } }); } @Override public boolean onInterceptTouchEvent(MotionEvent event){ return viewDragHelper.shouldInterceptTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { viewDragHelper.processtouchEvent(event); return true; } }
第三步,将拼图Bitmap按九宫格切割,生成ImageView添加到PuzzleLayout并进行排列。
首先,外界需要传入一个切割参数mSquareRootNum做为宽和高的切割份数,我们需要获取PuzzleLayout的宽和高,然后计算出每一块的宽mItemWidth和高mItemHeight, 将Bitmap等比例缩放到和PuzzleLayout大小相等,然后将图片按照类似上面这张图所标的形式进行切割,生成mSquareRootNum*mSquareRootNum份Bitmap,每个Bitmap对应创建一个ImageView载体添加到PuzzleLayout中,并进行布局排列。
创建子view, mHelper是封装的用来操作对应数据模型的帮助类DataHelper。
/** * 将子View index与mHelper中models的index一一对应, * 每次在交换子View位置的时候model同步更新currentPosition。 */ private void createChildren(){ mHelper.setSquareRootNum(mSquareRootNum); displayMetrics dm = getResources().getdisplayMetrics(); BitmapFactory.Options options = new BitmapFactory.Options(); options.inDensity = dm.densityDpi; Bitmap resource = BitmapFactory.decodeResource(getResources(),mDrawableId,options); Bitmap bitmap = BitmapUtil.zoomImg(resource,mWidth,mHeight); resource.recycle(); mItemWidth = mWidth / mSquareRootNum; mItemHeight = mHeight / mSquareRootNum; for (int i = 0; i < mSquareRootNum; i++){ for (int j = 0; j < mSquareRootNum; j++){ Log.d(TAG,"mItemWidth * x " + (mItemWidth * i)); Log.d(TAG,"mItemWidth * y " + (mItemWidth * j)); ImageView iv = new ImageView(getContext()); iv.setScaleType(ImageView.ScaleType.FIT_XY); LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT); lp.leftMargin = j * mItemWidth; lp.topMargin = i * mItemHeight; iv.setLayoutParams(lp); Bitmap b = Bitmap.createBitmap(bitmap,lp.leftMargin,lp.topMargin,mItemWidth,mItemHeight); iv.setimageBitmap(b); addView(iv); } } }
第四步,创建ImageView的对应数据模型。
public class Block { public Block(int position,int vPosition,int hPosition){ this.position = position; this.vPosition = vPosition; this.hPosition = hPosition; } public int position; public int vPosition; public int hPosition; }
DataHelper.class
子View在父类的index与mHelper中model在models的index一一对应
class DataHelper { static final int N = -1; static final int L = 0; static final int T = 1; static final int R = 2; static final int B = 3; private static final String TAG = DataHelper.class.getSimpleName(); private int squareRootNum; private List<Block> models; DataHelper(){ models = new ArrayList<>(); } private void reset() { models.clear(); int position = 0; for (int i = 0; i< squareRootNum; i++){ for (int j = 0; j < squareRootNum; j++){ models.add(new Block(position,i,j)); position ++; } } } void setSquareRootNum(int squareRootNum){ this.squareRootNum = squareRootNum; reset(); } }
第五步,ViewDragHelper.Callback控制滑动边界的实现。
tryCaptureView的实现
public boolean tryCaptureView(View child,int pointerId) { int index = indexOfChild(child); return mHelper.getScrollDirection(index) != DataHelper.N; }
DataHelper的getScrollDirection函数
/** * 获取索引处model的可移动方向,不能移动返回 -1。 */ int getScrollDirection(int index){ Block model = models.get(index); int position = model.position; //获取当前view所在位置的坐标 x y /* * * * * * * * o * * * * * * * * * * * * */ int x = position % squareRootNum; int y = position / squareRootNum; int invisibleModelPosition = models.get(0).position; /* * 判断当前位置是否可以移动,如果可以移动就return可移动的方向。 */ if(x != 0 && invisibleModelPosition == position - 1) return L; if(x != squareRootNum - 1 && invisibleModelPosition == position + 1) return R; if(y != 0 && invisibleModelPosition == position - squareRootNum) return T; if(y != squareRootNum - 1 && invisibleModelPosition == position + squareRootNum) return B; return N; }
clampViewPositionHorizontal的实现细节,获取滑动方向左或右,再控制对应的滑动区域。
public int clampViewPositionHorizontal(View child,int dx) { int index = indexOfChild(child); int position = mHelper.getModel(index).position; int selfLeft = (position % mSquareRootNum) * mItemWidth; int leftEdge = selfLeft - mItemWidth; int rightEdge = selfLeft + mItemWidth; int direction = mHelper.getScrollDirection(index); //Log.d(TAG,"left " + left + " index" + index + " dx " + dx + " direction " + direction); switch (direction){ case DataHelper.L: if(left <= leftEdge) return leftEdge; else if(left >= selfLeft) return selfLeft; else return left; case DataHelper.R: if(left >= rightEdge) return rightEdge; else if (left <= selfLeft) return selfLeft; else return left; default: return selfLeft; } }
clampViewPositionVertical的实现细节,获取滑动方向上或下,再控制对应的滑动区域。
public int clampViewPositionVertical(View child,int dy) { int index = indexOfChild(child); Block model = mHelper.getModel(index); int position = model.position; int selfTop = (position / mSquareRootNum) * mItemHeight; int topEdge = selfTop - mItemHeight; int bottomEdge = selfTop + mItemHeight; int direction = mHelper.getScrollDirection(index); //Log.d(TAG,"top " + top + " index " + index + " direction " + direction); switch (direction){ case DataHelper.T: if(top <= topEdge) return topEdge; else if (top >= selfTop) return selfTop; else return top; case DataHelper.B: if(top >= bottomEdge) return bottomEdge; else if (top <= selfTop) return selfTop; else return top; default: return selfTop; } }
onViewReleased的实现,当松手时,不可见View和松开的View之间进行布局参数交换,同时对应的model之间也需要通过swapValueWithInvisibleModel函数进行数据交换。
public void onViewReleased(View releasedChild,float yvel) { Log.d(TAG,"xvel " + xvel + " yvel " + yvel); int index = indexOfChild(releasedChild); boolean isCompleted = mHelper.swapValueWithInvisibleModel(index); Block item = mHelper.getModel(index); viewDragHelper.settleCapturedViewAt(item.hPosition * mItemWidth,item.vPosition * mItemHeight); View invisibleView = getChildAt(0); ViewGroup.LayoutParams layoutParams = invisibleView.getLayoutParams(); invisibleView.setLayoutParams(releasedChild.getLayoutParams()); releasedChild.setLayoutParams(layoutParams); invalidate(); if(isCompleted){ invisibleView.setVisibility(VISIBLE); mOnCompleteCallback.onComplete(); } }
viewDragHelper.settleCapturedViewAt和viewDragHelper.continueSettling配合实现松手后的动画效果。
PuzzleLayout重写computeScroll函数。
@Override public void computeScroll() { if(viewDragHelper.continueSettling(true)) { invalidate(); } }
swapValueWithInvisibleModel函数,每次交换完成后会return拼图是否完成
/** * 将索引出的model的值与不可见 * model的值互换。 */ boolean swapValueWithInvisibleModel(int index){ Block formModel = models.get(index); Block invisibleModel = models.get(0); swapValue(formModel,invisibleModel); return isCompleted(); } /** * 交换两个model的值 */ private void swapValue(Block formModel,Block invisibleModel) { int position = formModel.position; int hPosition = formModel.hPosition; int vPosition = formModel.vPosition; formModel.position = invisibleModel.position; formModel.hPosition = invisibleModel.hPosition; formModel.vPosition = invisibleModel.vPosition; invisibleModel.position = position; invisibleModel.hPosition = hPosition; invisibleModel.vPosition = vPosition; } /** * 判断是否拼图完成。 */ private boolean isCompleted(){ int num = squareRootNum * squareRootNum; for (int i = 0; i < num; i++){ Block model = models.get(i); if(model.position != i){ return false; } } return true; }
第六步,打乱ImageView的摆放位置。
这里不能随意打乱顺序,否则你可能永远也不能复原拼图了,这里使用的办法是每次在不可见View附近随机找一个View与不可见View进行位置交换,这里的位置交换指的是布局参数的交换,同时对应的数据模型也需要进行数据交换。
public void randomOrder(){ int num = mSquareRootNum * mSquareRootNum * 8; View invisibleView = getChildAt(0); View neighbor; for (int i = 0; i < num; i ++){ int neighborPosition = mHelper.findNeighborIndexOfInvisibleModel(); ViewGroup.LayoutParams invisibleLp = invisibleView.getLayoutParams(); neighbor = getChildAt(neighborPosition); invisibleView.setLayoutParams(neighbor.getLayoutParams()); neighbor.setLayoutParams(invisibleLp); mHelper.swapValueWithInvisibleModel(neighborPosition); } invisibleView.setVisibility(INVISIBLE); }
DataHelper中findNeighborIndexOfInvisibleModel函数
/** * 随机查询出不可见 * 位置周围的一个model的索引。 */ public int findNeighborIndexOfInvisibleModel() { Block invisibleModel = models.get(0); int position = invisibleModel.position; int x = position % squareRootNum; int y = position / squareRootNum; int direction = new Random(System.nanoTime()).nextInt(4); Log.d(TAG,"direction " + direction); switch (direction){ case L: if(x != 0) return getIndexByCurrentPosition(position - 1); case T: if(y != 0) return getIndexByCurrentPosition(position - squareRootNum); case R: if(x != squareRootNum - 1) return getIndexByCurrentPosition(position + 1); case B: if(y != squareRootNum - 1) return getIndexByCurrentPosition(position + squareRootNum); } return findNeighborIndexOfInvisibleModel(); } /** * 通过给定的位置获取model的索引 */ private int getIndexByCurrentPosition(int currentPosition){ int num = squareRootNum * squareRootNum; for (int i = 0; i < num; i++) { if(models.get(i).position == currentPosition) return i; } return -1; }
以上为主要的代码实现,全部工程已上传Github,欢迎学习,欢迎star,传送门
https://github.com/kevin-mob/Puzzle
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持编程小技巧。
Android ViewDragHelper 及移动处理总结
概述
2013 年谷歌 i/o 大会上介绍了两个新的 layout: SlidingPaneLayout 和 DrawerLayout,现在这俩个类被广泛的运用。我们知道在我们实际的开发中往往会涉及到很多的拖动效果,而 ViewDragHelper 解决了 android 中手势处理过于复杂的问题。
其实 ViewDragHelper 并不是第一个用于分析手势处理的类,gesturedetector 也是,但是在和拖动相关的手势分析方面 gesturedetector 只能说是勉为其难,其拓展性并不好。
为了方便大家的理解,我们首先来看一下 android View 对移动事件的处理。
View 移动方法总结
layout
在自定义控件中,View 绘制的一个重写方法 layout (),用来设置显示的位置。所以,可以通过修改 View 的坐标值来改变 view 在父 View 的位置,以此可以达到移动的效果!但是缺点是只能移动指定的 View,如常见的:
view.layout(l,t,r,b);
offsetLeftAndRight /offsetTopAndBottom
非常方便的封装方法,只需提供水平、垂直方向上的偏移量,展示效果与 layout () 方法相同。
view.offsetLeftAndRight(offset);//同时改变left和right view.offsetTopAndBottom(offset);//同时改变top和bottom
LayoutParams
此类保存了一个 View 的布局参数,可通过 LayoutParams 动态改变一个布局的位置参数,以此动态地修改布局,达到 View 位置移动的效果!但是在获取 getLayoutParams () 时,要根据该子 View 对应的父 View 布局来决定自身的 LayoutParams 。所以一切的前提是:必须要有一个父 View,否则无法获取 LayoutParams。
LinearLayout.LayoutParamslayoutParams = (LinearLayout.LayoutParams)getLayoutParams();
layoutParams.leftMargin = getLeft() + dx; layoutParams.topMargin = getTop() + dy; setLayoutParams(layoutParams);
scrollTo/scrollBy
通过改变 scrollX 和 scrollY 来移动,但是可以移动所有的子 View。scrollTo (x,y) 表示移动到一个具体的坐标点 (x,y),而 scrollBy (x,y) 表示移动的增量为 dx,dy。
注意:这里使用 scrollBy (xOffset,yOffset);,你会发现并没有效果,因为以上两个方法移动的是 View 的 content。若在 ViewGroup 中使用,移动的是所有子 View;若在 View 中使用,移动的是 View 的内容(比如 TextView)。所以,不可在 view 中使用以上方法!
要想使用 scrollBy,应该在 View 所在的 ViewGroup 中使用:
((View)getParent()).scrollBy(offsetX, offsetY);
canvas
通过改变 Canvas 绘制的位置来移动 View 的内容,用的少,一般用在自定义的 View 中,比如老早之前实现手写板:
canvas.drawBitmap(bitmap, left, top, paint)
说完 View 的移动相关的属性,我们来看一下大名鼎鼎的 ViewDragHelper。
ViewDragHelper
要理解 ViewDragHelper,我们需要掌握以下几点:
- ViewDragHelper.Callback 是连接 ViewDragHelper 与 view 之间的桥梁;
- ViewDragHelper 的实例是通过静态工厂方法创建的;
- ViewDragHelper 可以检测到是否触及到边缘;
- ViewDragHelper 并不是直接作用于要被拖动的 View,而是使其控制的视图容器中的子 View 可以被拖动,如果要指定某个子 view 的行为,需要在 Callback 中实现;
- ViewDragHelper 的本质其实是分析 onInterceptTouchEvent 和 onTouchEvent 的 MotionEvent 参数,然后根据分析的结果去改变一个容器中被拖动子 View 的位置。
ViewDragHelper 使用
- ViewDragHelper 的初始化
ViewDragHelper 一般用在一个自定义 ViewGroup 的内部,比如下面自定义了一个继承于 LinearLayout 的 DragLayout,DragLayout 内部有一个子 view mDragView 作为成员变量:
public class DragLayout extends LinearLayout {
private final ViewDragHelper mDragHelper;
private View mDragView;
public DragLayout(Context context) {
this(context, null);
}
public DragLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DragLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
创建一个带有回调接口的 ViewDragHelper。
public DragLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mDragHelper = ViewDragHelper.create(this, 1.0f, new DragHelperCallback());
}
说明:其中其二个参数是敏感度,参数参数越大越敏感。
然后 ViewDragHelper 将触摸事件传递给 ViewDragHelper 进行处理。如:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mDragHelper.cancel();
return false;
}
return mDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
mDragHelper.processTouchEvent(ev);
return true;
}
- 拖动行为处理
在 DragHelperCallback 的回调方法中有很多的方法可以检测 View 的事件,如常见的 clampViewPositionHorizontal、clampViewPositionVertical,并且 clampViewPositionHorizontal 和 clampViewPositionVertical 必须要重写,因为默认它返回的是 0。
来看 clampViewPositionHorizontal 的处理。
在 DragHelperCallback 中实现 clampViewPositionHorizontal 方法, 并且返回一个适当的数值就能实现横向拖动效果。
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
Log.d("DragLayout", "clampViewPositionHorizontal " + left + "," + dx);
final int leftBound = getPaddingLeft();
final int rightBound = getWidth() - mDragView.getWidth();
final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
return newLeft;
}
- 其他事件处理
滑动边缘事件检测
分为滑动左边缘还是右边缘:EDGE_LEFT 和 EDGE_RIGHT,下面的代码设置了可以处理滑动左边缘:
mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
如上,我们设置为左边缘检测,当 onEdgeTouched 方法会在左边缘滑动的时候被调用,这种情况下一般都是没有和子 view 接触的情况。
@Override
public void onEdgeTouched(int edgeFlags, int pointerId) {
super.onEdgeTouched(edgeFlags, pointerId);
Toast.makeText(getContext(), "edgeTouched", Toast.LENGTH_SHORT).show();
}
如果你想在边缘滑动的时候根据滑动距离移动一个子 view,可以通过实现 onEdgeDragStarted 方法,并在 onEdgeDragStarted 方法中手动指定要移动的子 View,如之前仿音悦台的页面交互就用到了子 View 的检测。
ViewDragHelper 实战
其实就之前是的的仿音悦台的页面交互效果吧,在 13 年就有国外的大神实现了 https://github.com/flavienlaurent/flavienlaurent.com
我们来看一段完整的代码:
public class YoutubeLayout extends ViewGroup {
private final ViewDragHelper mDragHelper;
private View mHeaderView;
private View mDescView;
private float mInitialMotionX;
private float mInitialMotionY;
private int mDragRange;
private int mTop;
private float mDragOffset;
public YoutubeLayout(Context context) {
this(context, null);
}
public YoutubeLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
@Override
protected void onFinishInflate() {
mHeaderView = findViewById(R.id.viewHeader);
mDescView = findViewById(R.id.viewDesc);
}
public YoutubeLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mDragHelper = ViewDragHelper.create(this, 1f, new DragHelperCallback());
}
public void maximize() {
smoothSlideTo(0f);
}
boolean smoothSlideTo(float slideOffset) {
final int topBound = getPaddingTop();
int y = (int) (topBound + slideOffset * mDragRange);
if (mDragHelper.smoothSlideViewTo(mHeaderView, mHeaderView.getLeft(), y)) {
ViewCompat.postInvalidateOnAnimation(this);
return true;
}
return false;
}
private class DragHelperCallback extends ViewDragHelper.Callback {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child == mHeaderView;
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
mTop = top;
mDragOffset = (float) top / mDragRange;
mHeaderView.setPivotX(mHeaderView.getWidth());
mHeaderView.setPivotY(mHeaderView.getHeight());
mHeaderView.setScaleX(1 - mDragOffset / 2);
mHeaderView.setScaleY(1 - mDragOffset / 2);
mDescView.setAlpha(1 - mDragOffset);
requestLayout();
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
int top = getPaddingTop();
if (yvel > 0 || (yvel == 0 && mDragOffset > 0.5f)) {
top += mDragRange;
}
mDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top);
}
@Override
public int getViewVerticalDragRange(View child) {
return mDragRange;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
final int topBound = getPaddingTop();
final int bottomBound = getHeight() - mHeaderView.getHeight() - mHeaderView.getPaddingBottom();
final int newTop = Math.min(Math.max(top, topBound), bottomBound);
return newTop;
}
}
@Override
public void computeScroll() {
if (mDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
if (( action != MotionEvent.ACTION_DOWN)) {
mDragHelper.cancel();
return super.onInterceptTouchEvent(ev);
}
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mDragHelper.cancel();
return false;
}
final float x = ev.getX();
final float y = ev.getY();
boolean interceptTap = false;
switch (action) {
case MotionEvent.ACTION_DOWN: {
mInitialMotionX = x;
mInitialMotionY = y;
interceptTap = mDragHelper.isViewUnder(mHeaderView, (int) x, (int) y);
break;
}
case MotionEvent.ACTION_MOVE: {
final float adx = Math.abs(x - mInitialMotionX);
final float ady = Math.abs(y - mInitialMotionY);
final int slop = mDragHelper.getTouchSlop();
if (ady > slop && adx > ady) {
mDragHelper.cancel();
return false;
}
}
}
return mDragHelper.shouldInterceptTouchEvent(ev) || interceptTap;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
mDragHelper.processTouchEvent(ev);
final int action = ev.getAction();
final float x = ev.getX();
final float y = ev.getY();
boolean isHeaderViewUnder = mDragHelper.isViewUnder(mHeaderView, (int) x, (int) y);
switch (action & MotionEventCompat.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
mInitialMotionX = x;
mInitialMotionY = y;
break;
}
case MotionEvent.ACTION_UP: {
final float dx = x - mInitialMotionX;
final float dy = y - mInitialMotionY;
final int slop = mDragHelper.getTouchSlop();
if (dx * dx + dy * dy < slop * slop && isHeaderViewUnder) {
if (mDragOffset == 0) {
smoothSlideTo(1f);
} else {
smoothSlideTo(0f);
}
}
break;
}
}
return isHeaderViewUnder && isViewHit(mHeaderView, (int) x, (int) y) || isViewHit(mDescView, (int) x, (int) y);
}
private boolean isViewHit(View view, int x, int y) {
int[] viewLocation = new int[2];
view.getLocationOnScreen(viewLocation);
int[] parentLocation = new int[2];
this.getLocationOnScreen(parentLocation);
int screenX = parentLocation[0] + x;
int screenY = parentLocation[1] + y;
return screenX >= viewLocation[0] && screenX < viewLocation[0] + view.getWidth() &&
screenY >= viewLocation[1] && screenY < viewLocation[1] + view.getHeight();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
int maxWidth = MeasureSpec.getSize(widthMeasureSpec);
int maxHeight = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, 0),
resolveSizeAndState(maxHeight, heightMeasureSpec, 0));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mDragRange = getHeight() - mHeaderView.getHeight();
mHeaderView.layout(
0,
mTop,
r,
mTop + mHeaderView.getMeasuredHeight());
mDescView.layout(
0,
mTop + mHeaderView.getMeasuredHeight(),
r,
mTop + b);
}
页面引用 xml
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:tag="list"
/>
<com.example.vdh.YoutubeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/youtubeLayout"
android:orientation="vertical"
android:visibility="visible">
<TextView
android:id="@+id/viewHeader"
android:layout_width="match_parent"
android:layout_height="128dp"
android:fontFamily="sans-serif-thin"
android:textSize="25sp"
android:tag="text"
android:gravity="center"
android:textColor="@android:color/white"
android:background="#AD78CC"/>
<TextView
android:id="@+id/viewDesc"
android:tag="desc"
android:textSize="35sp"
android:gravity="center"
android:text="Loreum Loreum"
android:textColor="@android:color/white"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF00FF"/>
</com.example.vdh.YoutubeLayout>
</FrameLayout>
其实就是两个子 2View,ViewDragHelper 的事件检测,然后回调里面的方法 进行页面的 Onlayout,进而控制页面刷新等等。
本文同步分享在 博客 “xiangzhihong8”(CSDN)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与 “OSC 源创计划”,欢迎正在阅读的你也加入,一起分享。
Android ViewDragHelper 实现 QQ 侧滑边栏
Android ViewDragHelper 实现 QQ 侧滑边栏
移动手机版的 QQ 的左边侧栏,有一个特殊的交互设计效果:当用户手指向右或向左滑动时,QQ 的左边会弹出或收缩一个侧滑的边栏。这种效果简单的做法可以使用 Android 提供的 NavigationView,DrawerLayout 实现之(附录文章 1,2,3),但如果要想做到 QQ 那样的精致效果,或许得使用 Android ViewDragHelper。
如图所示,即为使用 Android ViewDragHelper,仿照 QQ 移动版的样式做出的交互效果:
工程的全部源代码我已经打包上传到 CSDN 供感兴趣者下载,CSDN 下载链接地址:http://download.csdn.net/download/zhangphil/9223701
下载解压后,直接导入到 Eclipse,作为一个 Android application 运行即可。
标题栏中左上角的动画 shake,具体详情参考附录文章 4。
在 MainActivity.java 中,加载图标使用了 Glide,关于 Glide 的内容,参考附录文章 5,6。
附录我写一部分参考文章:
【文章 1】《Android Material Design: NavigationView 抽屉导航菜单》链接地址:http://blog.csdn.net/zhangphil/article/details/48931221
【文章 2】《基于 Android 官方 DrawerLayout 实现抽屉导航菜单》链接地址:http://blog.csdn.net/zhangphil/article/details/48710453
【文章 3】《集成 Android SlidingMenu(SlideMenu)》链接地址:http://blog.csdn.net/zhangphil/article/details/44078805
【文章 4】《Android 动画循环弹动 cycleInterpolator》链接地址:http://blog.csdn.net/zhangphil/article/details/49449935
【文章 5】《基于开源框架 Glide 加载 Gif 资源图到 Android ImageView 中》链接地址:http://blog.csdn.net/zhangphil/article/details/45561983
【文章 6】《Android 图片加载与缓存开源框架:Android Glide》链接地址:http://blog.csdn.net/zhangphil/article/details/45535693
Android ViewDragHelper 源码解析
在自定义 ViewGroup 的过程中,如果涉及到 View 的拖动滑动,ViewDragHelper 的使用应该是少不了的,它提供了一系列用于用户拖动子 View 的辅助方法和相关的状态记录,像 Navigation Drawer 的边缘滑动、QQ5.x 的侧滑菜单、知乎里的页面滑动返回都可以由它实现,所以有必要完全掌握它的使用。
要想完全掌握 ViewDragHelper 的使用和原理,最好的办法就是读懂它的源码,所以就有了这篇分析,以便在印象模糊之时可以再次快速回顾 ViewDragHelper 的原理、用法、注意事项等。
基本用法
在自定义 ViewGroup 的构造方法里调用 ViewDragHelper 的静态工厂方法 create () 创建 ViewDragHelper 实例
实现 ViewDragHelper.Callback
最重要的几个方法是 tryCaptureView ()、clampViewPositionVertical ()、clampViewPositionHorizontal ()、getViewHorizontalDragRange ()、getViewVerticalDragRange ()tryCaptureView () 里会传递当前触摸区域下的子 View 实例作为参数,如果需要对当前触摸的子 View 进行拖拽移动就返回 true,否则返回 false。
clampViewPositionVertical () 决定了要拖拽的子 View 在垂直方向上应该移动到的位置,该方法会传递三个参数:要拖拽的子 View 实例、期望的移动后位置子 View 的 top 值、移动的距离。返回值为子 View 在最终位置时的 top 值,一般直接返回第二个参数即可。
clampViewPositionHorizontal () 与 clampViewPositionVertical () 同理,只不过是发生在水平方向上,最终返回的是 View 的 left 值。
getViewVerticalDragRange () 要返回一个大于 0 的数,才会在在垂直方向上对触摸到的 View 进行拖动。
getViewHorizontalDragRange () 与 getViewVerticalDragRange () 同理,只不过是发生在水平方向上。
在 onInterceptTouchEvent () 方法里调用并返回 ViewDragHelper 的 shouldInterceptTouchEvent () 方法
在 onTouchEvent () 方法里调用 ViewDragHelper () 的 processTouchEvent () 方法。ACTION_DOWN 事件发生时,如果当前触摸点下要拖动的子 View 没有消费事件,此时应该在 onTouchEvent () 返回 true,否则将收不到后续事件,不会产生拖动。
上面几个步骤已经实现了子 View 拖动的效果,如果还想要实现 fling 效果(滑动时松手后以一定速率继续自动滑动下去并逐渐停止,类似于扔东西)或者松手后自动滑动到指定位置,需要实现自定义 ViewGroup 的 computeScroll () 方法,方法实现如下:
@Override
public void computeScroll() {
if (mDragHelper.continueSettling(true)) {
postInvalidate();
}
}
并在 ViewDragHelper.Callback 的 onViewReleased () 方法里调用 settleCapturedViewAt ()、flingCapturedView (),或在任意地方调用 smoothSlideViewTo () 方法。
如果要实现边缘拖动的效果,需要调用 ViewDragHelper 的 setEdgeTrackingEnabled () 方法,注册想要监听的边缘。然后实现 ViewDragHelper.Callback 里的 onEdgeDragStarted () 方法,在此手动调用 captureChildView () 传递要拖动的子 View。
具体的使用 Demo 请见最后面公布的几个案例。
源码详解
ViewDragHelper 的完整源码可在 GitHub 或 GrepCode 上在线查看。在最后的总结部分,我画了简单的流程图,梳理了整个触摸事件传递过重中相关方法的调用,有需要的就先去总结部分看看。
预备知识
了解 View 的坐标系统,Android View 坐标 getLeft, getRight, getTop, getBottom
了解 MotionEvent 中关于多点触控的机制,android 触控,先了解 MotionEvent (一)
了解 Scroller 类原理,Android 中滑屏实现 ---- 手把手教你如何实现触摸滑屏以及 Scroller 类详解
了解 Touch 事件的分发机制,Andriod 从源码的角度详解 View,ViewGroup 的 Touch 事件的分发机制
ViewDragHelper 实例的创建
ViewDragHelper 重载了两个 create () 静态方法,先看两个参数的 create () 方法:
/**
* 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);
}
create () 的两个参数很好理解,第一个是我们自定义的 ViewGroup,第二个是控制子 View 拖拽需要的回调对象。create () 直接调用了 ViewDragHelper 构造方法,我们再来看看这个构造方法。
/**
* Apps should use ViewDragHelper.create() to get a new instance.
* This will allow VDH to use internal compatibility implementations for different
* platform versions.
*
* @param context Context to initialize config-dependent params from
* @param forParent Parent view to monitor
*/
private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) {
if (forParent == null) {
throw new IllegalArgumentException("Parent view may not be null");
}
if (cb == null) {
throw new IllegalArgumentException("Callback may not be null");
}
mParentView = forParent;
mCallback = cb;
final ViewConfiguration vc = ViewConfiguration.get(context);
final float density = context.getResources().getDisplayMetrics().density;
mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);
mTouchSlop = vc.getScaledTouchSlop();
mMaxVelocity = vc.getScaledMaximumFlingVelocity();
mMinVelocity = vc.getScaledMinimumFlingVelocity();
mScroller = ScrollerCompat.create(context, sInterpolator);
}
这个构造函数是私有的,也是仅有的构造函数,所以外部只能通过 create () 工厂方法来创建 ViewDragHelper 实例了。这里要求了我们传递的自定义 ViewGroup 和回调对象不能为空,否则会直接抛出异常中断程序。在这里也初始化了一些触摸滑动需要的参考值和辅助类。
mParentView 和 mCallback 分别保存传递过来的对应参数
ViewConfiguration 类里定义了 View 相关的一系列时间、大小、距离等常量
mEdgeSize 表示边缘触摸的范围。例如 mEdgeSize 为 20dp 并且用户注册监听了左侧边缘触摸时,触摸点的 x 坐标小于 mParentView.getLeft () + mEdgeSize 时(即触摸点在容器左边界往右 20dp 内)就算做是左侧的边缘触摸,详见 ViewDragHelper 的 getEdgesTouched () 方法。
mTouchSlop 是一个很小的距离值,只有在前后两次触摸点的距离超过 mTouchSlop 的值时,我们才把这两次触摸算作是 “滑动”,我们只在此时进行滑动处理,否则任何微小的距离的变化我们都要处理的话会显得太频繁,如果处理过程又比较复杂耗时就会使界面产生卡顿。
mMaxVelocity、mMinVelocity 是 fling 时的最大、最小速率,单位是像素每秒。
mScroller 是 View 滚动的辅助类,该类的详细解析参见下面几篇文章
Android 中滑屏实现 ---- 手把手教你如何实现触摸滑屏以及 Scroller 类详解
Android 中 Scroller 类的分析
再看三个参数的 create () 方法:
/**
* 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;
}
第二个参数 sensitivity 是用来调节 mTouchSlop 的值。sensitivity 越大,mTouchSlop 越小,对滑动的检测就越敏感。例如 sensitivity 为 1 时,前后触摸点距离超过 20dp 才进行滑动处理,现在 sensitivity 为 2 的话,前后触摸点距离超过 10dp 就进行处理了。
对 Touch 事件的处理
当 mParentView(自定义 ViewGroup)被触摸时,首先会调用 mParentView 的 onInterceptTouchEvent (MotionEvent ev),接着就调用 shouldInterceptTouchEvent (MotionEvent ev) ,所以先来看看这个方法的 ACTION_DOWN 部分:
/**
* Check if this event as provided to the parent view''s onInterceptTouchEvent should
* cause the parent to intercept the touch event stream.
*
* @param ev MotionEvent provided to onInterceptTouchEvent
* @return true if the parent view should return true from onInterceptTouchEvent
*/
public boolean shouldInterceptTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
final int actionIndex = MotionEventCompat.getActionIndex(ev);
if (action == MotionEvent.ACTION_DOWN) {
// Reset things for a new event stream, just in case we didn''t get
// the whole previous stream.
cancel();
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
switch (action) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
final int pointerId = MotionEventCompat.getPointerId(ev, 0);
saveInitialMotion(x, y, pointerId);
final View toCapture = findTopChildUnder((int) x, (int) y);
// Catch a settling view if possible.
if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
tryCaptureViewForDrag(toCapture, pointerId);
}
final int edgesTouched = mInitialEdgesTouched[pointerId];
if ((edgesTouched & mTrackingEdges) != 0) {
mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
}
break;
}
// 其他case暂且省略
}
return mDragState == STATE_DRAGGING;
}
看 9~21 行,首先是关于多点触控(MotionEvent 的 actionIndex、ACTION_POINTER_DOWN 等概念),不明白的请参阅 android 触控,先了解 MotionEvent (一)。
mVelocityTracker 记录下触摸的各个点信息,稍后可以用来计算本次滑动的速率,每次发生 ACTION_DOWN 事件都会调用 cancel (),而在 cancel () 方法里 mVelocityTracker 又被清空了,所以 mVelocityTracker 记录下的是本次 ACTION_DOWN 事件直至 ACTION_UP 事件发生后(下次 ACTION_DOWN 事件发生前)的所有触摸点的信息。
再来看 24~42 行 case MotionEvent.ACTION_DOWN 部分,先是调用 saveInitialMotion (x, y, pointerId) 保存手势的初始信息,即 ACTION_DOWN 发生时的触摸点坐标(x、y)、触摸手指编号(pointerId),如果触摸到了 mParentView 的边缘还会记录触摸的是哪个边缘。接着调用 findTopChildUnder ((int) x, (int) y); 来获取当前触摸点下最顶层的子 View,看 findTopChildUnder 的源码:
/**
* Find the topmost child under the given point within the parent view''s coordinate system.
* The child order is determined using {@link Callback#getOrderedChildIndex(int)}.
*
* @param x X position to test in the parent''s coordinate system
* @param y Y position to test in the parent''s coordinate system
* @return The topmost child view under (x, y) or null if none found.
*/
public View findTopChildUnder(int x, int y) {
final int childCount = mParentView.getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i));
if (x >= child.getLeft() && x < child.getRight() &&
y >= child.getTop() && y < child.getBottom()) {
return child;
}
}
return null;
}
代码很简单,注释里也说明的很清楚了。如果在同一个位置有两个子 View 重叠,想要让下层的子 View 被选中,那么就要实现 Callback 里的 getOrderedChildIndex (int index) 方法来改变查找子 View 的顺序;例如 topView(上层 View)的 index 是 4,bottomView(下层 View)的 index 是 3,按照正常的遍历查找方式(getOrderedChildIndex () 默认直接返回 index),会选择到 topView,要想让 bottomView 被选中就得这么写:
public int getOrderedChildIndex(int index) {
int indexTop = mParentView.indexOfChild(topView);
int indexBottom = mParentView.indexOfChild(bottomView);
if (index == indexTop) {
return indexBottom;
}
return index;
}
32~35 行,这里还看到了一个 mDragState 成员变量,它共有三种取值:
STATE_IDLE:所有的 View 处于静止空闲状态
STATE_DRAGGING:某个 View 正在被用户拖动(用户正在与设备交互)
STATE_SETTLING:某个 View 正在安置状态中(用户并没有交互操作),就是自动滚动的过程中
mCapturedView 默认为 null,所以一开始不会执行这里的代码,mDragState 处于 STATE_SETTLING 状态时才会执行 tryCaptureViewForDrag (),执行的情况到后面再分析,这里先跳过。
37~40 行调用了 Callback.onEdgeTouched 向外部通知 mParentView 的某些边缘被触摸到了,mInitialEdgesTouched 是在刚才调用过的 saveInitialMotion 方法里进行赋值的。
ACTION_DOWN 部分处理完了,跳过 switch 语句块,剩下的代码就只有 return mDragState == STATE_DRAGGING;。在 ACTION_DOWN 部分没有对 mDragState 进行赋值,其默认值为 STATE_IDLE,所以此处返回 false。
那么返回 false 后接下来应该是会调用哪个方法呢,根据 Andriod 从源码的角度详解 View,ViewGroup 的 Touch 事件的分发机制里的解析,接下来会在 mParentView 的所有子 View 中寻找响应这个 Touch 事件的 View(会调用每个子 View 的 dispatchTouchEvent () 方法,dispatchTouchEvent 里一般又会调用 onTouchEvent ());
如果没有子 View 消费这次事件(子 View 的 dispatchTouchEvent () 返回都是 false),会调用 mParentView 的 super.dispatchTouchEvent (ev),即 View 中的 dispatchTouchEvent (ev),然后调用 mParentView 的 onTouchEvent () 方法,再调用 ViewDragHelper 的 processTouchEvent (MotionEvent ev) 方法。此时(ACTION_DOWN 事件发生时)mParentView 的 onTouchEvent () 要返回 true,onTouchEvent () 才能继续接受到接下来的 ACTION_MOVE、ACTION_UP 等事件,否则无法完成拖动(除了 ACTION_DOWN 外的其他事件发生时返回 true 或 false 都不会影响接下来的事件接受),因为拖动的相关代码是写在 processTouchEvent () 里的 ACTION_MOVE 部分的。要注意的是返回 true 后 mParentView 的 onInterceptTouchEvent () 就不会收到后续的 ACTION_MOVE、ACTION_UP 等事件了。
如果有子 View 消费了本次 ACTION_DOWN 事件,mParentView 的 onTouchEvent () 就收不到 ACTION_DOWN 事件了,也就是 ViewDragHelper 的 processTouchEvent (MotionEvent ev) 收不到 ACTION_DOWN 事件了。不过只要该 View 没有调用过 requestDisallowInterceptTouchEvent (true),mParentView 的 onInterceptTouchEvent () 的 ACTION_MOVE 部分还是会执行的,如果在此时返回了 true 拦截了 ACTION_MOVE 事件,processTouchEvent () 里的 ACTION_MOVE 部分也就会正常执行,拖动也就没问题了。onInterceptTouchEvent () 的 ACTION_MOVE 部分具体做了怎样的处理,稍后再来解析。
接下来对这两种情况逐一解析。
假设没有子 View 消费这次事件,根据刚才的分析最终就会调用 processTouchEvent (MotionEvent ev) 的 ACTION_DOWN 部分:
/**
* Process a touch event received by the parent view. This method will dispatch callback events
* as needed before returning. The parent view''s onTouchEvent implementation should call this.
*
* @param ev The touch event received by the parent view
*/
public void processTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
final int actionIndex = MotionEventCompat.getActionIndex(ev);
if (action == MotionEvent.ACTION_DOWN) {
// Reset things for a new event stream, just in case we didn''t get
// the whole previous stream.
cancel();
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
switch (action) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
final int pointerId = MotionEventCompat.getPointerId(ev, 0);
final View toCapture = findTopChildUnder((int) x, (int) y);
saveInitialMotion(x, y, pointerId);
// Since the parent is already directly processing this touch event,
// there is no reason to delay for a slop before dragging.
// Start immediately if possible.
tryCaptureViewForDrag(toCapture, pointerId);
final int edgesTouched = mInitialEdgesTouched[pointerId];
if ((edgesTouched & mTrackingEdges) != 0) {
mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
}
break;
}
// 其他case暂且省略
}
}
这段代码跟 shouldInterceptTouchEvent () 里 ACTION_DOWN 那部分基本一致,唯一区别就是这里没有约束条件直接调用了 tryCaptureViewForDrag () 方法,现在来看看这个方法:
/**
* Attempt to capture the view with the given pointer ID. The callback will be involved.
* This will put us into the "dragging" state. If we''ve already captured this view with
* this pointer this method will immediately return true without consulting the callback.
*
* @param toCapture View to capture
* @param pointerId Pointer to capture with
* @return true if capture was successful
*/
boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
if (toCapture == mCapturedView && mActivePointerId == pointerId) {
// Already done!
return true;
}
if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
mActivePointerId = pointerId;
captureChildView(toCapture, pointerId);
return true;
}
return false;
}
这里调用了 Callback 的 tryCaptureView (View child, int pointerId) 方法,把当前触摸到的 View 和触摸手指编号传递了过去,在 tryCaptureView () 中决定是否需要拖动当前触摸到的 View,如果要拖动当前触摸到的 View 就在 tryCaptureView () 中返回 true,让 ViewDragHelper 把当前触摸的 View 捕获下来,接着就调用了 captureChildView (toCapture, pointerId) 方法:
/**
* Capture a specific child view for dragging within the parent. The callback will be notified
* but {@link Callback#tryCaptureView(android.view.View, int)} will not be asked permission to
* capture this view.
*
* @param childView Child view to capture
* @param activePointerId ID of the pointer that is dragging the captured child view
*/
public void captureChildView(View childView, int activePointerId) {
if (childView.getParent() != mParentView) {
throw new IllegalArgumentException("captureChildView: parameter must be a descendant " +
"of the ViewDragHelper''s tracked parent view (" + mParentView + ")");
}
mCapturedView = childView;
mActivePointerId = activePointerId;
mCallback.onViewCaptured(childView, activePointerId);
setDragState(STATE_DRAGGING);
}
代码很简单,在 captureChildView (toCapture, pointerId) 中将要拖动的 View 和触摸的手指编号记录下来,并调用 Callback 的 onViewCaptured (childView, activePointerId) 通知外部有子 View 被捕获到了,再调用 setDragState () 设置当前的状态为 STATE_DRAGGING,看 setDragState () 源码:
void setDragState(int state) {
mParentView.removeCallbacks(mSetIdleRunnable);
if (mDragState != state) {
mDragState = state;
mCallback.onViewDragStateChanged(state);
if (mDragState == STATE_IDLE) {
mCapturedView = null;
}
}
}
状态改变后会调用 Callback 的 onViewDragStateChanged () 通知状态的变化。
假设 ACTION_DOWN 发生后在 mParentView 的 onTouchEvent () 返回了 true,接下来就会执行 ACTION_MOVE 部分:
/**
* Process a touch event received by the parent view. This method will dispatch callback events
* as needed before returning. The parent view''s onTouchEvent implementation should call this.
*
* @param ev The touch event received by the parent view
*/
public void processTouchEvent(MotionEvent ev) {
switch (action) {
// 省略其他case...
case MotionEvent.ACTION_MOVE: {
if (mDragState == STATE_DRAGGING) {
final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, index);
final float y = MotionEventCompat.getY(ev, index);
final int idx = (int) (x - mLastMotionX[mActivePointerId]);
final int idy = (int) (y - mLastMotionY[mActivePointerId]);
dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);
saveLastMotion(ev);
} else {
// Check to see if any pointer is now over a draggable view.
final int pointerCount = MotionEventCompat.getPointerCount(ev);
for (int i = 0; i < pointerCount; i++) {
final int pointerId = MotionEventCompat.getPointerId(ev, i);
final float x = MotionEventCompat.getX(ev, i);
final float y = MotionEventCompat.getY(ev, i);
final float dx = x - mInitialMotionX[pointerId];
final float dy = y - mInitialMotionY[pointerId];
reportNewEdgeDrags(dx, dy, pointerId);
if (mDragState == STATE_DRAGGING) {
// Callback might have started an edge drag.
break;
}
final View toCapture = findTopChildUnder((int) x, (int) y);
if (checkTouchSlop(toCapture, dx, dy) &&
tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
saveLastMotion(ev);
}
break;
}
// 省略其他case...
}
}
要注意的是,如果一直没松手,这部分代码会一直调用。这里先判断 mDragState 是否为 STATE_DRAGGING,而唯一调用 setDragState (STATE_DRAGGING) 的地方就是 tryCaptureViewForDrag () 了,刚才在 ACTION_DOWN 里调用过 tryCaptureViewForDrag (),现在又要分两种情况。
如果刚才在 ACTION_DOWN 里捕获到要拖动的 View,那么就执行 if 部分的代码,这个稍后解析,先考虑没有捕获到的情况。没有捕获到的话,mDragState 依然是 STATE_IDLE,然后会执行 else 部分的代码。这里主要就是检查有没有哪个手指触摸到了要拖动的 View 上,触摸上了就尝试捕获它,然后让 mDragState 变为 STATE_DRAGGING,之后就会执行 if 部分的代码了。这里还有两个方法涉及到了 Callback 里的方法,需要来解析一下,分别是 reportNewEdgeDrags () 和 checkTouchSlop (),先看 reportNewEdgeDrags ():
private void reportNewEdgeDrags(float dx, float dy, int pointerId) {
int dragsStarted = 0;
if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) {
dragsStarted |= EDGE_LEFT;
}
if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) {
dragsStarted |= EDGE_TOP;
}
if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_RIGHT)) {
dragsStarted |= EDGE_RIGHT;
}
if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) {
dragsStarted |= EDGE_BOTTOM;
}
if (dragsStarted != 0) {
mEdgeDragsInProgress[pointerId] |= dragsStarted;
mCallback.onEdgeDragStarted(dragsStarted, pointerId);
}
}
这里对四个边缘都做了一次检查,检查是否在某些边缘产生拖动了,如果有拖动,就将有拖动的边缘记录在 mEdgeDragsInProgress 中,再调用 Callback 的 onEdgeDragStarted (int edgeFlags, int pointerId) 通知某个边缘开始产生拖动了。虽然 reportNewEdgeDrags () 会被调用很多次(因为 processTouchEvent () 的 ACTION_MOVE 部分会执行很多次),但 mCallback.onEdgeDragStarted (dragsStarted, pointerId) 只会调用一次,具体的要看 checkNewEdgeDrag () 这个方法:
private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) {
final float absDelta = Math.abs(delta);
final float absODelta = Math.abs(odelta);
if ((mInitialEdgesTouched[pointerId] & edge) != edge || (mTrackingEdges & edge) == 0 ||
(mEdgeDragsLocked[pointerId] & edge) == edge ||
(mEdgeDragsInProgress[pointerId] & edge) == edge ||
(absDelta <= mTouchSlop && absODelta <= mTouchSlop)) {
return false;
}
if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) {
mEdgeDragsLocked[pointerId] |= edge;
return false;
}
return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop;
}
checkNewEdgeDrag () 返回 true 表示在指定的 edge(边缘)开始产生拖动了。
方法的两个参数 delta 和 odelta 需要解释一下,odelta 里的 o 应该代表 opposite,这是什么意思呢,以 reportNewEdgeDrags () 里调用 checkNewEdgeDrag (dx, dy, pointerId, EDGE_LEFT) 为例,我们要监测左边缘的触摸情况,所以主要监测的是 x 轴方向上的变化,这里 delta 为 dx,odelta 为 dy,也就是说 delta 是指我们主要监测的方向上的变化,odelta 是另外一个方向上的变化,后面要判断假另外一个方向上的变化是否要远大于主要方向上的变化,所以需要另外一个方向上的距离变化的值。
mInitialEdgesTouched 是在 ACTION_DOWN 部分的 saveInitialMotion () 里生成的,ACTION_DOWN 发生时触摸到的边缘会被记录在 mInitialEdgesTouched 中。如果 ACTION_DOWN 发生时没有触摸到边缘,或者触摸到的边缘不是指定的 edge,就直接返回 false 了。
mTrackingEdges 是由 setEdgeTrackingEnabled (int edgeFlags) 设置的,当我们想要追踪监听边缘触摸时才需要调用 setEdgeTrackingEnabled (int edgeFlags),如果我们没有调用过它,这里就直接返回 false 了。
mEdgeDragsLocked 它在这个方法里被引用了多次,它在整个 ViewDragHelper 里唯一被赋值的地方就是这里的第 12 行,所以默认值是 0,第 6 行 mEdgeDragsLocked [pointerId] & edge) == edge 执行的结果是 false。我们再跳到 11 到 14 行看看,absDelta < absODelta * 0.5f 的意思是检查在次要方向上移动的距离是否远超过主要方向上移动的距离,如果是再调用 Callback 的 onEdgeLock (edge) 检查是否需要锁定某个边缘,如果锁定了某个边缘,那个边缘就算触摸到了也不会被记录在 mEdgeDragsInProgress 里了,也不会收到 Callback 的 onEdgeDragStarted () 通知了。并且将锁定的边缘记录在 mEdgeDragsLocked 变量里,再次调用本方法时就会在第 6 行进行判断了,第 6 行里如果检测到给定的 edge 被锁定,就直接返回 false 了。
回到第 7 行的 (mEdgeDragsInProgress [pointerId] & edge) == edge,mEdgeDragsInProgress 是保存已发生过拖动事件的边缘的,如果给定的 edge 已经保存过了,那就没必要再检测其他东西了,直接返回 false 了。
第 8 行 (absDelta <= mTouchSlop && absODelta <= mTouchSlop) 很简单了,就是检查本次移动的距离是不是太小了,太小就不处理了。
最后一句返回的时候再次检查给定的 edge 有没有记录过,确保了每个边缘只会调用一次 reportNewEdgeDrags 的 mCallback.onEdgeDragStarted (dragsStarted, pointerId)
再来看 checkTouchSlop () 方法:
/**
* Check if we''ve crossed a reasonable touch slop for the given child view.
* If the child cannot be dragged along the horizontal or vertical axis, motion
* along that axis will not count toward the slop check.
*
* @param child Child to check
* @param dx Motion since initial position along X axis
* @param dy Motion since initial position along Y axis
* @return true if the touch slop has been crossed
*/
private boolean checkTouchSlop(View child, float dx, float dy) {
if (child == null) {
return false;
}
final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0;
final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0;
if (checkHorizontal && checkVertical) {
return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
} else if (checkHorizontal) {
return Math.abs(dx) > mTouchSlop;
} else if (checkVertical) {
return Math.abs(dy) > mTouchSlop;
}
return false;
}
这个方法主要就是检查手指移动的距离有没有超过触发处理移动事件的最短距离(mTouchSlop)了,注意 dx 和 dy 指的是当前触摸点到 ACTION_DOWN 触摸到的点的距离。这里先检查 Callback 的 getViewHorizontalDragRange (child) 和 getViewVerticalDragRange (child) 是否大于 0,如果想让某个 View 在某个方向上滑动,就要在那个方向对应的方法里返回大于 0 的数。否则在 processTouchEvent () 的 ACTION_MOVE 部分就不会调用 tryCaptureViewForDrag () 来捕获当前触摸到的 View 了,拖动也就没办法进行了。
回到 processTouchEvent () 的 ACTION_MOVE 部分,假设现在我们的手指已经滑动到可以被捕获到的 View 上了,也都正常的实现了 Callback 中的相关方法,让 tryCaptureViewForDrag () 正常的捕获到触摸到的 View 了,下一次 ACTION_MOVE 时就执行 if 部分的代码了,也就是开始不停的调用 dragTo () 对 mCaptureView 进行真正拖动了,看 dragTo () 方法:
private void dragTo(int left, int top, int dx, int dy) {
int clampedX = left;
int clampedY = top;
final int oldLeft = mCapturedView.getLeft();
final int oldTop = mCapturedView.getTop();
if (dx != 0) {
clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
mCapturedView.offsetLeftAndRight(clampedX - oldLeft);
}
if (dy != 0) {
clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
mCapturedView.offsetTopAndBottom(clampedY - oldTop);
}
if (dx != 0 || dy != 0) {
final int clampedDx = clampedX - oldLeft;
final int clampedDy = clampedY - oldTop;
mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY,
clampedDx, clampedDy);
}
}
参数 dx 和 dy 是前后两次 ACTION_MOVE 移动的距离,left 和 top 分别为 mCapturedView.getLeft () + dx, mCapturedView.getTop () + dy,也就是期望的移动后的坐标,对 View 的 getLeft () 等方法不理解的请参阅 Android View 坐标 getLeft, getRight, getTop, getBottom。
这里通过调用 offsetLeftAndRight () 和 offsetTopAndBottom () 来完成对 mCapturedView 移动,这两个是 View 中定义的方法,看它们的源码就知道内部是通过改变 View 的 mLeft、mRight、mTop、mBottom,即改变 View 在父容器中的坐标位置,达到移动 View 的效果,所以如果调用 mCapturedView 的 layout (int l, int t, int r, int b) 方法也可以实现移动 View 的效果。
具体要移动到哪里,由 Callback 的 clampViewPositionHorizontal () 和 clampViewPositionVertical () 来决定的,如果不想在水平方向上移动,在 clampViewPositionHorizontal (View child, int left, int dx) 里直接返回 child.getLeft () 就可以了,这样 clampedX - oldLeft 的值为 0,这里调用 mCapturedView.offsetLeftAndRight (clampedX - oldLeft) 就不会起作用了。垂直方向上同理。
最后会调用 Callback 的 onViewPositionChanged (mCapturedView, clampedX, clampedY,clampedDx, clampedDy) 通知捕获到的 View 位置改变了,并把最终的坐标(clampedX、clampedY)和最终的移动距离(clampedDx、 clampedDy)传递过去。
ACTION_MOVE 部分就算告一段落了,接下来应该是用户松手触发 ACTION_UP,或者是达到某个条件导致后续的 ACTION_MOVE 被 mParentView 的上层 View 给拦截了而收到 ACTION_CANCEL,一起来看这两个部分:
public void processTouchEvent(MotionEvent ev) {
// 省略
switch (action) {
// 省略其他case
case MotionEvent.ACTION_UP: {
if (mDragState == STATE_DRAGGING) {
releaseViewForPointerUp();
}
cancel();
break;
}
case MotionEvent.ACTION_CANCEL: {
if (mDragState == STATE_DRAGGING) {
dispatchViewReleased(0, 0);
}
cancel();
break;
}
}
}
这两个部分都是重置所有的状态记录,并通知 View 被放开了,再看下 releaseViewForPointerUp () 和 dispatchViewReleased () 的源码:
private void releaseViewForPointerUp() {
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
final float xvel = clampMag(
VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
mMinVelocity, mMaxVelocity);
final float yvel = clampMag(
VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId),
mMinVelocity, mMaxVelocity);
dispatchViewReleased(xvel, yvel);
}
releaseViewForPointerUp () 里也调用了 dispatchViewReleased (),只不过传递了速率给它,这个速率就是由 processTouchEvent () 的 mVelocityTracker 追踪算出来的。再看 dispatchViewReleased ():
/**
* Like all callback events this must happen on the UI thread, but release
* involves some extra semantics. During a release (mReleaseInProgress)
* is the only time it is valid to call {@link #settleCapturedViewAt(int, int)}
* or {@link #flingCapturedView(int, int, int, int)}.
*/
private void dispatchViewReleased(float xvel, float yvel) {
mReleaseInProgress = true;
mCallback.onViewReleased(mCapturedView, xvel, yvel);
mReleaseInProgress = false;
if (mDragState == STATE_DRAGGING) {
// onViewReleased didn''t call a method that would have changed this. Go idle.
setDragState(STATE_IDLE);
}
}
这里调用 Callback 的 onViewReleased (mCapturedView, xvel, yvel) 通知外部捕获到的 View 被释放了,而在 onViewReleased () 前后有个 mReleaseInProgress 值得注意,注释里说唯一可以调用 ViewDragHelper 的 settleCapturedViewAt () 和 flingCapturedView () 的地方就是在 Callback 的 onViewReleased () 里了。
首先这两个方法是干什么的呢。在现实生活中保龄球的打法是,先做扔的动作让球的速度达到最大,然后突然松手,由于惯性,保龄球就以最后松手前的速度为初速度抛出去了,直至自然停止,或者撞到边界停止,这种效果叫 fling。
flingCapturedView (int minLeft, int minTop, int maxLeft, int maxTop) 就是对捕获到的 View 做出这种 fling 的效果,用户在屏幕上滑动松手之前也会有一个滑动的速率。fling 也引出来的一个问题,就是不知道 View 最终会滚动到哪个位置,最后位置是在启动 fling 时根据最后滑动的速度来计算的(flingCapturedView 的四个参数 int minLeft, int minTop, int maxLeft, int maxTop 可以限定最终位置的范围),假如想要让 View 滚动到指定位置应该怎么办,答案就是使用 settleCapturedViewAt (int finalLeft, int finalTop)。
为什么唯一可以调用 settleCapturedViewAt () 和 flingCapturedView () 的地方是 Callback 的 onViewReleased () 呢?看看它们的源码
/**
* Settle the captured view at the given (left, top) position.
* The appropriate velocity from prior motion will be taken into account.
* If this method returns true, the caller should invoke {@link #continueSettling(boolean)}
* on each subsequent frame to continue the motion until it returns false. If this method
* returns false there is no further work to do to complete the movement.
*
* @param finalLeft Settled left edge position for the captured view
* @param finalTop Settled top edge position for the captured view
* @return true if animation should continue through {@link #continueSettling(boolean)} calls
*/
public boolean settleCapturedViewAt(int finalLeft, int finalTop) {
if (!mReleaseInProgress) {
throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " +
"Callback#onViewReleased");
}
return forceSettleCapturedViewAt(finalLeft, finalTop,
(int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
(int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId));
}
/**
* Settle the captured view based on standard free-moving fling behavior.
* The caller should invoke {@link #continueSettling(boolean)} on each subsequent frame
* to continue the motion until it returns false.
*
* @param minLeft Minimum X position for the view''s left edge
* @param minTop Minimum Y position for the view''s top edge
* @param maxLeft Maximum X position for the view''s left edge
* @param maxTop Maximum Y position for the view''s top edge
*/
public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) {
if (!mReleaseInProgress) {
throw new IllegalStateException("Cannot flingCapturedView outside of a call to " +
"Callback#onViewReleased");
}
mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(),
(int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId),
(int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId),
minLeft, maxLeft, minTop, maxTop);
setDragState(STATE_SETTLING);
}
这两个方法里一开始都会判断 mReleaseInProgress 为 false,如果为 false 就会抛一个 IllegalStateException 异常,而 mReleaseInProgress 唯一为 true 的时候就是在 dispatchViewReleased () 里调用 onViewReleased () 的时候。
Scroller 的用法请参阅 Android 中滑屏实现 ---- 手把手教你如何实现触摸滑屏以及 Scroller 类详解 ,或者自行解读 Scroller 源码,代码量不多。
ViewDragHelper 还有一个移动 View 的方法是 smoothSlideViewTo (View child, int finalLeft, int finalTop),看下它的源码:
/**
* Animate the view <code>child</code> to the given (left, top) position.
* If this method returns true, the caller should invoke {@link #continueSettling(boolean)}
* on each subsequent frame to continue the motion until it returns false. If this method
* returns false there is no further work to do to complete the movement.
*
* <p>This operation does not count as a capture event, though {@link #getCapturedView()}
* will still report the sliding view while the slide is in progress.</p>
*
* @param child Child view to capture and animate
* @param finalLeft Final left position of child
* @param finalTop Final top position of child
* @return true if animation should continue through {@link #continueSettling(boolean)} calls
*/
public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {
mCapturedView = child;
mActivePointerId = INVALID_POINTER;
boolean continueSliding = forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0);
if (!continueSliding && mDragState == STATE_IDLE && mCapturedView != null) {
// If we''re in an IDLE state to begin with and aren''t moving anywhere, we
// end up having a non-null capturedView with an IDLE dragState
mCapturedView = null;
}
return continueSliding;
}
可以看到它不受 mReleaseInProgress 的限制,所以可以在任何地方调用,效果和 settleCapturedViewAt () 类似,因为它们最终都调用了 forceSettleCapturedViewAt () 来启动自动滚动,区别在于 settleCapturedViewAt () 会以最后松手前的滑动速率为初速度将 View 滚动到最终位置,而 smoothSlideViewTo () 滚动的初速度是 0。forceSettleCapturedViewAt () 里有地方调用了 Callback 里的方法,所以再来看看这个方法:
/**
* Settle the captured view at the given (left, top) position.
*
* @param finalLeft Target left position for the captured view
* @param finalTop Target top position for the captured view
* @param xvel Horizontal velocity
* @param yvel Vertical velocity
* @return true if animation should continue through {@link #continueSettling(boolean)} calls
*/
private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
final int startLeft = mCapturedView.getLeft();
final int startTop = mCapturedView.getTop();
final int dx = finalLeft - startLeft;
final int dy = finalTop - startTop;
if (dx == 0 && dy == 0) {
// Nothing to do. Send callbacks, be done.
mScroller.abortAnimation();
setDragState(STATE_IDLE);
return false;
}
final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
mScroller.startScroll(startLeft, startTop, dx, dy, duration);
setDragState(STATE_SETTLING);
return true;
}
可以看到自动滑动是靠 Scroll 类完成,在这里生成了调用 mScroller.startScroll () 需要的参数。再来看看计算滚动时间的方法 computeSettleDuration ():
private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) {
xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity);
yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity);
final int absDx = Math.abs(dx);
final int absDy = Math.abs(dy);
final int absXVel = Math.abs(xvel);
final int absYVel = Math.abs(yvel);
final int addedVel = absXVel + absYVel;
final int addedDistance = absDx + absDy;
final float xweight = xvel != 0 ? (float) absXVel / addedVel :
(float) absDx / addedDistance;
final float yweight = yvel != 0 ? (float) absYVel / addedVel :
(float) absDy / addedDistance;
int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child));
int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child));
return (int) (xduration * xweight + yduration * yweight);
}
clampMag () 方法确保参数中给定的速率在正常范围之内。最终的滚动时间还要经过 computeAxisDuration () 算出来,通过它的参数可以看到最终的滚动时间是由 dx、xvel、mCallback.getViewHorizontalDragRange () 共同影响的。看 computeAxisDuration ():
private int computeAxisDuration(int delta, int velocity, int motionRange) {
if (delta == 0) {
return 0;
}
final int width = mParentView.getWidth();
final int halfWidth = width / 2;
final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width);
final float distance = halfWidth + halfWidth *
distanceInfluenceForSnapDuration(distanceRatio);
int duration;
velocity = Math.abs(velocity);
if (velocity > 0) {
duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
} else {
final float range = (float) Math.abs(delta) / motionRange;
duration = (int) ((range + 1) * BASE_SETTLE_DURATION);
}
return Math.min(duration, MAX_SETTLE_DURATION);
}
6~10 行没看明白,直接看 14~19 行,如果给定的速率 velocity 不为 0,就通过距离除以速率来算出时间;如果 velocity 为 0,就通过要滑动的距离(delta)除以总的移动范围(motionRange,就是 Callback 里 getViewHorizontalDragRange ()、getViewVerticalDragRange () 返回值)来算出时间。最后还会对计算出的时间做过滤,最终时间反正是不会超过 MAX_SETTLE_DURATION 的,源码里的取值是 600 毫秒,所以不用担心在 Callback 里 getViewHorizontalDragRange ()、getViewVerticalDragRange () 返回错误的数而导致自动滚动时间过长了。
在调用 settleCapturedViewAt ()、flingCapturedView () 和 smoothSlideViewTo () 时,还需要实现 mParentView 的 computeScroll ():
@Override
public void computeScroll() {
if (mDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
这属于 Scroll 类用法的范畴,不明白的请参阅 Android 中滑屏实现 ---- 手把手教你如何实现触摸滑屏以及 Scroller 类详解 的 “知识点二: computeScroll ()方法介绍”。
至此,整个触摸流程和 ViewDragHelper 的重要的方法都过了一遍。之前在讨论 shouldInterceptTouchEvent () 的 ACTION_DOWN 部分执行完后应该再执行什么的时候,还有一种情况没有展开详解,就是有子 View 消费了本次 ACTION_DOWN 事件的情况,现在来看看这种情况。
假设现在 shouldInterceptTouchEvent () 的 ACTION_DOWN 部分执行完了,也有子 View 消费了这次的 ACTION_DOWN 事件,那么接下来就会调用 mParentView 的 onInterceptTouchEvent () 的 ACTION_MOVE 部分,不明白为什么的请参阅 Andriod 从源码的角度详解 View,ViewGroup 的 Touch 事件的分发机制,接着调用 ViewDragHelper 的 shouldInterceptTouchEvent () 的 ACTION_MOVE 部分:
public boolean shouldInterceptTouchEvent(MotionEvent ev) {
// 省略
switch (action) {
// 省略其他 case...
case MotionEvent.ACTION_MOVE: {
if (mInitialMotionX == null || mInitialMotionY == null) break;
// First to cross a touch slop over a draggable view wins. Also report edge drags.
final int pointerCount = MotionEventCompat.getPointerCount(ev);
for (int i = 0; i < pointerCount; i++) {
final int pointerId = MotionEventCompat.getPointerId(ev, i);
final float x = MotionEventCompat.getX(ev, i);
final float y = MotionEventCompat.getY(ev, i);
final float dx = x - mInitialMotionX[pointerId];
final float dy = y - mInitialMotionY[pointerId];
final View toCapture = findTopChildUnder((int) x, (int) y);
final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
if (pastSlop) {
// check the callback''s
// getView[Horizontal|Vertical]DragRange methods to know
// if you can move at all along an axis, then see if it
// would clamp to the same value. If you can''t move at
// all in every dimension with a nonzero range, bail.
final int oldLeft = toCapture.getLeft();
final int targetLeft = oldLeft + (int) dx;
final int newLeft = mCallback.clampViewPositionHorizontal(toCapture,
targetLeft, (int) dx);
final int oldTop = toCapture.getTop();
final int targetTop = oldTop + (int) dy;
final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop,
(int) dy);
final int horizontalDragRange = mCallback.getViewHorizontalDragRange(
toCapture);
final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture);
if ((horizontalDragRange == 0 || horizontalDragRange > 0
&& newLeft == oldLeft) && (verticalDragRange == 0
|| verticalDragRange > 0 && newTop == oldTop)) {
break;
}
}
reportNewEdgeDrags(dx, dy, pointerId);
if (mDragState == STATE_DRAGGING) {
// Callback might have started an edge drag
break;
}
if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
saveLastMotion(ev);
break;
}
// 省略其他 case...
}
return mDragState == STATE_DRAGGING;
}
如果有多个手指触摸到屏幕上了,对每个触摸点都检查一下,看当前触摸的地方是否需要捕获某个 View。这里先用 findTopChildUnder (int x, int y) 寻找触摸点处的子 View,再用 checkTouchSlop (View child, float dx, float dy) 检查当前触摸点到 ACTION_DOWN 触摸点的距离是否达到了 mTouchSlop,达到了才会去捕获 View。
接着看 19~41 行 if (pastSlop){...} 部分,这里检查在某个方向上是否可以进行拖动,检查过程涉及到 getView [Horizontal|Vertical] DragRange 和 clampViewPosition [Horizontal|Vertical] 四个方法。如果 getView [Horizontal|Vertical] DragRange 返回都是 0,就会认作是不会产生拖动。clampViewPosition [Horizontal|Vertical] 返回的是被捕获的 View 的最终位置,如果和原来的位置相同,说明我们没有期望它移动,也就会认作是不会产生拖动的。不会产生拖动就会在 39 行直接 break,不会执行后续的代码,而后续代码里有调用 tryCaptureViewForDrag (),所以不会产生拖动也就不会去捕获 View 了,拖动也不会进行了。
如果检查到可以在某个方向上进行拖动,就会调用后面的 tryCaptureViewForDrag () 捕获子 View,如果捕获成功,mDragState 就会变成 STATE_DRAGGING,shouldInterceptTouchEvent () 返回 true,mParentView 的 onInterceptTouchEvent () 返回 true,后续的移动事件就会在 mParentView 的 onTouchEvent () 执行了,最后执行的就是 mParentView 的 processTouchEvent () 的 ACTION_MOVE 部分,拖动正常进行。
回头再看之前在 shouldInterceptTouchEvent () 的 ACTION_DOWN 部分留下的坑:
public boolean shouldInterceptTouchEvent(MotionEvent ev) {
// 省略其他部分...
switch (action) {
// 省略其他case...
case MotionEvent.ACTION_DOWN: {
// 省略其他部分...
// Catch a settling view if possible.
if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
tryCaptureViewForDrag(toCapture, pointerId);
}
// 省略其他部分...
break;
}
// 省略其他case...
}
return mDragState == STATE_DRAGGING;
}
现在应该明白这部分代码会在什么情况下执行了。当我们松手后捕获的 View 处于自动滚动的过程中时,用户再次触摸屏幕,就会执行这里的 tryCaptureViewForDrag () 尝试捕获 View,如果捕获成功,mDragState 就变为 STATE_DRAGGING 了,shouldInterceptTouchEvent () 就返回 true 了,然后就是 mParentView 的 onInterceptTouchEvent () 返回 true,接着执行 mParentView 的 onTouchEvent (),再执行 processTouchEvent () 的 ACTION_DOWN 部分。此时(ACTION_DOWN 事件发生时)mParentView 的 onTouchEvent () 要返回 true,onTouchEvent () 才能继续接受到接下来的 ACTION_MOVE、ACTION_UP 等事件,否则无法完成拖动。
至此整个事件传递流程和 ViewDragHelper 的重要方法基本都解析完了,shouldInterceptTouchEvent () 和 processTouchEvent () 的 ACTION_POINTER_DOWN、ACTION_POINTER_UP 部分就留给读者自己解析了。
总结
对于整个触摸事件传递过程,我画了简要的流程图,方便日后快速回顾。
单点触摸,没有考虑边缘滑动检测的最简流程图
单点触摸,考虑了边缘滑动检测的流程图
多点触摸情况我就没研究了,在这里忽略~
三个开启自动滚动的方法:
settleCapturedViewAt(int finalLeft, int finalTop)
以松手前的滑动速度为初速动,让捕获到的 View 自动滚动到指定位置。只能在 Callback 的 onViewReleased () 中调用。flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop)
以松手前的滑动速度为初速动,让捕获到的 View 在指定范围内 fling。只能在 Callback 的 onViewReleased () 中调用。smoothSlideViewTo(View child, int finalLeft, int finalTop)
指定某个 View 自动滚动到指定的位置,初速度为 0,可在任何地方调用。
Callback 的各个方法总结:
void onViewDragStateChanged(int state)
拖动状态改变时会调用此方法,状态 state 有 STATE_IDLE、STATE_DRAGGING、STATE_SETTLING 三种取值。
它在 setDragState () 里被调用,而 setDragState () 被调用的地方有shouldInterceptTouchEvent () 的 ACTION_DOWN 部分捕获到
shouldInterceptTouchEvent () 的 ACTION_MOVE 部分捕获到
processTouchEvent () 的 ACTION_MOVE 部分捕获到
tryCaptureViewForDrag () 成功捕获到子 View 时
调用 settleCapturedViewAt ()、smoothSlideViewTo ()、flingCapturedView () 时
拖动 View 松手时(processTouchEvent () 的 ACTION_UP、ACTION_CANCEL)
自动滚动停止时(continueSettling () 里检测到滚动结束时)
外部调用 abort () 时
void onViewPositionChanged(View changedView, int left, int top, int dx, int dy)
正在被拖动的 View 或者自动滚动的 View 的位置改变时会调用此方法。在 dragTo () 里被调用(正在被拖动时)
在 continueSettling () 里被调用(自动滚动时)
外部调用 abort () 时被调用
void onViewCaptured(View capturedChild, int activePointerId)
tryCaptureViewForDrag () 成功捕获到子 View 时会调用此方法。在 shouldInterceptTouchEvent () 的 ACTION_DOWN 里成功捕获
在 shouldInterceptTouchEvent () 的 ACTION_MOVE 里成功捕获
在 processTouchEvent () 的 ACTION_MOVE 里成功捕获
手动调用 captureChildView ()
void onViewReleased(View releasedChild, float xvel, float yvel)
拖动 View 松手时(processTouchEvent () 的 ACTION_UP)或被父 View 拦截事件时(processTouchEvent () 的 ACTION_CANCEL)会调用此方法。void onEdgeTouched(int edgeFlags, int pointerId)
ACTION_DOWN 或 ACTION_POINTER_DOWN 事件发生时如果触摸到监听的边缘会调用此方法。edgeFlags 的取值为 EDGE_LEFT、EDGE_TOP、EDGE_RIGHT、EDGE_BOTTOM 的组合。boolean onEdgeLock(int edgeFlags)
返回 true 表示锁定 edgeFlags 对应的边缘,锁定后的那些边缘就不会在 onEdgeDragStarted () 被通知了,默认返回 false 不锁定给定的边缘,edgeFlags 的取值为 EDGE_LEFT、EDGE_TOP、EDGE_RIGHT、EDGE_BOTTOM 其中之一。void onEdgeDragStarted(int edgeFlags, int pointerId)
ACTION_MOVE 事件发生时,检测到开始在某些边缘有拖动的手势,也没有锁定边缘,会调用此方法。edgeFlags 取值为 EDGE_LEFT、EDGE_TOP、EDGE_RIGHT、EDGE_BOTTOM 的组合。可在此手动调用 captureChildView () 触发从边缘拖动子 View 的效果。int getOrderedChildIndex(int index)
在寻找当前触摸点下的子 View 时会调用此方法,寻找到的 View 会提供给 tryCaptureViewForDrag () 来尝试捕获。如果需要改变子 View 的遍历查询顺序可改写此方法,例如让下层的 View 优先于上层的 View 被选中。int getViewHorizontalDragRange(View child)、int getViewVerticalDragRange(View child)
返回给定的 child 在相应的方向上可以被拖动的最远距离,默认返回 0。ACTION_DOWN 发生时,若触摸点处的 child 消费了事件,并且想要在某个方向上可以被拖动,就要在对应方法里返回大于 0 的数。
被调用的地方有三处:在 checkTouchSlop () 中被调用,返回值大于 0 才会去检查 mTouchSlop。在 ACTION_MOVE 里调用 tryCaptureViewForDrag () 之前会调用 checkTouchSlop ()。如果 checkTouchSlop () 失败,就不会去捕获 View 了。
如果 ACTION_DOWN 发生时,触摸点处有子 View 消费事件,在 shouldInterceptTouchEvent () 的 ACTION_MOVE 里会被调用。如果两个方向上的 range 都是 0(两个方法都返回 0),就不会去捕获 View 了。
在调用 smoothSlideViewTo () 时被调用,用于计算自动滚动要滚动多长时间,这个时间计算出来后,如果超过最大值,最终时间就取最大值,所以不用担心在 getView [Horizontal|Vertical] DragRange 里返回了不合适的数导致计算的时间有问题,只要返回大于 0 的数就行了。
boolean tryCaptureView(View child, int pointerId)
在 tryCaptureViewForDrag () 中被调用,返回 true 表示捕获给定的 child。tryCaptureViewForDrag () 被调用的地方有shouldInterceptTouchEvent () 的 ACTION_DOWN 里
shouldInterceptTouchEvent () 的 ACTION_MOVE 里
processTouchEvent () 的 ACTION_MOVE 里
int clampViewPositionHorizontal(View child, int left, int dx)、int clampViewPositionVertical(View child, int top, int dy)
child 在某方向上被拖动时会调用对应方法,返回值是 child 移动过后的坐标位置,clampViewPositionHorizontal () 返回 child 移动过后的 left 值,clampViewPositionVertical () 返回 child 移动过后的 top 值。
两个方法被调用的地方有两处:在 dragTo () 中被调用,dragTo () 在 processTouchEvent () 的 ACTION_MOVE 里被调用。用来获取被拖动的 View 要移动到的位置。
如果 ACTION_DOWN 发生时,触摸点处有子 View 消费事件,在 shouldInterceptTouchEvent () 的 ACTION_MOVE 里会被调用。如果两个方向上返回的还是原来的 left 和 top 值,就不会去捕获 View 了。
案例参考
在这里列举一部分对 ViewDragHelper 的应用案例,大家自己剖析它们的源码来实践巩固。
YoutubeLayout,这是最简单的 Demo
QQ5.x 侧滑菜单、ResideLayout
SwipeBackLayout、SwipeBack
SlidingUpPanel
DrawerLayout
其他关于 ViewDragHelper 的分析文章
Each Navigation Drawer Hides a ViewDragHelper,文中的源码就是上面的 YoutubeLayout
ViewDragHelper 详解,这是上面文章的简略中文版
Android ViewDragHelper 的简单使用
使用 ViewDragHelper 创建 SlidingMenu
ViewDragHelper 的功能类似于 ViewHelper,用来【滑动】View,不同点是 ViewHelper 的滑动会主动更新 parentView, 但 ViewDragHelper 需要手动更新
优点:
-
避免了直接判断滑动方向,大小等
-
降低了代码复杂度,提高了编程效率
-
简化对滑动的理解,将【滚动驱动】转变为【滑动驱动】,避免了对【滚动方向】的理解
public class DragViewGrop extends FrameLayout {
private View mMenuView;
private View mContentView;
private ViewDragHelper mViewDragHelper;
private int mMenuWidth;
public DragViewGrop(Context context) {
this(context, null);
}
public DragViewGrop(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DragViewGrop(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mViewDragHelper = ViewDragHelper.create(this,mViewDragHelperCallback);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMenuView = getChildAt(0);
mContentView = getChildAt(1);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mMenuWidth = mMenuView.getMeasuredWidth();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mViewDragHelper.processTouchEvent(event); //将事件交由onTouchEvent处理
return true;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
private ViewDragHelper.Callback mViewDragHelperCallback = new ViewDragHelper.Callback(){
public boolean tryCaptureView(View child, int pointerId){
return child==mContentView;
}
@Override
public void onViewDragStateChanged(int state) {
if(state==ViewDragHelper.STATE_IDLE)
{
Log.i("STATE","停止滑动!");
}
super.onViewDragStateChanged(state);
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return Math.min(mMenuWidth * 2 / 3,Math.max(left,0)); //横向滑动,注意范围控制
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
if(mContentView.getLeft()<(mMenuWidth/2)*2/3)
{
mViewDragHelper.smoothSlideViewTo(mContentView,0,0);
ViewCompat.postInvalidateOnAnimation(DragViewGrop.this);
}else{
mViewDragHelper.smoothSlideViewTo(mContentView,mMenuWidth*2/3,0);
ViewCompat.postInvalidateOnAnimation(DragViewGrop.this);
}
}
};
@Override
public void computeScroll() {
super.computeScroll();
//使用消息队列发送状态数据到Callback.onViewDragStateChanged,并判断是否仍然在滑动
if(mViewDragHelper.continueSettling(true))
{
//ViewCompat用来刷新当前布局,否则滑动效果无法显示,可以替换成invalidate()或者postInvalidate()
ViewCompat.postInvalidateOnAnimation(DragViewGrop.this);
}
}
}
关于Android利用ViewDragHelper轻松实现拼图游戏的示例和android 拼图游戏的问题就给大家分享到这里,感谢你花时间阅读本站内容,更多关于Android ViewDragHelper 及移动处理总结、Android ViewDragHelper 实现 QQ 侧滑边栏、Android ViewDragHelper 源码解析、Android ViewDragHelper 的简单使用等相关知识的信息别忘了在本站进行查找喔。
本文标签: