android studio_Android ScrollView滚动机制及嵌套滑动机制NestedScrolling

更新时间:2020-03-20    来源:ling    手机版     字体:

【www.bbyears.com--ling】

Android ScrollView滚动机制


我们都知道通过View#scrollTo(x,y)既可以实现将View滚动的效果,如果再添加Scroller类,就可以实现滚到效果。但是,这背后是如何实现的呢?这个问题涉及到View的绘图机制。我们先看看View的绘图的基本流程


(图片来自于网上比较常见的view绘图流程图)

关于三个阶段的简单描述:

1. measure:预估计ViewTree的各个View的占用空间。
2. layout :   确定ViewTree中各个View所处的空间位置,包括width,height,left,top,right,bottom
3. draw:  使用RootViewImpl中的一个surface.lockCanvas(dirty)对象作为画布,然ViewTree上所有的View都在这个Canvas上进行画图,

值得注意的是,Canvas通过getHeight() 和 getWidth()就是整个屏幕的真实大小。包括了通知栏(虽然在打印出来的ViewTree看不到,但是通过top属性,留下了一点空间给通知栏),标题栏,Content,底部虚拟按键等。

我们先看看mScrollX/mScrollY在代码中的注释:


mScrollX/mScrollY相对这个View的内容(文字,图片,子View)垂直/水平的像素偏移。如下图:


在设置mScrollX / mScrollY后,就可以滚动到指定的“内容",而mScrollX/mScrollY 就是相对于“内容”的偏移量,内容原点为(0,0)。

而这种内容大小以及偏移是如何发生的?在ViewGroup中,存在一个API drawChild(),这个函数主要完成对子View的空间大小的限制以及偏移,见如下的描述

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    boolean more = false;
    //获取子View的空间大小
    final int cl = child.mLeft;
    final int ct = child.mTop;
    final int cr = child.mRight;
    final int cb = child.mBottom;
    //通知子View进行判断是否完成滚动,这里就是通过Scroller代码实现滚动的关键点
    child.computeScroll();
    //获取最新的偏移量
    final int sx = child.mScrollX;
    final int sy = child.mScrollY;
    //创建一个还原点
    final int restoreTo = canvas.save();
    //偏移,通过这个API,实现了scroll对内容偏移, 先把内容的原点进行偏移到负数区域
    canvas.translate(cl - sx, ct - sy);
    //剪切,因为之前有一个translate操作,所有剪切出来的空间就是父View给定的可见区域
    //所以如果子View填充Canvas的内容超出给定的空间,也不会显示出来
    canvas.clipRect(sx, sy, sx + (cr - cl), sy + (cb - ct));
    //让子View进行绘图,注意子View不用处理Scroll属性,既可以实现内容偏移
    child.draw(canvas);
    //还原
    canvas.restoreToCount(restoreTo);
    return more;
}



值得注意的是,ListView不是采用这种机制实现的,而是采用替换ChildView来实现滑动效果的。



Android 嵌套滑动机制(NestedScrolling

Android 在发布 Lollipop版本之后,为了更好的用户体验,Google为Android的滑动机制提供了NestedScrolling特性

NestedScrolling的特性可以体现在哪里呢?
比如你使用了Toolbar,下面一个ScrollView,向上滚动隐藏Toolbar,向下滚动显示Toolbar,这里在逻辑上就是一个NestedScrolling ―― 因为你在滚动整个Toolbar在内的View的过程中,又嵌套滚动了里面的ScrollView。

效果如上图


在这之前,我们知道Android对Touch事件的分发是有自己一套机制的。主要是有是三个函数:
dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。

这种分发机制有一个漏洞:

    如果子view获得处理touch事件机会的时候,父view就再也没有机会去处理这个touch事件了,直到下一次手指再按下。

也就是说,我们在滑动子View的时候,如果子View对这个滑动事件不想要处理的时候,只能抛弃这个touch事件,而不会把这些传给父view去处理。

但是Google新的NestedScrolling机制就很好的解决了这个问题。
我们看看如何实现这个NestedScrolling,首先有几个类(接口)我们需要关注一下

    NestedScrollingChild
    NestedScrollingParent
    NestedScrollingChildHelper
    NestedScrollingParentHelper


以上四个类都在support-v4包中提供,Lollipop的View默认实现了几种方法。
实现接口很简单,这边我暂时用到了NestedScrollingChild系列的方法(因为Parent是support-design提供的CoordinatorLayout)

    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        super.setNestedScrollingEnabled(enabled);
        mChildHelper.setNestedScrollingEnabled(enabled);
    }
    @Override
    public boolean isNestedScrollingEnabled() {
        return mChildHelper.isNestedScrollingEnabled();
    }
    @Override
    public boolean startNestedScroll(int axes) {
        return mChildHelper.startNestedScroll(axes);
    }
    @Override
    public void stopNestedScroll() {
        mChildHelper.stopNestedScroll();
    }
    @Override
    public boolean hasNestedScrollingParent() {
        return mChildHelper.hasNestedScrollingParent();
    }
    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }
    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }
    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }


