常见滑动方式
掌握滑动的方法是实现自定义 View 的基础,常见的实现滑动的方式有三种
- ScrollTo/SrollBy
- 动画
- 改变View的LayoutParams使得View重新布局从而实现滑动
使用ScrollTo/ScrollBy
1 | public void scrollTo(int x,int y) |
x: 滑动到的距离,这个距离指的是相对于初始位置(并非上一次的位置,而是View内容最初的位置,最初是0)
y: 同上
View内部会有两个变量来记录当前内容的位置 : mScrollX 和 mScrollY ,向左或者向上为正,反之为负
1 | public void scrollBy(int x,int y) |
x: 相对于 oldX 的偏移量,向左为正,反之为负
y:同上
实际上 scrollBy 也是通过 scrollTo 来实现的,将 偏移量x与oldX相加即可得到绝对值
Scroller
为什么把 scroller 拿出来一起说呢,因为其实吧,scroller 也是通过 scrollTo 来实现的,不过它具有弹性的效果。其实学过微积分的都很容易理解,无非就是将一个滑动过程分解为时间间隔较小的scrollTo。由此,我们也能很容易地联想到,使用最基本地属性动画(单纯地将 duration 分解,再在update里面调用scrollTo即可)也可以实现这样的效果。
如何使用Scroller?
1 | public class Scroller{ |
2 | ... |
3 | private int mStartX; |
4 | private int mStartY; |
5 | private int mFInalX; |
6 | private int mFinalY; |
7 | ... |
8 | |
9 | public void startScroll(int startX, int startY, int dx, int dy) { |
10 | startScroll(startX, startY, dx, dy, DEFAULT_DURATION); |
11 | } |
12 | |
13 | public void startScroll(int startX, int startY, int dx, int dy, int duration) { |
14 | mMode = SCROLL_MODE; |
15 | mFinished = false; |
16 | mDuration = duration; |
17 | mStartTime = AnimationUtils.currentAnimationTimeMillis(); |
18 | mStartX = startX; |
19 | mStartY = startY; |
20 | mFinalX = startX + dx; |
21 | mFinalY = startY + dy; |
22 | mDeltaX = dx; |
23 | mDeltaY = dy; |
24 | mDurationReciprocal = 1.0f / (float) mDuration; |
25 | } |
26 | |
27 | // 计算下一次滑动到的 x 和 y |
28 | public boolean computeScrollOffset() { |
29 | |
30 | } |
31 | } |
要记住一点,Scroller 是不会产生滑动的逻辑的,它的作用是记录这个滑动的起始状态,以及提供一个计算当前滑动分段的函数 computeScrollOffset 函数。
由此,我们可以知道 ,要想实现这个弹性滑动,需要一个触发滑动的动作,以及不断产生下一次滑动的动作。这就引出了一个很重要的函数:View 的 computeScroll() 方法,在 View中这是一个空实现,在每次 draw 的时候,draw 方法都会调用 computeScroll 方法(我们可以在这里触发下一次绘制,而下一次绘制又会调用这个方法,如此循环往复直到滑动结束)。上面的机制满足了我们实现这个滑动过程的两个因素。
1 | public MView extends View{ |
2 | |
3 | Scroller mScroller = new Scroller(); |
4 | |
5 | //我们自定义的 smoothScrollTo 方法 |
6 | public void smoothScrollTo(int destX,int destY){ |
7 | int scrollX = getScrollX(); |
8 | int deltaX = destX - scrollX; |
9 | // startScroll 方法负责记录滑动的起始状态信息,以及duration,以便计算每一个细分滑动的状态 |
10 | mScroller.startScroll(scrollX,0,deltaX,0,1000); |
11 | // 触发第一次滑动 |
12 | invalidate(); |
13 | } |
14 | |
15 | // 重写 computeScroll 方法,计算下一次滑动的位置信息,并调用 invalidate触发下一次滑动 |
16 | |
17 | public void computeScroll(){ |
18 | // 判断滑动是否结束,没有的话,计算出下一次的位置信息 |
19 | if(mScroller.computeScrollOffSet()){ |
20 | scrollTo(mScroller.getCurX(),mScroller.getCurY()); |
21 | // 触发下一次重绘 |
22 | postInvalidate(); |
23 | } |
24 | } |
25 | |
26 | } |
除了上面提到的 Scroller 和 属性动画之外,我们也可以通过 postDelayed 或者 sleep 等延时策略,其基本思想都是一致的,即弹性滑动分解。
动画:
1 | ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(1000).start(); |
tranbslationX: 指的是View的偏移量,而不是View内容的偏移量
View还有 x,y 参数,其中 x 表示 View 左上角的横坐标 (不一定扽古语left,left表示初始坐标), x = left + tranlastionX;要注意区别 x 和 left ,以及 tranlastionX 与 scrollX
另外,有一个特殊的 属性动画,我们可以用它来做一些连续性的动作:
1 | ValueAnimator animator = ValueAnimator,ofInt(0,1).setDuration(1000); |
2 | animator.addUpdateListener(new AnimatorUpdateListener(){ |
3 | |
4 | public void onAnimationUpdate(ValueAnimator animator){ |
5 | float fraction = animator.getAnimatedFraction(); |
6 | mButton1.scrollTo(startX+(int)(deltaX*fraction),0); |
7 | } |
8 | }); |
9 | animator.start(); |
ViewDragHelper
2013 Google I/O 大会上介绍了两个新的 Layout : SlidingPanelLayout 和 DrawerLayout ,现在这两个类都被广泛地运用,其实研究他们的源码你会发现这两个类都运用了 ViewDragHelper 来处理拖动。ViewDragHelper是 framework中不为人知却非常有用的一个工具
ViewDragHelper 解决了Android 手势处理过于复杂的问题。在DrawLayout出来之前,侧滑菜单都是由第三方开源代码来实现的,其中最著名的是 MenuDrawer ,MenuDrawer 重写了 onTouchEvent 方法来实现侧滑效果,代码量很大,实现逻辑也需要很大耐心才能看懂。如果每个开发人员都从这么原始的步奏开始做起,那对于安卓生态是相当不利的。所以说ViewDragHelper等的出现反映了安卓开发框架已经开始向成熟的方向迈进
ViewDrapHelper基本用法
ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number of useful operations and state tracking for allowing a user to drag and reposition views within their parent ViewGroup.
– Android Developer
官方文档之指出,ViewDragHelper 是一个工具类,通常用于自定义 ViewGroup 之中,为拖拽和重新布局其子 View 提供帮助。
其实ViewDragHelper 并不是第一个用于分析手势处理的类, GestureDetector 也是,但是在拖动相关的手势分析方面,GestureDetector 只能说是勉为其难。
ViewdragHelper 其本质也是分析 onInterceptTouchEvent 和 onTouchEvent 的 MotionEvent 参数,然后根据分析的结果去改变一个容器中被拖动子 View 的位置(通过 offsetTopAndBottom 和 offsetLeftANdRight 方法)。
初始化
ViewDragHelper 一般用在一个自定义 ViewGroup 的内部,内部有一个 View mDragView 作为成员变量
1 | public class DragLayout extends LinearLayout{ |
2 | private final ViewDragHelper mDragHelper; |
3 | private View mDragView; |
4 | |
5 | public DragLayout(Context context){this(context,null);} |
6 | |
7 | public DragLayout(Context context,AttributeSet attrs){ |
8 | super(context,attr); |
9 | //1.0f是敏感度参数,越大越敏感 |
10 | mDragHelper = ViewDragHelper.create(this,1.0f,new DragHelperCallback()); |
11 | } |
12 | } |
要想让 ViewDragHelper 能够处理拖动需要将其触摸事件传递给它,这点和 GestureDetector一样。
1 |
|
2 | public boolean onInterceptTouchEvent(MotionEvent ev){ |
3 | final int action = MotionEventCompat.getActionMasked(ev); |
4 | |
5 | if(action==MotionEvent.ACTION_CANCEL || action==MotionEvent.ACTION_UP){ |
6 | mDragHelper.cancel(); |
7 | return false; |
8 | } |
9 | |
10 | // 这里有点疑问,通常 ,DragHelper 不拦截,不代表 ViewGroup 就不拦截啊。 |
11 | return mDragHelper.shoudlInterceptTouchEvent(ev); |
12 | } |
13 | |
14 |
|
15 | public boolean onTouchEvent(MotionEvent ev){ |
16 | mDragHelper.processTouchEvent(ev); |
17 | |
18 | // 我觉得,也可以对 ev 进一步处理吧,毕竟ViewGroup也不仅仅是处理拖拽嘛 |
19 | return true; |
20 | } |
接下来,你就可以在回调中处理各种拖动行为了
拖动处理
1.处理横向的拖动
第二个参数 left 指当前拖动子 View 应该到达的 x 坐标。所以按照常理,这个方法原封返回 left 就可以了。不过这里我们多加一些处理,子View遇到边界之后不再拖动。
1 | public int clampViewPositionHorizontal(View child,int left,int dx){ |
2 | final int leftBound = getPaddingLeft(); |
3 | final int rightBound = getWidth() - mDragView.getWidth(); |
4 | final int newLeft = Math.min(Math.max(left,leftBound),rightBound); |
5 | |
6 | return newLeft; |
7 | } |
2.处理纵向的滑动
同上,具体代码我就不贴了
注意
这两个方法都要重写,比如你只重写了横向滑动相关的,这并不代表它纵向上就不滑动了,我一开始也是这么以为的,结果子View一滑动就跳到parent的顶部,纠结了很久。后来发现,这两个方法都有默认的返回值:0,所以才会出现上面的情况。
如果你不想纵向滑动,那就计算下原始的top值,并返回这个top值就可以了,它就不会纵向滑动了。
3.tryCaptureView函数关联子View [determin which view can drag]
tryeCaptureView也是定义在callback中的函数,它会决定当前的这个drag可否生效
1 | mDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() { |
2 | ..... |
3 | |
4 | // 判断当前View是否可以拖拽 |
5 | |
6 | public boolean tryCaptureView(@NonNull View child, int pointerId) { |
7 | return true; |
8 | } |
9 | }); |
child表示当前 motionevent作用的子View,我们用来判断是否它可以被拖拽。但实际上我们并不知道这个child实际上是谁,所以通常的做法是设计一个 API,类似
1 | public void setDragable(View child); |
提供给用户,我们来试一下:
1 | // 判断当前View是否可以拖拽 |
2 |
|
3 | public boolean tryCaptureView(@NonNull View child, int pointerId) { |
4 | //return true; |
5 | return child.getTag()!=null; |
6 | } |
7 | |
8 | //-----api------ |
9 | public void setDragable(View child){ |
10 | child.setTag(new Object()); |
11 | } |
然后在MainActivity里面通过 dragLayout.setDragable(view)就可以设置可滑动了。
滑动边缘
滑动边缘也同样分为滑动左边缘或者右边缘(EDGE_LEFT 和 EDGE_RIGHT)。可能你会有所疑问,到底是滑动 ViewGroup 的边缘还是滑动子 View 的边缘啊,注意哈,本文中的滑动拖拽都是站在 ViewGroup 的角度来说的。
例如打开左边缘滑动开关:
1 | mDragHelper.setEgdeTrackingEnabled(ViewDragHelper.EDGE_LEFT); |
Callback中监听边缘滑动有两个相关的方法
onEdgeTouched:
1 | // Called when one of the subscribed edges in the parent view has been touched by the user while no child view is currently captured. |
2 | // edgeFlags: 表示当前哪个edge触摸了,edgeFlags也可能表示有几个edge 被触摸了(复合值) |
3 | // pointerId: |
4 | public void onEdgeTouched(int edgeFlags,int pointerId); |
关于 edgeFlags, 在 ViewDragHelper 中定义了下面:
1 | public static final int EDGE_LEFT = 1 << 0; |
2 | public static final int EDGE_RIGHT = 1 << 1; |
3 | public static final int EDGE_TOP = 1 << 2; |
4 | public static final int EDGE_BOTTOM = 1 << 3; |
onEdgeDragStarted:
当边缘拖动开始时调用
1 |
|
2 | public void onEdgeDragStarted(int edgeFlags,int pointerId){ |
3 | // do sth |
4 | } |
Useful APIs
这个方法决定是否捕获当前正在 drag 区域的 childView,这个是一个内部调用
1 | public boolean tryCaptureView(@NonNull child: View, pointerId: Int) |
这个方法则可以手动指定要移动的 childView,要注意其与 tryCaptureView 的区别,这个通常可以手动调用
1 | public void captureChildView(@NonNull View childView, int activePointerId) |
在拖拽结束的时候回调,利用这个方法,比如我们可以实现拖拽结束后回到原位置等骚操作(一般用弹性滑动来处理)
1 | public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) |