欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android 自定义LinearLayout和Behavior实现嵌套滑动

程序员文章站 2024-03-21 12:14:28
...

嵌套滑动事件传递过程如下图所示:
Android 自定义LinearLayout和Behavior实现嵌套滑动

1 实现NestedScrollingParent2接口

自定义LinearLayout,实现NestedScrollingParent2接口,先让onStartNestedScroll方法返回false

public class MyNestedLinearLayout extends LinearLayout implements NestedScrollingParent2 {

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

    @Override
    public boolean onStartNestedScroll(@NonNull View view, @NonNull View view1, int i, int i1) {
        Log.i("hongx", "11111111111111--->调用了onStartNestedScroll方法");
        return false;
    }
    @Override
    public void onNestedScrollAccepted(@NonNull View view, @NonNull View view1, int i, int i1) {
        Log.i("hongx", "22222222222222--->调用了onNestedScrollAccepted方法");
    }
    @Override
    public void onStopNestedScroll(@NonNull View view, int i) {
        Log.i("hongx", "33333333333333--->调用了onStopNestedScroll方法");
    }
    @Override
    public void onNestedScroll(@NonNull View view, int i, int i1, int i2, int i3, int i4) {
        Log.i("hongx", "44444444444444--->调用了onNestedScroll方法");
    }
    @Override
    public void onNestedPreScroll(@NonNull View view, int i, int i1, @NonNull int[] ints, int i2) {
        Log.i("hongx", "555555555555555--->调用了onNestedPreScroll方法");
    }
}

布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<com.hongx.behavior.MyNestedLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:my="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="@color/colorAccent"
        android:gravity="center"
        android:text="Hello World!"
        android:textColor="@android:color/white" />
    <androidx.core.widget.NestedScrollView
        android:id="@+id/scollView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/colorPrimary">
        <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.hongx.behavior.MyNestedLinearLayout>

滑动NestedScrollView,查看打印结果:

Android 自定义LinearLayout和Behavior实现嵌套滑动
结果只调用了onStartNestedScroll方法,并没有调用其他方法。

将onStartNestedScroll方法返回值设置为true后查看打印结果,如下:

Android 自定义LinearLayout和Behavior实现嵌套滑动

嵌套滑动抽象化:
Android 自定义LinearLayout和Behavior实现嵌套滑动

2 自定义MyBehavior

先设置behavior的自定义属性:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyNestedLinearLayout">
        <attr name="layout_behavior" format="string"></attr>
    </declare-styleable>
</resources>

自定义一个MyBehavior,这个MyBehavior的作用是改变TextView的显示。

public class MyBehavior {

    public MyBehavior(Context context) {

    }

    /**
     * 观察者
     */
    public void setV(View child) {
        TextView textView = (TextView) child;
        textView.setText("变化了");
    }

}

MyNestedLinearLayout如下:

public class MyNestedLinearLayout extends LinearLayout implements NestedScrollingParent2 {

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

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

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


    /**
     * 这个是嵌套滑动控制事件分发的控制方法,只有返回true才能接收到事件分发
     *
     * @param view  包含target的ViewParent的直接子View  嵌套滑動的子控件
     * @param view1 嵌套滑動的子控件
     * @param i     滑动的方向,数值和水平方向  這裡說的不是手勢  而是當前控件的需求  或者環境
     * @param i1    发起嵌套事件的类型 分为触摸(ViewParent.TYPE_TOUCH)和非触摸(ViewParent.TYPE_NON_TOUCH)
     * @return
     */
    @Override
    public boolean onStartNestedScroll(@NonNull View view, @NonNull View view1, int i, int i1) {
        Log.i("hongx", "11111111111111--->调用了onStartNestedScroll方法");
        return true;
    }

    @Override
    public void onNestedScrollAccepted(@NonNull View view, @NonNull View view1, int i, int i1) {
        Log.i("hongx", "22222222222222--->调用了onNestedScrollAccepted方法");
    }

    @Override
    public void onStopNestedScroll(@NonNull View view, int i) {
        Log.i("hongx", "33333333333333--->调用了onStopNestedScroll方法");
    }

    /**
     * 在子View滑动过程中会通知这个嵌套滑动的方法,要想这里收到嵌套滑动事件必须在onStartNestedScroll返回true
     *
     * @param view 當前滑動的控件
     * @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();
        Log.i("hongx", "childCount = " + childCount);
        //遍历所有的子控件
        for (int x = 0; x < childCount; x++) {
            //得到子控件
            View childAt = this.getChildAt(x);
            //获取到子控件的属性对象
            MyLayoutParams layoutParams = (MyLayoutParams) childAt.getLayoutParams();
            MyBehavior behavior = layoutParams.behavior;
            if (behavior != null) {
                Log.i("hongx", "behavior != null");
                behavior.setV(childAt);
            }
        }
    }

    @Override
    public void onNestedPreScroll(@NonNull View view, int i, int i1, @NonNull int[] ints, int i2) {
        Log.i("hongx", "555555555555555--->调用了onNestedPreScroll方法");
    }

    /**
     * 这个方法的作用其实就是定义当前你这个控件下所有的子控件使用的LayoutParams类
     *
     * @param attrs
     * @return
     */
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MyLayoutParams(getContext(), attrs);
    }

    class MyLayoutParams extends LayoutParams {
        private MyBehavior behavior;

        public MyLayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            //将自定义的属性交给一个TypedArray来管理
            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.MyNestedLinearLayout);
            //通过TypedArray获取到我们定义的属性的值 Behavoir类名
            String className = a.getString(R.styleable.MyNestedLinearLayout_layout_behavior);
            //根据类名 将Behavoir实例化
            behavior = parseBehavior(c, attrs, className);
            //清空  不清空占内存
            a.recycle();
        }

        /**
         * 将Behavoir实例化
         */
        private MyBehavior parseBehavior(Context c, AttributeSet attrs, String className) {
            MyBehavior behavior = null;
            if (TextUtils.isEmpty(className)) {
                return null;
            }
            try {
                Class aClass = Class.forName(className);
                if (!MyBehavior.class.isAssignableFrom(aClass)) {
                    return null;
                }
                //去获取到它的构造方法
                Constructor<? extends MyBehavior> 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);
        }
    }
}