对,简单的话你就这么实现就好了。

这些接口都是我们在需要的时候自己调用的。childHelper干了些什么事呢?,看一下startNestedScroll方法

    /**
     * Start a new nested scroll for this view.
     *
     * 

This is a delegate method. Call it from your {@link android.view.View View} subclass      * method/{@link NestedScrollingChild} interface method with the same signature to implement      * the standard policy.

     *      * @param axes Supported nested scroll axes.      *             See {@link NestedScrollingChild#startNestedScroll(int)}.      * @return true if a cooperating parent view was found and nested scrolling started successfully      */     public boolean startNestedScroll(int axes) {         if (hasNestedScrollingParent()) {             // Already in progress             return true;         }         if (isNestedScrollingEnabled()) {             ViewParent p = mView.getParent();             View child = mView;             while (p != null) {                 if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {                     mNestedScrollingParent = p;                     ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);                     return true;                 }                 if (p instanceof View) {                     child = (View) p;                 }                 p = p.getParent();             }         }         return false;     }

可以看到这里是帮你实现一些跟NestedScrollingParent交互的一些方法。
ViewParentCompat是一个和父view交互的兼容类,它会判断api version,如果在Lollipop以上,就是用view自带的方法,否则判断是否实现了NestedScrollingParent接口,去调用接口的方法。

那么具体我们怎么使用这一套机制呢?比如子View这时候我需要通知父view告诉它我有一个嵌套的touch事件需要我们共同处理。那么针对一个只包含scroll交互,它整个工作流是这样的:


一、startNestedScroll

首先子view需要开启整个流程(内部主要是找到合适的能接受nestedScroll的parent),通知父View,我要和你配合处理TouchEvent


二、dispatchNestedPreScroll

在子View的onInterceptTouchEvent或者onTouch中(一般在MontionEvent.ACTION_MOVE事件里),调用该方法通知父View滑动的距离。该方法的第三第四个参数返回父view消费掉的scroll长度和子View的窗体偏移量。如果这个scroll没有被消费完,则子view进行处理剩下的一些距离,由于窗体进行了移动,如果你记录了手指最后的位置,需要根据第四个参数offsetInWindow计算偏移量,才能保证下一次的touch事件的计算是正确的。
如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
这个函数一般在子view处理scroll前调用。


三、dispatchNestedScroll

向父view汇报滚动情况,包括子view消费的部分和子view没有消费的部分。
如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
这个函数一般在子view处理scroll后调用。


四、stopNestedScroll

结束整个流程。

整个对应流程是这样


子view父viewstartNestedScrollonStartNestedScroll、onNestedScrollAccepteddispatchNestedPreScrollonNestedPreScrolldispatchNestedScrollonNestedScrollstopNestedScrollonStopNestedScroll


一般是子view发起调用,父view接受回调。

我们最需要关注的是dispatchNestedPreScroll中的consumed参数。

    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) ;

它是一个int型的数组,长度为2,第一个元素是父view消费的x方向的滚动距离;第二个元素是父view消费的y方向的滚动距离,如果这两个值不为0,则子view需要对滚动的量进行一些修正。正因为有了这个参数,使得我们处理滚动事件的时候,思路更加清晰,不会像以前一样被一堆的滚动参数搞混。

对NestedScroll的介绍暂时到这里,下一次将讲一下CoordinatorLayout的使用(其中让人较难理解的Behavior对象),以及在SegmentFault Android客户端中的实践。谢谢支持。


本文来源:http://www.bbyears.com/shujuku/88274.html

猜你感兴趣

热门标签

更多>>

本类排行