王學崗高級UI(15)嵌套滑動詳解和自定義LinerLayout,Behavior實現嵌套滑動

嵌套滑動的方法詳解

嵌套滑動的方法詳解
public interface NestedScrollingParent2 extends NestedScrollingParent {
    /**
     * 這個是嵌套滑動控制事件分發的控制方法,只有返回true才能接收到事件分發
     * @param child 包含target的ViewParent的直接子View
     * @param target 發起滑動事件的View
     * @param axes 滑動的方向,數值和水平方向{@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
     *                         {@link ViewCompat#SCROLL_AXIS_VERTICAL} 
     * @return true 表示父View接受嵌套滑動監聽,否則不接受
     */
    boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes,@NestedScrollType int type);

    /**
     * 這個方法在onStartNestedScroll返回true之後在正式滑動之前回調
     * @param child 包含target的父View的直接子View
     * @param target 發起嵌套滑動的View
     * @param axes 滑動的方向,數值和水平方向{@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
     *                         {@link ViewCompat#SCROLL_AXIS_VERTICAL} or both
     */
    void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes,@NestedScrollType int type);

    /**
     *
     * @param target View that initiated the nested scroll
     */
    void onStopNestedScroll(@NonNull View target);

    /**
     * 在子View滑動過程中會分發這個嵌套滑動的方法,要想這裏收到嵌套滑動事件必須在onStartNestedScroll返回true
     * @param dxConsumed 子View在水平方向已經消耗的距離
     * @param dyConsumed 子View在垂直方法已經消耗的距離
     * @param dxUnconsumed 子View在水平方向剩下的未消耗的距離
     * @param dyUnconsumed 子View在垂直方法剩下的未消耗的距離
     * @param type 發起嵌套事件的類型 分爲觸摸(ViewParent.TYPE_TOUCH)和非觸摸(ViewParent.TYPE_NON_TOUCH)
     */
    void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type);

    /**
     * 在子View開始滑動之前讓父View有機會先進行滑動處理
     * @param dx 水平方向將要滑動的距離
     * @param dy 豎直方向將要滑動的距離
     * @param consumed Output. 父View在水平和垂直方向要消費的距離,consumed[0]表示水平方向的消耗,consumed[1]表示垂直方向的消耗,
     */
    void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
            @NestedScrollType int type);

}




public interface NestedScrollingChild2 extends NestedScrollingChild {

    //返回值true表示找到了嵌套交互的ViewParent,type表示引起滑動事件的類型,這個事件和parent中的onStartNestedScroll是對應的
    boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type);

    
    //停止嵌套滑動的回調
    void stopNestedScroll(@NestedScrollType int type);

    //表示有實現了NestedScrollingParent2接口的父類
    boolean hasNestedScrollingParent(@NestedScrollType int type);

    //分發嵌套滑動事件的過程
    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
            @NestedScrollType int type);

    //在嵌套滑動之前分發事件
    boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow, @NestedScrollType int type);
}


Android嵌套滑動講解簡書:https://www.jianshu.com/p/f4763bf8f9ba

方法總結
startNestedScroll : 起始方法, 主要作用是找到接收滑動距離信息的外控件.

dispatchNestedPreScroll : 在內控件處理滑動前把滑動信息分發給外控件.

dispatchNestedScroll : 在內控件處理完滑動後把剩下的滑動距離信息分發給外控件.

stopNestedScroll : 結束方法, 主要作用就是清空嵌套滑動的相關狀態

setNestedScrollingEnabled和isNestedScrollingEnabled : 一對get&set方法, 用來判斷控件是否支持嵌套滑動.

dispatchNestedPreFling和dispatchNestedFling : 跟Scroll的對應方法作用類似
NestedScrollingParent

on
StartNestedScroll : 對應startNestedScroll, 內控件通過調用外控件的這個方法來確定外控件是否接收滑動信息.

onNestedScrollAccepted : 當外控件確定接收滑動信息後該方法被回調, 可以讓外控件針對嵌套滑動做一些前期工作.

onNestedPreScroll : 關鍵方法, 接收內控件處理滑動前的滑動距離信息, 在這裏外控件可以優先響應滑動操作, 消耗部分或者全部滑動距離.