布局文件做如下修改:

<?xml version="1.0" encoding="utf-8"?>
<com.hongx.behavior.MyNestedLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:my="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="@color/colorAccent"
        android:gravity="center"
        android:text="观察者"
        android:textColor="@android:color/white"
        my:layout_behavior="com.hongx.behavior.MyBehavior" />
...


Android 自定义LinearLayout和Behavior实现嵌套滑动

从效果可以看出,滚动NestedScrollView的时候,TextView的内容改变了。


3 新增TextViewBehavior

再定义个TextViewBehavior继承MyBehavior:

public class TextViewBehavior extends MyBehavior {
    public TextViewBehavior(Context context) {
        super(context);
    }

    @Override
    public void setV(View child) {
        TextView textView = (TextView) child;
        textView.setVisibility(View.GONE);
    }

}

添加页一个TextView使用TextViewBehavior:

    <TextView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="@color/colorAccent"
        android:gravity="center"
        android:text="观察者1"
        android:textColor="@android:color/white"
        my:layout_behavior="com.hongx.behavior.TextViewBehavior" />

Android 自定义LinearLayout和Behavior实现嵌套滑动


4 添加Behavior限制条件

public class MyBehavior {

    public MyBehavior(Context context) {

    }

    public boolean layoutDependsOn(@NonNull View parent, @NonNull View child, @NonNull View dependency) {
        return dependency instanceof NestedScrollView && dependency.getId() == R.id.scollView;
    }

    /**
     * 观察者
     */
    public void setV(View child) {
        TextView textView = (TextView) child;
        textView.setText("变化了");
    }

}

在MyNestedLinearLayout中设置这个限制条件,只有当滑动的控件(被观察者)是ScrollView,才去执行Behavior中的方法,让TextView(观察者)改变状态
Android 自定义LinearLayout和Behavior实现嵌套滑动

public class TextViewBehavior extends MyBehavior {

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

    @Override
    public boolean layoutDependsOn(@NonNull View parent, @NonNull View child, @NonNull View dependency) {
        return dependency instanceof RecyclerView;//如果滑动的是RecyclerView才会执行Behavior中的setV方法
    }

    @Override
    public void setV(View child) {
        TextView textView = (TextView) child;
        textView.setVisibility(View.GONE);
    }

}

Android 自定义LinearLayout和Behavior实现嵌套滑动

可以看到,第一个TextView变化了,而第二个TextView并没有任何变化。


5 最终实现

MyNestedLinearLayout中的onNestedScroll方法接收到的各个参数值传递给自定义的Behavior。

    /**
     * 在子View滑动过程中会通知这个嵌套滑动的方法,要想这里收到嵌套滑动事件必须在onStartNestedScroll返回true
     *
     * @param target 當前滑動的控件
     * @param dxConsumed    滑動的控件在水平方向已经消耗的距离
     * @param dyConsumed    滑動的控件在垂直方法已经消耗的距离
     * @param dxUnconsumed   滑動的控件在水平方向剩下的未消耗的距离
     * @param dyUnconsumed   滑動的控件在垂直方法剩下的未消耗的距离
     * @param type   发起嵌套事件的类型 分为触摸(ViewParent.TYPE_TOUCH)和非触摸(ViewParent.TYPE_NON_TOUCH)
     */
    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,int dxUnconsumed, int dyUnconsumed, int type) {
        int childCount = this.getChildCount();
        //遍历直接子控件
        for (int x = 0; x < childCount; x++) {
            View childAt = this.getChildAt(x);
            //当前属性对象是没有自定义属性的!!!!!
            MyLayoutParams lp = (MyLayoutParams) childAt.getLayoutParams();
            //获取到控件的myBehavior对象
            MyBehavior myBehavior = lp.behavior;
            //如果子控件设置了myBehavior
            if (myBehavior != null) {
                //判断当前的滑动的控件是不是当前子控件的被观察者
                if (myBehavior.layoutDependsOn(this, childAt, target)) {
                    myBehavior.onNestedScroll(this, childAt, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
                }
            }
        }
    }

在MyBehavior的onNestedScroll方法中实现想要的效果。

    /**
     * 嵌套滑动中的方法
     *
     * @param parent
     * @param child
     * @param target
     * @param dxConsumed
     * @param dyConsumed
     * @param dxUnconsumed
     * @param dyUnconsumed
     */
    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);
            }
        }

    }

最后效果如下所示:
Android 自定义LinearLayout和Behavior实现嵌套滑动

Github:https://github.com/345166018/AndroidUI/tree/master/HxBehavior