Android的滑动分析

来源:互联网 发布:百度人工智能平台接入 编辑:程序博客网 时间:2024/06/03 19:20

Android的滑动分析

滑动应该可以说是Android中最常见的一种视觉效果,也是View编写中的关键,本篇文章就来分析一下Android中滑动的实现方式。

滑动通常分为计算位移以及执行滑动两部分,本篇将会涉及这两个方面。对于计算部分,通常有三种方式,1:阻塞;2:通过各种Scroller;3:属性动画。其中1和2其实是差不多的,因为要省事的话,都会需要Scroller来执行计算任务,只是两者的触发时机不一样,这一点会在后面分析。对于滑动的执行,就只有两种方式:改变子view本身的位置和改变父View的mScrollY/mScrollX。

1. 滑动的实现方式

1.1 改变父view的mScrollX/mScrollY

看过源码的同学大概都会知道,这两个是View类中的属性,它们是来干嘛的呢?就是来改变View的视图位置。只要看一下View类中的一个方法就知道这两个属性是干嘛的:

    /**     * Return the visible drawing bounds of your view. Fills in the output     * rectangle with the values from getScrollX(), getScrollY(),     * getWidth(), and getHeight(). These bounds do not account for any     * transformation properties currently set on the view, such as     * {@link #setScaleX(float)} or {@link #setRotation(float)}.     *     * @param outRect The (scrolled) drawing bounds of the view.     */    public void getDrawingRect(Rect outRect) {        outRect.left = mScrollX;        outRect.top = mScrollY;        outRect.right = mScrollX + (mRight - mLeft);        outRect.bottom = mScrollY + (mBottom - mTop);    }

这个方法是返回当前View的绘制边界,它并不是简简单单地返回View的上下左右的坐标,而是中间加了这两个属性。以mScrollX为例,显然如果它大于0,那么实际显示的区域就比原来偏右,如果小于0,那么就偏左。
下面先让我通俗地解释一个概念:Android中View的区域实际是无限大的(不是很准确,但意思就是说比显示的区域要大很多),相当于一个画布。而我们能看到的区域相当于一个窗口,我们只能通过这个窗口来看到对应窗口位置的View的内容。那么mScrollX和mScrollY就代表着窗口在这个画布上相对于原点在x轴和y轴的差(这里假设View初始化时窗口所在的位置为原点),可正可负。

因此,我们在改变这两个值时,窗口就会移动,在我们看来,View的内容就在移动。当增大mScrollY时,窗口就向下移动,看起来就是View的内容在向上移动,这样我们就实现了滑动。

但是通过这种方式实现的滑动有个问题,那就是会对事件分发有影响。因为在我们改变窗口位置时,事件的坐标并没有变化。举个例子,在View初始化后,mScrollX和mScrollY都为0,此时窗口位于原点。我们点一下,发送了一个坐标为(500,500)的事件(这里的坐标取getX()getY()的值),这并没有什么问题。然后滑动一下,使得mScrollX = 100,mScrollY = 100,然后我们再在原来相同的位置点一下,再次发送一个事件,坐标仍然为(500,500),我们在自己的ViewGroup中拿到这个事件后,却不能直接使用了。因为窗口的位置已经变了,窗口坐标为(500,500)的位置,对应的应该是ViewGroup布局中的(600,600)的位置。而事件的坐标对应的都是窗口中的位置(为什么?因为刚说了,View实际的区域是无限大的,而平时说的大小、位置,说的其实是窗口的信息,毕竟它是真正绘制的区域。上层View在向下传递事件时,会根据子view的位置来改变事件中的坐标,使其坐标值成为相对于子view窗口参照系的坐标值,关于事件分发的详细知识,大家可以查找一下资料。由于上层view知道的只是子view的窗口的大小及位置信息,因此转换来的坐标自然是事件在窗口中的坐标值),既然如此,如果我们想把它映射到View的实际布局区域中,就要考虑mScrollXmScrollY的影响了。

那么什么时候我们该考虑这方面的影响呢?这是关键的部分。如果你不使用事件也不介入事件的分发流程,那么基本就不需要考虑。View原本的dispatchTouchEvent()方法是有考虑到这两个属性的,主要是通过dispatchTransformedTouchEvent()方法进行的。它会根据自己的mScrollXmScrollY以及子view的位置对事件坐标进行转换,以保证事件在子view中的对应坐标是正确的。不过完全不使用事件的情况很少见,因此大多数时候你还是得考虑这两个属性。

下面举两个例子,是我最近遇到的坑,相信你看过了就知道什么时候该考虑了:
1、判断ACTION_DOWN发生在什么地方并决定是否拦截
情景是我自定义了可以滑动的layout,在onInterceptTouchEvent()中判断,如果ACTION_DOWN发生在headView里,就拦截这个事件,否则不拦截。这里我就是通过改变mScrollY进行滑动的。当我滑动到headView已经完全隐藏后,点击上部分程序仍然判断我点击在headView内。后来我将事件坐标偏移了mScrollY后正常。然后我干脆就在onTouchEvent中也将事件偏移了mScrollY。

