嵌套滑动的方法详解
嵌套滑动的方法详解
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>