onNestedScroll : 關鍵方法, 接收內控件處理完滑動後的滑動距離信息, 在這裏外控件可以選擇是否處理剩餘的滑動距離.

onStopNestedScroll : 對應stopNestedScroll, 用來做一些收尾工作.

onNestedPreFling和onNestedFling : 同上略


嵌套滑動事件傳遞過程
在這裏插入圖片描述
Appbarlayout不需要寫behavior,是因爲appbarlayout已經集成了behavior,只需定義Appbarlayout子控件的layout_srollFlags的屬性就可以。這是上兩節篇文章寫到的東西,今天我們把最外層的CoordinatorLayout換成我們自定義的佈局,比如自定義的LinearLayout,讓他實現NestedScrollingParent2接口。
看下我們比較重要的兩個接口

NestedScrollingParent2
給嵌套滑動的父View使用的,它的方法是用來接收嵌套滑動子View的通知。用法還是容易理解核心邏輯就是在子View消費之前與之後,向父View發出通知,看看父View是否對這此有何處理,然後再將結果返還給子View。
NestedScrollingChild2
給嵌套滑動的子View使用的,它的方法是用來給嵌套滑動父View的發送通知。用法也很容易理解,核心邏輯就是在子View消費之前,向父View發出通知,看看父View是否對這此有何處理,然後再將結果返還給子View。

當手指觸摸的時候,嵌套子控件會調用NestedScrollingChild2的startNestedScroll()通知父控價。父控件(CoordinatorLayout)就會循環所有的子控件,看哪個子控件有behavior。
appbarlayout 是一個觀察者,NestedAcrollView是被觀察者。觀察者與被觀察者都必須是CoordinatorLayout的直接子控件。當被觀察者在滾動的時候,把通知發給父親,父親拿到通知後就會遍歷所有的子View,看看哪個子控件裏有behavior。appBarLayout裏已經封裝了Behavior。然後拿出behavior,然後調用behavior裏的方法。
三個控件CoordinatorLayout,appbarlayout ,NestedAcrollView。在解釋一遍,顯示NestedAcrollView滑動,通知CoordinatorLayout,CoordinatorLayout遍歷所有的子控件,發現子控件appbarlayout 裏有behavior。appbarlayout根據behavior做出相應的改變。

下面我們自定義我們的LinearLayout。具有CoordinatorLayout相同的嵌套滑動功能

package com.dongnao.dn_vip_ui_16_2.view;

import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
import android.support.v4.view.NestedScrollingParent2;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;

import com.dongnao.dn_vip_ui_16_2.Behavior;
import com.dongnao.dn_vip_ui_16_2.R;

import java.lang.reflect.Constructor;

public class MyNestedLinearLayout extends LinearLayout implements NestedScrollingParent2 {


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

    public MyNestedLinearLayout(Context context,  AttributeSet attrs) {
        super(context, attrs);
    }

    public MyNestedLinearLayout(Context context,AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public MyNestedLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }


    /**
     * 接收開始嵌套滑動的通知方法
     * @param view
     * @param view1
     * @param i
     * @param i1
     * @return
     */
    @Override
    public boolean onStartNestedScroll(@NonNull View view, @NonNull View view1, int i, int i1) {
        //返回true表示需要監聽嵌套滑動,
        // 如果返回false,當我們滑動NestedScrollView的時候,該類其它的方法不會執行
        return true;
    }
     //手指觸摸滑動的時候調用該方法。慣性滑動不調用
    @Override
    public void onNestedScrollAccepted(@NonNull View view, @NonNull View view1, int i, int i1) {
    }

    @Override
    public void onStopNestedScroll(@NonNull View view, int i) {
        //遍歷它所有的子控件  然後去看子控件 有沒有設置Behavior  這個Behavior就是去操作子控件作出動作
        //得到當前控件中所有的子控件的數量
        int childCount = this.getChildCount();
        //遍歷所有的子控件
        for(int x=0;x<childCount;x++){
            //得到子控件
            View childAt = this.getChildAt(x);
            //獲取到子控件的屬性對象
            MyLayoutParams layoutParams = (MyLayoutParams) childAt.getLayoutParams();
            Behavior behavior = layoutParams.behavior;
            if(behavior!=null){
                if(behavior.layoutDependsOn(this,childAt,view)){
                    behavior.onStopNestedScroll(view,i);
                }
            }
        }
    }

