Android NestedScrolling机制

NestedScrolling机制现在在App的作用越来越重要,许多很漂亮的交互都是基于NestedScrolling机制进行完成的。

NestedScrolling机制主要是能够让父View和子View在滚动时互相协调配合。其中有两个重要的类,分别是:

接口类
NestedScrollingParent(最新:NestedScrollingParent2)
NestedScrollingChild(最新:NestedScrollingChild2)

帮助类
NestedScrollingChildHelper
NestedScrollingParentHelper

父类继承NestedScrollingParent接口,而子类继承NestedScrollingChild接口,同时让父类包含子类,而不是自接父子关系,就搭起了NestedScrollingParent机制的基本骨架。

其主要流程是:

  1. 子类滑动,把滑动产生的事件和参数传给父类
  2. 父类根据子类传过来的参数进行各种交互操作,如变大缩小之类的

而NestedScrollingChildHelper和NestedScrollingParentHelper是两个帮助类,在实现NestedScrollingChild和NestedScrollingParent接口时,使用这两个帮助类可以简化我们的工作。

NestedScrollingChild 接口类

public interface NestedScrollingChild {

    /**
     * 设置嵌套滑动是否能用
     */
    @Override
    public void setNestedScrollingEnabled(boolean enabled);

    /**
     * 判断嵌套滑动是否可用
     */
    @Override
    public boolean isNestedScrollingEnabled();

    /**
     * 开始嵌套滑动
     *
     * @param axes 表示方向轴,有横向和竖向
     */
    @Override
    public boolean startNestedScroll(int axes);

    /**
     * 停止嵌套滑动
     */
    @Override
    public void stopNestedScroll();

    /**
     * 判断是否有父View 支持嵌套滑动
     */
    @Override
    public boolean hasNestedScrollingParent() ;

    /**
     * 滑行时调用
     * @param velocityX x 轴上的滑动速率
     * @param velocityY y 轴上的滑动速率
     * @param consumed 是否被消费
     * @return  true if the nested scrolling parent consumed or otherwise reacted to the fling
     */
    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) ;

    /**
     * 进行滑行前调用
     * @param velocityX x 轴上的滑动速率
     * @param velocityY y 轴上的滑动速率
     * @return true if a nested scrolling parent consumed the fling
     */
    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) ;

    /**
     * 子view处理scroll后调用
     * @param dxConsumed x轴上被消费的距离(横向)
     * @param dyConsumed y轴上被消费的距离(竖向)
     * @param dxUnconsumed x轴上未被消费的距离
     * @param dyUnconsumed y轴上未被消费的距离
     * @param offsetInWindow 子View的窗体偏移量
     * @return  true if the event was dispatched, false if it could not be dispatched.
     */
    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) ;

    /**
     * 在子View的onInterceptTouchEvent或者onTouch中,调用该方法通知父View滑动的距离
     * @param dx  x轴上滑动的距离
     * @param dy  y轴上滑动的距离
     * @param consumed 父view消费掉的scroll长度
     * @param offsetInWindow   子View的窗体偏移量
     * @return 支持的嵌套的父View 是否处理了 滑动事件
     */
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow);

}

上面这个方法方法和代表的意思我都已经贴出来, 然后是只是一个接口类上面的方法要怎么实现呢,这时候就要用到上面的帮助类NestedScrollingChildHelper,一个完整的实现模板如下:

public class MyNestedScrollingChild extends LinearLayout implements NestedScrollingChild {
    private NestedScrollingChildHelper mNestedScrollingChildHelper;

    public MyNestedScrollingChild(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
        mNestedScrollingChildHelper.setNestedScrollingEnabled(true);
    }

    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return mNestedScrollingChildHelper.isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return mNestedScrollingChildHelper.startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {
        mNestedScrollingChildHelper.stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return mNestedScrollingChildHelper.hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mNestedScrollingChildHelper.dispatchNestedFling(velocityX,velocityY,consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX,velocityY);
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow) {
        return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed,dyConsumed,dxUnconsumed,dyUnconsumed,offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) {
        return dispatchNestedPreScroll(dx,dy,consumed,offsetInWindow);
    }

}

NestedScrollingParent 接口

public interface NestedScrollingParent {

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

    @Override
    public void onStopNestedScroll(View child);

    @Override
    public void onNestedScrollAccepted(View child, View target, int axes);

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY);

    @Override
    public int getNestedScrollAxes();
}

从上面的代码可以看出NestedScrollingChild的方法执行之后就会回调父View的各个方法,从方法名也知道作用和NestedScrollingChild的用作大同小异。当子View执行startNestedScroll时,就会回调父View的onStartNestedScroll、onNestedScrollAccepted方法,当子View执行dispatchNestedPreScroll方法时,就会回调父View的onNestedPreScroll,当子View执行dispatchNestedScroll方法时,就会回调父View的onNestedScroll方法,由此类推,dispatchNestedPreFling回调父View的onNestedPreFling方法,dispatchNestedFling回调父View的onNestedFling方法,等。

