自定义Behaviour
2019-11-01 本文已影响0人
Magic旭
伴随着业务的迭代,总会有奇奇怪怪的历史原因,导致需要一些奇奇怪怪的逻辑要实现。
问题描述
- 由于历史接口数据结构原因,原本不同层级的数组希望放在一起,作为RecyclerView的一部分进行滚动。当然你可以用正常方法实现两个类型的Holder,但是我这边因为动态卡片架构不支持,所以只能用到Behaviour去实现了。下面介绍简单demo讲解实现的过程。
前景知识
clipChildren与clipToPadding

- clipChildren:定义是否限制子View在其边界内绘制。默认值是true。
注 : 当clipChildren为false时候,就代表里面的子View可以高过父View而不会被裁切掉。 - clipToPadding:当padding不等于0时候,定义ViewGroup是否会裁切它里面的子View和重新计算他们之间的依赖关系(不裁切)
注 : clipToPadding为false时候,就代表子View的展示可以忽略内边距padding进行绘制。
正题介绍
CoordinatorLayout.Bahaviour
- 自定义属于自己的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>
- 判断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)
}
- 监听被依赖的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
}
- 被依赖的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区域。
