Android NestedScrolling机制

作者: Jack921 | 来源:发表于2018-09-25 20:54 被阅读220次

    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

    相关文章

      网友评论

      • wanban:非常见知识点 赞一个

      本文标题:Android NestedScrolling机制

      本文链接:https://www.haomeiwen.com/subject/dxlhoftx.html