这里需要注意的是,ViewGroup的dispatchTouchEvent只有在将事件分发给子view时才会对坐标进行转换,其余都不会转换。这意味着你在自己的onInterceptTouchEvent()以及onTouchEvent()还有其他监听器中收到的事件坐标都是没有使用自己的mScrollX/mScrollY进行变换的。上层View传给我们的事件坐标是多少,我们在这些地方收到的事件坐标就是什么样。

为什么不在dispatchTouchEvent()中直接偏移,然后再调用super.dispatchTouchEvent()不是更方便吗?因为ViewGroup的dispatchTouchEvent有自己完善的逻辑,它只会在给子View分发事件时对坐标进行转换,并且如果一个子view分发不成功后,会将坐标再恢复,在分发给下一个子view时进行转换。如果你直接在ViewGroup的dispatchTouchEvent逻辑之前就修改了事件坐标,那么就会对后面向子view分发事件转换坐标时产生影响。

2、根据ACTION_MOVE事件来对子view进行滑动
这是很常见的逻辑,在onTouchEvent()中通过判断两次事件的坐标差来滑动,之前通过改变子view的位置进行滑动时并没有什么问题,但这次出现了问题,在滑动时不跟手。

这是因为我在onTouchEvent()中使用mScrollY对事件坐标进行了转换,这样一来每次的事件坐标就变成了MotionEvent.getY() + mScrollY,而mScrollY也是时时在变化的,这就会导致滑动不平滑。于是,我又把onTouchEvent()里的坐标转换给去掉才正常。

OK,以上就是我使用这种方法进行滑动时遇到的坑,看起来这种方式并不适合用来自定义一个可滑动的layout。但不可否认,它也有它的方便,比如不需要对那么多子view进行位移操作,只需要改变一个值就可以滑动。另外对TextView以及ImageView等用来展示内容的控件来说,它也是唯一的可以滑动的手段,除非你愿意自己去定义一个。

需要注意的是,并不是说你使用这种方式就不用注意mScrollXmScrollY的影响,毕竟scrollTo()scrollBy()方法可都是公开的。

1.2 改变子view的位置参数

这种方式就清晰明了简单多了,事实上,Android系统中的ListView、GridView以及RecyclerView等都是使用这种方式来实现滑动的,这也说明了如果我们想实现一个可以滑动的容器,就应该使用这种方式。

使用这种方式需要注意的就是onLayout函数。如果你的onLayout中的布局逻辑总是初始化布局,那么应该在change == true时再进入到布局逻辑,否则你会发现onLayout函数经常会被调用以至于你的界面总是回到初始状态。或者干脆你写个智能点的,能够依照当前布局情况进行布局,这一点在写RecyclerView的LayoutManager时会有体会。

额。。其实这种方式真的很简单,那我也就到这为止了吧。接下来看一下滑动的计算。

2. 滑动的计算

滑动能够执行的前提是你进行了正确的计算,能够计算出本次刷新需要滑动的距离。Android上的滑动分为拖拽和Fling(怎么翻译?原意作为不及物动词有急行、冲的意思。还是及物动词意思比较好理解一些:抛。意思就是给个很大的速度,然后手指离开,View的滑动继续,然后由于阻力慢慢停下来。)。拖拽基本是不需要计算的,前后两次触摸事件的坐标差就是你要滑动的距离。大多数需要计算的都是fling。

2.1 Scroller

Scroller是负责计算滑动距离的类,它有两个方法分别对应滑动和fling。先看一下滑动:

    /**     * Start scrolling by providing a starting point, the distance to travel,     * and the duration of the scroll.     *      * @param startX Starting horizontal scroll offset in pixels. Positive     *        numbers will scroll the content to the left.     * @param startY Starting vertical scroll offset in pixels. Positive numbers     *        will scroll the content up.     * @param dx Horizontal distance to travel. Positive numbers will scroll the     *        content to the left.     * @param dy Vertical distance to travel. Positive numbers will scroll the     *        content up.     * @param duration Duration of the scroll in milliseconds.     */    public void startScroll(int startX, int startY, int dx, int dy, int duration)

参数什么的一目了然,传入起始位置和总的滑动间距,以及滑动完成的时长,就可以开始滑动了。