同时也有几个接口是需要帮助类进行实现的,模板代码如下:

public class MyNestedScrollingParent extends LinearLayout implements NestedScrollingParent {
    private NestedScrollingParentHelper mNestedScrollingParentHelper;

    public MyNestedScrollingParent(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
    }

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return super.onStartNestedScroll(child, target, nestedScrollAxes);
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(target, dx, dy, consumed);
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        return super.onNestedFling(target, velocityX, velocityY, consumed);
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return super.onNestedPreFling(target, velocityX, velocityY);
    }

    @Override
    public void onStopNestedScroll(View child) {
        mNestedScrollingParentHelper.onStopNestedScroll(child);
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int axes) {
        mNestedScrollingParentHelper.onNestedScrollAccepted(child,target,axes);
    }

    @Override
    public int getNestedScrollAxes() {
        return mNestedScrollingParentHelper.getNestedScrollAxes();
    }

}

最后总结,子View通过startNestedScroll()发起嵌套滑动,同时父View也会回调自己的onStartNestedScroll()方法,接着子View每次在滚动前都会调用dispatchNestedPreScroll()方法,父View的onNestedPreScroll()也会操作,父View决定是否熬滑动,然后才是子View自己滑动,之后子View也可以调用上面的其它方法做相应的处理,最后调用stopNestedScroll()结束。

最后举一个实例吧

public class MyNestedScrollChild extends LinearLayout implements NestedScrollingChild2 {
    private NestedScrollingChildHelper mNestedScrollingChildHelper;
    private int[] offset=new int[2];
    private int[] consumed=new int[2];
    private TextView scrollText;
    private int showHeight;
    private int lastY;
    private  boolean srcollTop=false;

    public MyNestedScrollChild(Context context) {
        super(context);
    }

    public MyNestedScrollChild(Context context, AttributeSet attrs) {
        super(context, attrs);
        setBackgroundColor(context.getResources().getColor(R.color.colorffffff));
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        scrollText=(TextView)getChildAt(0);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        showHeight = getMeasuredHeight();
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    public boolean canChildScrollUp() {
        return srcollTop;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch(event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastY=(int)event.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                int y=(int)(event.getRawY());
                int dy=y-lastY;
                lastY=y;
                if(startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)
                        &&dispatchNestedPreScroll(0,dy,consumed,offset)){
                    int remain = dy - consumed[1];
                    if (remain != 0) {
                        scrollBy(0, -remain);
                    }
                }else{
                    scrollBy(0, -dy);
                }
                break;
        }
        return true;
    }

    //限制滚动范围
    @Override
    public void scrollTo(int x, int y) {
        int maxY = getMeasuredHeight()- showHeight;
        if (y > maxY) {
            y = maxY;
            srcollTop=false;
        }else if (y < 0) {
            y = 0;
            srcollTop=true;
        }else{
            srcollTop=false;
        }
        super.scrollTo(x, y);
    }

    public NestedScrollingChildHelper getNestedScrollingChildHelper(){
        if(mNestedScrollingChildHelper==null){
            mNestedScrollingChildHelper=new NestedScrollingChildHelper(this);
            mNestedScrollingChildHelper.setNestedScrollingEnabled(true);
        }
        return mNestedScrollingChildHelper;
    }

    @Override
    public boolean startNestedScroll(int axes, int type) {
        return getNestedScrollingChildHelper().startNestedScroll(axes,type);
    }

    @Override
    public void stopNestedScroll(int type) {
        getNestedScrollingChildHelper().stopNestedScroll(type);
    }

