CoordinatorLayout+自定义Behavior实战学习

前言部分

CoordinatorLayout这个view出现已经很久了,今天看到知乎上的一个神奇联动的效果,类似的效果在很多应用上都有使用,以前虽然经常看到但是没有实际去查看如何实现的。这几天特意去查看资料学习其中的方式,但是由于能力有限和资料不是很全面只学到了一个皮毛。在此做一个记录,也算是这几天的努力的小总结吧。

说到CoordinatorLayout我们最先试用过的AppBarLayout和CollapsingToolbarLayout实现折叠头部。或者实现视差效果等等。。这里我都不做介绍了文章很多,实现的效果也很棒,最主是不需要我们自己子们处理逻辑,系统都帮我们实现好了。

我们虽然使用过,但是其实并没有去深入了解过实现的原理是什么,今天就通过实现一个简单的效果来学习一下自定义的Behavior。

实现的就是类似知乎等一些阅读类app等效果,如下入:

### 内容部分

事实上我们做这些工作是为了更好的实现view之间的协调联动,在没有CoordinatorLayout辅助view协调滑动之前,我们都是通过parent来对触摸事件进行分发,但这这样做就需要我们对事件的传递有很深刻的理解,因为事件的传递是从外由内的,但是事件消耗确实从内向外,parent响应事件后在传递给内部的子view,子view可以决定是否消耗事件,但是这也有一个问题,就是子view消耗事件后,其他的view都不会在收到事件的信息。所以协调多个子view联动就变得很麻烦。

下面一些详细的讲解可以去连接看看大佬的介绍,我也是读了文章开始自己写一个小 demo练手的。

大佬文章连接

上面文章中有个总结很棒,这里直接贴出来。

自定义 Behavior 的两种目的

我们可以按照两种目的来实现自己的 Behavior,当然也可以两种都实现啦

  • 某个 view 监听另一个 view 的状态变化,例如大小、位置、显示状态等
  • 某个 view 监听 CoordinatorLayout 内的 NestedScrollingChild 的接口实现类的滑动状态
实例编写

最终目标实现:

头部view、尾部view和列表绑定,当列表处于初始位置的时候,下拉列表实现头部view同时拉出,上拉列表的时候实现头部view隐藏。尾部view的规则是列表上拉时候隐藏,列表下拉的时候出现。

第一步实现如下图

1. 首先实现一下列表下拉,和头部view的联动。这里涉及到两个Behavior,一个是RecyclerView的一个是TextView的。下面看代码:
public class RecyclerTitleBehavior extends CoordinatorLayout.Behavior<RecyclerView> {

    public RecyclerTitleBehavior() {
    }

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

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, RecyclerView child, View dependency) {
        return dependency instanceof TextView;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, RecyclerView child, View dependency) {
        //计算列表y坐标,最小为0
        float y = dependency.getTranslationY();
        if (y > dependency.getHeight()) {
            y = dependency.getHeight();
        } else if (y < 0) {
            y = 0;
        }
        child.setY(y);
        return true;
    }
}

这个比较简单,主要是通过依赖头部view来改变自己的位置。如上面所说的两种方式一种:RecyclerView是负责监控头部view的别致变化。头部view发生位移变化时候,自身也作出相应的位移变化。

    1. 这一步实现头部view监听CoordinatorLayout的直接子 view的滑动,RecyclerView之所以作为子view可以被监听,因为其内部是实现了NestedScrollingChild2接口,所以当发生滑动的时候会通过NestedScrollingChildHelper调用父view的onStartNestedScroll接口(前提是父 view实现了NestedScrollingParent2接口),CoordinatorLayout作为父view可以接收到它滑动的信息,然后通过循环遍历childCount获取Behavior,通过Behavior来把滑动信息发送给所有的子view(这里父 view也可以自己选择接受滑动,这样会优先子 view滑动)。其他子view如果依赖这个view则实现其他子 view的滑动;

    2. 同时其他的子 view(不可滑动的View默认是没有实现NestedScrollingChild2接口)通过设置Behavior也实现了NestedScrollingChild2接口,通过Behavior来接收父view的滑动事件回调。如果子view发生滑动后,也会通过接口的形式回掉到NestedScrollingParent2接口中,然后父view决定自己是否配合子view的滑动,然后父view把自己滑动的情况反馈给子view,这时候子view再从新计算滑动的距离,在进行滑动。

    3. 如上所述,基本就是子view滑动,告诉一下父view我要滑动了,父view首先考虑一下自己要不要滑动,然后再告诉其它的子view有个子view滑动了,你们谁关注它了想一起动啊,然后把父 view自己滑动得结果告诉滑动的子view,让他重新计算一下距离,然后滑动剩下的距离。

    public class TextBehavior extends CoordinatorLayout.Behavior<TextView> {
    
        /**
         * 上边界
         */
        private boolean isUpLimit = false;
        /**
         * 下边界
         */
        private boolean isDownLimit = false;
    
        public TextBehavior() {
        }
    
        public TextBehavior(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull TextView child, @NonNull MotionEvent ev) {
    
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    isUpLimit = false;
                    isDownLimit = false;
                default:
                    break;
            }
            return super.onInterceptTouchEvent(parent, child, ev);
        }
    
        @Override
        public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull TextView child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
            return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
        }
    
        @Override
        public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull TextView child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
            super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
            //dy 下 为 负数 && 上 为 正数
            if (target instanceof RecyclerView) {
                RecyclerView recyclerView = (RecyclerView) target;
                LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
                int firstPosition = layoutManager.findFirstCompletelyVisibleItemPosition();
                float translationY = child.getTranslationY() - dy;
    //            LogUtil.d("translationY===" + translationY);
    //            child.setTranslationY(translationY);
    
    //            if (firstPosition == 0 && translationY == 0) {
    //                isUpLimit = true;
    //            }
    //
                if (firstPosition == 0 && canScroll(dy)) {
                    if (translationY > child.getHeight()) {
                        translationY = child.getHeight();
                        isUpLimit = false;
                        isDownLimit = true;
                    } else if (translationY < 0) {
                        translationY = 0;
                        isUpLimit = true;
                        isDownLimit = false;
    
                    }
                    child.setTranslationY(translationY);
                    consumed[1] = dy;
                }
    
            }
    
        }
    
        private boolean canScroll(int dy) {
            if (isUpLimit && dy > 0) {
                return false;
            }
            if (isDownLimit && dy < 0) {
                return false;
            }
            return true;
    
        }
    }
    

    基本上就这样,这也是醉简单的滑动效果了。