下面是fling:

    /**     * Start scrolling based on a fling gesture. The distance travelled will     * depend on the initial velocity of the fling.     *      * @param startX Starting point of the scroll (X)     * @param startY Starting point of the scroll (Y)     * @param velocityX Initial velocity of the fling (X) measured in pixels per     *        second.     * @param velocityY Initial velocity of the fling (Y) measured in pixels per     *        second     * @param minX Minimum X value. The scroller will not scroll past this     *        point.     * @param maxX Maximum X value. The scroller will not scroll past this     *        point.     * @param minY Minimum Y value. The scroller will not scroll past this     *        point.     * @param maxY Maximum Y value. The scroller will not scroll past this     *        point.     */    public void fling(int startX, int startY, int velocityX, int velocityY,            int minX, int maxX, int minY, int maxY)

参数什么的都不用细说了,fling需要传入滑动方向的初速度velocityX/velocityY。这个速度我们通常都会用VelocityTracker来获取。至于那几个min和max的参数,由于fling应该是自然的,不能因为滑动距离不同就使得fling的表现不一致,因此我们一般都会对min参数传入Integer.MIN_VALUE,而对于max参数传入Integer.MAX_VALUE。反正最后肯定会有边界检测。

要注意,使用这两个方法后不代表立刻就会开始滑动,还需要在这之后调用invalidate()方法。这是因为Scroller并不会主动触发View刷新。而View计算滑动的原理是:View类有个computeScroll()方法,这个方法用来计算当前的滑动值并执行滑动。这个方法是在View的draw()方法中被调用的,也就是每次刷新都会计算一次。因此我们需要在开始的时候手动触发一次刷新。

computeScroll()方法中,我们就可以获取Scroller根据时间计算的当前的滑动值。调用Scroller的computeScrollOffset()计算当前滑动的位置,并返回一个bool值,如果为true代表滑动还没有完成,false代表设定的滑动已经完成了。接着调用getCurrY()/getCurrX()获取当前的滑动位置,然后计算再进行滑动即可。注意如果滑动还未完成,要在computeScroll()最后调用invalidate()触发下一次刷新。就这样不断计算不断触发刷新,滑动就完成了。

2.2 阻塞

这种方法的本质实际上也是使用Scroller来计算滑动所需数据,与上面那种方式不同的地方在于触发计算和刷新的时机不同。上一种是在View的draw()方法中调用computeScroll()来触发计算的。而这种是向主线程post一个Runnable,这个Runnable会维护一个Scroller,其中的逻辑和computeScroll()是差不多的,只是在最后在滑动没有结束时再次向主线程post一下自己。AbsListView中就是用这种方法来实现fling的。
说起来可能比较复杂,看一下伪代码就很明白了。

    private class FlingRunnable extends Runnable {        private Scroller mScroller = new Scroller(getContext());        public void startScroll(int startX, int startY, int dx, int dy, int duration)        {            mScroller.startScroll(startX, startY, dx, dy, duration);            post(this);        }        public void startFling(int startX, int startY, int velocityX, int velocityY)        {            mScroller.fling(startX, startY, velocityX, velocityY, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);            post(this);        }        @Override        public void run() {            if(mScroller.computeScrollOffset())            {                //do your scroll logic                post(this);            }        }    };

2.3 动画

动画也是用来执行这种动作的好工具,这里肯定就是用属性动画ValueAnimator。但动画也有它的不足,比如写起来比较麻烦,需要一个Animator和一个Listener。而且动画只能初始化一次,对于操作如此频繁的滑动来说,你可能要不断地销毁创建Animator。开销比较大,因此也不推荐。

3. 滑动中的注意事项

3.1 滑动的判断

在前面的事件分发那一章我说过,你以为的单纯的点击其实并不一定是只有ACTION_DOWN和ACTION_UP两个动作。由于手机灵敏度比较高,绝大多数的动作中都有ACTION_MOVE,哪怕你只是点击。因此我们需要根据两次ACTION_MOVE之间的差的大小来判断是否是滑动,以免误将事件拦截。如果这个差大于某个阈值,那就认为是滑动了,否则就认为是点击中的干扰。而这个阈值不能太大也不能太小,太小效果不明显,太大则在慢速滑动中会有卡顿感。建议同时记录ACTION_DOWN的坐标,如果手指滑动的总距离超过这个阈值,那么不管每次移动的距离有多小,我们都认为它是滑动的。

3.2 多指事件

这里指滑动过程中多个手指落下抬起,如果不妥善处理的话,滑动就会发生跳动。这里的建议就是记录下滑动所依赖的那个point id,以后滑动数据都从这个point取值。在检测到ACTION_POINTER_DOWN或ACTION_POINTER_UP事件时,及时更改id和状态。详细就不展开讲了,大家可以搜一下MotionEvent相关的知识一看就明白。

总结

OK,以上就是关于Android的滑动的分析,虽然好像东西比较多,但也没什么复杂的,有疏漏的地方希望大家指正。

本人最近在找一份Android开发的工作,地点在深圳南山附近。如果有伯乐的话请给我留言。