    /**
     * 在子View滑動過程中會通知這個嵌套滑動的方法,要想這裏收到嵌套滑動事件必須在onStartNestedScroll返回true
     * @param view 當前滑動的控件,比如NestedScrollView
     * @param i  滑動的控件在水平方向已經消耗的距離
     * @param i1 滑動的控件在垂直方法已經消耗的距離  每次滑動的距離    如果是向上滑動就是正數  如果向下滑動 就是負數
     * @param i2  滑動的控件在水平方向剩下的未消耗的距離
     * @param i3  滑動的控件在垂直方法剩下的未消耗的距離,滑動控件距離父控件頂層的距離
     * @param i4 發起嵌套事件的類型 分爲觸摸(ViewParent.TYPE_TOUCH)和非觸摸(ViewParent.TYPE_NON_TOUCH)
     */
    //滑動的時候調用該方法,不管是手指觸摸滑動,還是慣性滑動
    @Override
    public void onNestedScroll(@NonNull View view, int i, int i1, int i2, int i3, int i4) {
        //遍歷它所有的子控件  然後去看子控件 有沒有設置Behavior  這個Behavior作用就是去操作子控件作出動作
        //得到當前控件中所有的子控件的數量
        int childCount = this.getChildCount();
        //遍歷所有的子控件
        for(int x=0;x<childCount;x++){
            //得到子控件
            View childAt = this.getChildAt(x);
            //獲取到子控件的屬性對象
            MyLayoutParams layoutParams = (MyLayoutParams) childAt.getLayoutParams();
            Behavior behavior = layoutParams.behavior;
            if(behavior!=null){
                //執行behavior動作之前,當前操作的對象是不是被觀察者
                if(behavior.layoutDependsOn(this,childAt,view)){
                    behavior.onNestedScroll(this,childAt,view,i,i1,i2,i3);
                }
            }
        }
    }


    @Override
    public void onNestedPreScroll(@NonNull View view, int i, int i1, @NonNull int[] ints, int i2) {
    }


    /**
     * 這個方法的作用其實就是定義當前你這個控件下所有的子控件使用的LayoutParams類
     * 把我們自己定義的behavior放進來就要重寫這個類。重寫了這裏才能強轉
     *  MyLayoutParams layoutParams = (MyLayoutParams) childAt.getLayoutParams();
     * @param attrs
     * @return
     */
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MyLayoutParams(getContext(),attrs);
    }



    class MyLayoutParams extends LayoutParams{
        //這個Behavior是自己定義的,不是系統的,這點要注意
        //我們要把自己定義的屬性注入到MyLayoutParams裏
        private Behavior behavior;

        public MyLayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            //將自定義的屬性交給一個TypedArray來管理,將自定義的屬性注入到attrs裏
            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.MyNestedLinearLayout);
            //通過TypedArray獲取到我們定義的屬性的值
            //這裏是Behavoir的包名+類名( my:layout_behavior="com.dongnao.dn_vip_ui_16_2.Behavior")
            String className = a.getString(R.styleable.MyNestedLinearLayout_layout_behavior);
            //根據類名 將Behavoir實例化
            behavior = parseBehavior(c, attrs, className);
            //清空  不清空佔內存
            a.recycle();
        }

        /**
         * 將Behavoir實例化
         * @param c
         * @param attrs
         * @param className
         */
        private Behavior parseBehavior(Context c, AttributeSet attrs, String className) {
            Behavior behavior = null;
            //判斷子控件有沒有設置這個屬性
            if(TextUtils.isEmpty(className)){
                return null;
            }
            try {
                Class aClass = Class.forName(className);
                if(!Behavior.class.isAssignableFrom(aClass)){
                    return null;
                }
                //去獲取到它的構造方法
                Constructor<? extends Behavior> constructor = aClass.getConstructor(Context.class);
                constructor.setAccessible(true);
                behavior = constructor.newInstance(c);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return behavior;
        }


        public MyLayoutParams(int width, int height) {
            super(width, height);
        }

        public MyLayoutParams(int width, int height, float weight) {
            super(width, height, weight);
        }

        public MyLayoutParams(ViewGroup.LayoutParams p) {
            super(p);
        }

        public MyLayoutParams(MarginLayoutParams source) {
            super(source);
        }

        @RequiresApi(api = Build.VERSION_CODES.KITKAT)
        public MyLayoutParams(LayoutParams source) {
            super(source);
        }
    }
}