下面我们在添加一个联动的子view

我们添加一个尾部的View,它也监听另外的一个View(就是RecyclerView了),如果View滑动了一段距离,尾部的View也会有对应的动作。代码如下:

/**
 * @Author : dongfang
 * @Created Time : 2019-05-28  15:08
 * @Description:
 */
public class TextBottomBehavior extends CoordinatorLayout.Behavior<View> {

    /**
     * 上边界
     */
    private boolean isUpLimit = false;
    /**
     * 下边界
     */
    private boolean isDownLimit = false;

    public TextBottomBehavior() {
    }

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

    @Override
    public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull MotionEvent ev) {

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isUpLimit = false;
                isDownLimit = false;
            default:
                break;
        }
        return super.onInterceptTouchEvent(parent, child, ev);
    }

    @Override
    public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
        return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
        //dy 下 为 负数 && 上 为 正数
        if (target instanceof RecyclerView) {
            float translationY = child.getTranslationY() + dy;
            LogUtil.d("translationY===" + child.getTranslationY() + "finalY==" + translationY);
            if (canScroll(dy)) {
                if (translationY < -child.getHeight()) {
                    translationY = -child.getHeight();
                    isUpLimit = false;
                    isDownLimit = true;
                } else if (translationY > 0) {
                    translationY = 0;
                    isUpLimit = true;
                    isDownLimit = false;
                }
                child.setTranslationY(translationY);
//                consumed[1] = dy;
            }
        }

    }

    private boolean canScroll(int dy) {
        if (isUpLimit && dy > 0) {
            return false;
        }
        if (isDownLimit && dy < 0) {
            return false;
        }
        return true;

    }

}

这个和上个基本是一致的,只是运动的方向是反的。

还有一点要注意,我们的被监听的view只是跟随头部的view滑动,并不跟随尾部View滑动

结尾部分

鉴于能力有限,写的不是很清楚的话,希望大家指出,如果有更好的资料也请给我留言。

final 谢谢

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要实现这样的效果,需要自定义一个 Behavior 类。下面是一个简单的示例代码: ``` public class MyBehavior extends CoordinatorLayout.Behavior<View> { private int mTotalScrollRange; public MyBehavior() { super(); } public MyBehavior(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) { return dependency instanceof AppBarLayout; } @Override public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) { if (dependency instanceof AppBarLayout) { mTotalScrollRange = ((AppBarLayout) dependency).getTotalScrollRange(); float ratio = -dependency.getY() / mTotalScrollRange; child.setTranslationY(ratio * child.getHeight()); return true; } return false; } } ``` 在这个 Behavior 类中,我们首先判断依赖的 view 是否是 AppBarLayout,如果是就设置一个 mTotalScrollRange 变量,该变量保存了 AppBarLayout 的总滑动范围。在 onDependentViewChanged 方法中,我们计算出当前的滑动比例 ratio,并根据这个比例来设置子 view 的 Y 轴偏移量,以实现子 view 的折叠和展开。 接下来,在布局文件中将该 Behavior 应用到 RecyclerView 对应的子 view 上即可: ``` <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior=".MyBehavior" /> ``` 注意,这个 Behavior 只是一个简单的示例,实际应用中可能需要根据具体需求进行修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值