高级UI

自定义Behaviour

2019-11-01  本文已影响0人  Magic旭

伴随着业务的迭代,总会有奇奇怪怪的历史原因,导致需要一些奇奇怪怪的逻辑要实现。

问题描述

  1. 由于历史接口数据结构原因,原本不同层级的数组希望放在一起,作为RecyclerView的一部分进行滚动。当然你可以用正常方法实现两个类型的Holder,但是我这边因为动态卡片架构不支持,所以只能用到Behaviour去实现了。下面介绍简单demo讲解实现的过程。

前景知识

clipChildren与clipToPadding
clipChildren和clipToPadding
  1. clipChildren:定义是否限制子View在其边界内绘制。默认值是true。
    : 当clipChildren为false时候,就代表里面的子View可以高过父View而不会被裁切掉。
  2. clipToPadding:当padding不等于0时候,定义ViewGroup是否会裁切它里面的子View和重新计算他们之间的依赖关系(不裁切)
    : clipToPadding为false时候,就代表子View的展示可以忽略内边距padding进行绘制。

正题介绍

CoordinatorLayout.Bahaviour
  1. 自定义属于自己的Behaviour,在attrs文件下定义自己Behaviour的属性。这里的Behaviour的attrs属性如何传递进来的,也将在下一章Behaviour源码分析中详细讲解。
class TogetherScrolBehaviour(context: Context?, attrs: AttributeSet?) :
    CoordinatorLayout.Behavior<View>(context, attrs) {
      @IdRes
    private var mAnchorId = 0 //被依赖View的id
    @Dimension
    private var mAnchorPadding = 0 //与被依赖View之间的间距

    init {
        if (context != null && attrs != null) {
            val arrayArray = context.obtainStyledAttributes(attrs, R.styleable.TogetherScrolBehaviour)
            val size = arrayArray.indexCount
            for (i in 0 until size) {
                val flag = arrayArray.getIndex(i)
                when (flag) {
                    R.styleable.TogetherScrolBehaviour_anchor_id -> mAnchorId = arrayArray.getResourceId(flag, 0)
                    R.styleable.TogetherScrolBehaviour_anchor_padding -> mAnchorPadding = arrayArray.getDimensionPixelSize(flag, 0)
                }
            }
        }
    }
    ……
}
//自定义属性,用于判断是否为正确被依赖的 target View
<declare-styleable name="TogetherScrolBehaviour">
        <attr name="anchor_padding" format="dimension" />
        <attr name="anchor_id" format="reference" />
  </declare-styleable>
  1. 判断CoordinatorLayout返回的target View是否是自己期待被依赖的View
override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
        if (dependency.id == mAnchorId) {  
            //这里为什么需要这一步,我将在下一章源码分析讲解,原因多个View在同个布局里可能用同一个Behaviour,所以这里看个人功能,我的demo里可能都不需要
            return true
        }
        return super.layoutDependsOn(parent, child, dependency)
    }
  1. 监听被依赖的View的变化,依赖的View伴随做出相应变化。
//这里相对简单,毕竟就是监听滑动距离
override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
        val recyclerView = dependency as? RecyclerView ?: return false
        //computeVerticalScrollOffset是recyclerView整体的滑动距离,这里为什么取一个区间值呢,因为我们只需要看不要依赖View就行了,不需要跟随RecyclerView滑的特别远。
        val trans = MathUtils.clamp(recyclerView.computeVerticalScrollOffset(), 0, child.measuredHeight)
        //每次RecyclerView滑动,依赖的View都要平移,同步RecyclerView的滑动效果
        child.translationY = -trans.toFloat()
        return true
    }
  1. 被依赖的View增加内边距,用在视图层面上把形成一种整体效果。
override fun onLayoutChild(parent: CoordinatorLayout, child: View, layoutDirection: Int): Boolean {
        //可以理解为ViewGroup的layout方法,把child摆放在对应位置上
        parent.onLayoutChild(child, layoutDirection)
        //依赖的View是child
        val headHeight = child.measuredHeight
        //从父布局中寻找被依赖的View,找不到直接返回false重新走一次CoordinatorLayout的onLayoutChild方法
        val recyclerView = parent.findViewById<RecyclerView>(mAnchorId) ?: return false

        if (recyclerView.paddingTop != headHeight + mAnchorPadding) {
            setAnchorPadding(recyclerView, headHeight + mAnchorPadding)
        }
        return true
    }
    //依赖的recyclerView里添加paddingTop,paddingTop为需要依赖的View高和自定义的padding值。
    private fun setAnchorPadding(recyclerView: RecyclerView, paddingTop: Int) {
        val yOffsetOld = recyclerView.computeVerticalScrollOffset()
        recyclerView.setPadding(recyclerView.paddingLeft, paddingTop, recyclerView.paddingRight, recyclerView.paddingBottom)
        val yOffsetNew = recyclerView.computeVerticalScrollOffset()
        recyclerView.offsetChildrenVertical(yOffsetNew - yOffsetOld)
    }
xml代码
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <android.support.design.widget.CoordinatorLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <android.support.v7.widget.RecyclerView
                android:id="@+id/recycler"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:clipToPadding="false"
                android:scrollbarStyle="insideOverlay">
            </android.support.v7.widget.RecyclerView>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="20dp"
                android:text="我要成为recyclerView的一份子"
                app:anchor_id="@+id/recycler"
                app:layout_anchor="@+id/recycler"
                app:layout_anchorGravity="top"
                app:layout_behavior="@string/behaviour_myself" />
        </android.support.design.widget.CoordinatorLayout>
    </FrameLayout>
</layout>

//string里面定义behaviour路径,用在CoordinatorLayout的layoutParams的时候通过我们的全限定名反射生成behaviour。
<resources>
    <string name="behaviour_myself" translatable="false">cn.bili.linsixu.commen_base.behaviour.TogetherScrolBehaviour</string>
</resources>

最终效果图

成功版本
小插曲

其中我第一版本没有设置clipToPadding,导致recyclerView的itemView只能在paddingTop里面进行展示,无法达到最终效果图的完美效果。因为clipToPadding默认是true,就代表着itemView的绘制区域无法越过已经设置好的padding区域。


失败版本.gif
上一篇 下一篇

猜你喜欢

热点阅读