package com.dongnao.dn_vip_ui_16_2;

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.v4.widget.NestedScrollView;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

/**
 * 自己定義的類似系統的behavior
 * 一個行爲類  就是用來監聽被觀察者作出動作的時候  觀察者也作出相應的動作  只是一個動作  只是我們想要做的操作
 */
public class Behavior {
    public Behavior(Context context){

    }


    /**
     * 這個方法是用來篩選 被觀察者的
     * @param parent  觀察者的父類
     * @param child 觀察者
     * @param dependency 被觀察者
     * @return
     */
    public boolean layoutDependsOn(@NonNull View parent, @NonNull View child, @NonNull View dependency) {
        return dependency instanceof NestedScrollView && dependency.getId() == R.id.scollView;
    }

    //與MyNestedLinearLayout中的onNestedScroll()方法對應
    public void onNestedScroll(@NonNull View parent, @NonNull View child, @NonNull View target,
                               int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        //向下滑動了 滑動距離是負數 就是向下
        if(dyConsumed<0){
            //當前觀察者控件的Y座標小於等於0   並且 被觀察者的Y座標不能超過觀察者控件的高度
            if(child.getY()<=0 && target.getY()<=child.getHeight()){
                child.setTranslationY(-(target.getScrollY()>child.getHeight()?
                        child.getHeight():target.getScrollY()));
                target.setTranslationY(-(target.getScrollY()>child.getHeight()?
                        child.getHeight():target.getScrollY()));
                ViewGroup.LayoutParams layoutParams = target.getLayoutParams();
                layoutParams.height= (int) (parent.getHeight()-child.getHeight()-child.getTranslationY());
                target.setLayoutParams(layoutParams);
            }
        }else{
            //向上滑動了 被觀察者的Y座標不能小於或者等於0
            if(target.getY()>0){
                //設置觀察者的Y座標的偏移  1.不能超過觀察者自己的高度
                child.setTranslationY(-(target.getScrollY()>child.getHeight()?
                        child.getHeight():target.getScrollY()));
                target.setTranslationY(-(target.getScrollY()>child.getHeight()?
                        child.getHeight():target.getScrollY()));
                //獲取到被觀察者的LayoutParams
                ViewGroup.LayoutParams layoutParams = target.getLayoutParams();
                //當我們向上滑動的時候  被觀察者的高度 就等於 它父親的高度 減去觀察者的高度 再減去觀察者Y軸的偏移值
                layoutParams.height= (int) (parent.getHeight()-child.getHeight()-child.getTranslationY());
                target.setLayoutParams(layoutParams);
            }
        }
    }


    public void onStopNestedScroll(@NonNull View view, int i) {
    }
}

佈局

<?xml version="1.0" encoding="utf-8"?>
<com.dongnao.dn_vip_ui_16_2.view.MyNestedLinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:my="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:orientation="vertical"
    android:layout_height="match_parent">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="觀察者"
        android:gravity="center"
        android:background="@color/colorAccent"
        android:textColor="@android:color/white"
        my:layout_behavior="com.dongnao.dn_vip_ui_16_2.Behavior"
        />
    <androidx.core.widget.NestedScrollView
        android:id="@+id/scollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorPrimary"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
            <TextView
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:text="111"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:text="222"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:text="333"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:text="444"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:text="555"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:text="666"/>
            <TextView
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:text="777"/>
        </LinearLayout>
    </androidx.core.widget.NestedScrollView>
</com.dongnao.dn_vip_ui_16_2.view.MyNestedLinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyNestedLinearLayout">
        <attr name="layout_behavior" format="string"></attr>
    </declare-styleable>
</resources>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章