    @Override
    public boolean hasNestedScrollingParent(int type) {
        return getNestedScrollingChildHelper().hasNestedScrollingParent(type);
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
                                        int dyUnconsumed, @Nullable int[] offsetInWindow, int type) {
        return getNestedScrollingChildHelper().dispatchNestedScroll(dxConsumed,dyConsumed,
                dxUnconsumed,dyUnconsumed,offsetInWindow,type);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow, int type) {
        return getNestedScrollingChildHelper().dispatchNestedPreScroll(dx,dy,consumed,offsetInWindow,type);
    }

    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        getNestedScrollingChildHelper().setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return getNestedScrollingChildHelper().isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return getNestedScrollingChildHelper().startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {
        getNestedScrollingChildHelper().stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return getNestedScrollingChildHelper().hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
                                        @Nullable int[] offsetInWindow) {
        return getNestedScrollingChildHelper().dispatchNestedScroll(dxConsumed,dyConsumed,dxUnconsumed,dyUnconsumed,offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed, @Nullable int[] offsetInWindow) {
        return getNestedScrollingChildHelper().dispatchNestedPreScroll(dx,dy,consumed,offsetInWindow);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return getNestedScrollingChildHelper().dispatchNestedFling(velocityX,velocityY,consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return getNestedScrollingChildHelper().dispatchNestedPreFling(velocityX,velocityY);
    }

}

首先给出NestedScrollingChild子View,重点看一下onTouchEvent()方法,当MotionEvent.ACTION_MOVE时,不断的调用startNestedScroll()和dispatchNestedPreScroll()向父View发送直接,然后滚动通过scrollBy()滚动触发事件的View,这就是最核心的代码了,接着看父View代码如下:

public class MyNestedScrollParent extends LinearLayout implements NestedScrollingParent2 {
    private NestedScrollingParentHelper mNestedScrollingParentHelper;
    private MyNestedScrollChild scrollChildView;
    private ImageView foodIV;
    private TextView titleTV;
    private int imageHeight;
    private int titleHeight;
    private int imageMargin;
    private int scrollY;

    public MyNestedScrollParent(Context context) {
        this(context,null);
    }

    public MyNestedScrollParent(Context context, AttributeSet attrs) {
        super(context, attrs);
        mNestedScrollingParentHelper=new NestedScrollingParentHelper(this);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        FrameLayout frameLayout=(FrameLayout) getChildAt(0);
        scrollChildView=(MyNestedScrollChild) getChildAt(1);
        foodIV=frameLayout.findViewById(R.id.foodIV);
        titleTV=frameLayout.findViewById(R.id.titleTV);
        foodIV.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                imageHeight=foodIV.getHeight();
            }
        });
        titleTV.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                titleHeight=titleTV.getHeight();
            }
        });
    }

    @Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
        if(target instanceof MyNestedScrollChild) {
            return true;
        }
        return false;
    }

    @Override
    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
        mNestedScrollingParentHelper.onNestedScrollAccepted(child,target,axes,type);
    }

    @Override
    public void onStopNestedScroll(@NonNull View target, int type) {
        mNestedScrollingParentHelper.onStopNestedScroll(target,type);
    }

    @Override
    public int getNestedScrollAxes() {
        return mNestedScrollingParentHelper.getNestedScrollAxes();
    }

    @Override
    public void onStopNestedScroll(@NonNull View target) {
        mNestedScrollingParentHelper.onStopNestedScroll(target);
    }

    @Override
    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
                               int dxUnconsumed, int dyUnconsumed, int type) {

    }

    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        imageMargin=titleHeight-imageHeight;
        scrollY+=dy;
        if(scrollY<=imageMargin){
            scrollY=imageMargin;
            scrollChildView.setTranslationY(scrollY);
        }else{
            if(dy<0){
                //上滑
                consumed[1]=dy;
                scrollChildView.setTranslationY(scrollY);
            }else{
                //下滑
                if(!scrollChildView.canChildScrollUp()){
                    scrollY-=dy;
                }
                if(scrollY>=0){
                    scrollY=0;
                }
                scrollChildView.setTranslationY(scrollY);
            }
        }
    }

    @Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes) {
        return false;
    }

}

前面的子View的dispatchNestedPreScroll()对应这个父View的onNestedScroll()如上面的代码,通过View.setTranslationY()来滑动整个子View,consumed[1]=dy;表示子View和滑动的View一起滑。最后看一下布局文件:

<?xml version="1.0" encoding="utf-8"?>
<com.jack.meituangoodsdetails.view.MyNestedScrollParent
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="170dp">

        <ImageView
            android:id="@+id/foodIV"
            android:layout_width="match_parent"
            android:layout_height="170dp"
            android:src="@mipmap/food_bg"
            android:scaleType="fitXY"/>

        <TextView
            android:id="@+id/titleTV"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:gravity="center"
            android:textColor="@android:color/white"
            android:background="@color/color2e8b57"
            android:text="MyTitle"/>

    </FrameLayout>

    <com.jack.meituangoodsdetails.view.MyNestedScrollChild
        android:id="@+id/scroll_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <TextView
            android:id="@+id/scrollText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:text="123\n456\n789\n111\n222\n333\n444\n555\n666\n777\n888\n999\n14\n12\n13\n44\n55\n66\n77\n88\n99\n11\n22\n33\n44\n55\n66\n77\n88\n99\n77\n88\n88\n8\n88\n88\n" />
    </com.jack.meituangoodsdetails.view.MyNestedScrollChild>

</com.jack.meituangoodsdetails.view.MyNestedScrollParent>


父View包裹子View,以达成依赖关系。

讲了NestedScrolling,就有必要讲解CoordinatorLayout.Behavior,下回讲吧,最后奉上源码吧https://github.com/jack921/MeiTuanGoodsDetails

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容