在上一节Android进阶宝典 — NestedScroll嵌套滑动机制完结吸顶作用 中,咱们经过自界说View的办法完结了TabBar的吸顶作用,其实除了这种办法之外,MD控件中供给了一个CoordinatorLayout,和谐者布局,这种布局相同可以完结吸顶作用,可是很多同伴们关于CoordinatorLayout有点儿生疏,或许以为它用起来比较麻烦,其实大多数原因是由于关于它的原理不太了解,不知道什么时分该用什么样的组件或许behavior,所以首要了解它的原理,就可以对CoordinatorLayout驾轻就熟。
1 CoordinatorLayout功用介绍
首要咱们先从源码中可以看到,CoordinatorLayout只完结了parent接口(这儿假如不清楚parent接口是干什么的,建议看看前面的文章,否则底子不清楚我讲的是什么),阐明CoordinatorLayout只能作为父容器来运用。
public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2,
NestedScrollingParent3
所以关于CoordinatorLayout来说,它的首要作用便是用来办理子View或许子View之间的联动交互。所以在上一篇文章中,咱们介绍的NestScroll嵌套滑动机制,它其实可以完结child与parent的嵌套滑动,可是是1对1的;而CoordinatorLayout是可以办理子View之间的交互,归于1对多的。
那么CoordinatorLayout可以完结哪些功用呢?
(1)子控件之间的交互依靠;
(2)子控件之间的嵌套滑动;
(3)子控件宽高的测量;
(4)子控件事情阻拦与响应;
那么以上一切的功用完结,全部都是依靠于CoordinatorLayout中供给的一个Behavior插件。CoordinatorLayout将一切的事情交互都扔给了Behavior,意图便是为了解耦;这样就不需求在父容器中做太多的业务逻辑,而是经过不同的Behavior控制子View产生不同的行为。
1.1 CoordinatorLayout的依靠交互原理
首要咱们先看第一个功用,处理子控件之间的依靠交互,这种处理办法其实在很多地方咱们都能看到,例如一些小的悬浮窗,你可以拖动它到任何地方,点击让其消失的时分,跟从这个View的其他View也会一并消失。
那么怎么运用CoordinatorLayout来完结这个功用呢?首要咱们先看一下CoordinatorLayout处理这种事情的原理。
看一下上面的图,在和谐者布局中,有3个子View:dependcy、child1、child2;当dependcy的产生位移或许消失的时分,那么CoordinatorLayout会告诉一切与dependcy依靠的控件,并且调用他们内部声明的Behavior,奉告其依靠的dependcy产生改变了。
那么怎么判别依靠哪个控件,CoordinatorLayout-Behavior供给一个办法:layoutDependsOn,接纳到的告诉是什么样的呢?onDependentViewChanged / onDependentViewRemoved 别离代表依靠的View方位产生了改变和依靠的View被移除,这些都会交给Behavior来处理。
1.2 CoordinatorLayout的嵌套滑动原理
这部分其实仍是挺简略的,假如有上一篇文章的基础,那么关于嵌套滑动就非常了解了
由于咱们前面说过, CoordinatorLayout只能作为父容器,由于只完结了parent接口,所以在CoordinatorLayout内部需求有一个child,那么当child滑动时,首要会把完结传递给父容器,也便是CoordinatorLayout,再由CoordinatorLayout分发给每个child的Behavior,由Behavior来完结子控件的嵌套滑动。
这儿有个问题,每个child都必定是CoordinatorLayout的直接子View吗?
剩下的两个功用就比较简略了,相同也是在Behavior中进行处理,就不做介绍了。
2 CoordinatorLayout源码剖析
首要这儿先跟咱们说一下,在看源码的时分,咱们最好依托于一个实例的完结,然后带着问题去源码中寻找答案,例如咱们在第一节中说到过的CoordinatorLayout的四大功用,可能都会有这些问题:
(1)e.g. 控件之间的交互依靠,为什么在一个child下设置一个Behavior,就可以跟从DependentView的方位改变一同改变,他们是怎么做依靠通信的?
(2)咱们在XML中设置Behavior,是在什么时分实例化的?
(3)咱们已然运用了CoordinatorLayout布局,那么内部是怎么区分谁依靠谁呢?依靠关系是怎么确认的?
(4)什么时分需求从头 onMeasureChild?什么时分需求从头onLayoutChild?
(5)每个设置Behavior的子View,必定要是CoordinatorLayout的直接子View吗?
那么带着这些问题,咱们经过源码来得到答案。
2.1 CoordinatorLayout的依靠交互完结
假如要完结依靠交互作用,首要需求两个人物,别离是:DependentView和子View
class DependentView @JvmOverloads constructor(
val mContext: Context,
val attributeSet: AttributeSet? = null,
val flag: Int = 0
) : View(mContext, attributeSet, flag) {
private var paint: Paint
private var mStartX = 0
private var mStartY = 0
init {
paint = Paint()
paint.color = Color.parseColor("#000000")
paint.style = Paint.Style.FILL
isClickable = true
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.let {
it.drawRect(
Rect().apply {
left = 200
top = 200
right = 400
bottom = 400
},
paint
)
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
Log.e("TAG","ACTION_DOWN")
mStartX = event.rawX.toInt()
mStartY = event.rawY.toInt()
}
MotionEvent.ACTION_MOVE -> {
Log.e("TAG","ACTION_MOVE")
val endX = event.rawX.toInt()
val endY = event.rawY.toInt()
val dx = endX - mStartX
val dy = endY - mStartY
ViewCompat.offsetTopAndBottom(this, dy)
ViewCompat.offsetLeftAndRight(this, dx)
postInvalidate()
mStartX = endX
mStartY = endY
}
}
return super.onTouchEvent(event)
}
}
这儿写了一个很简略的View,可以跟从手指滑动并一同移动,然后咱们在当时View下加一个TextView,并让这个TextView跟着DependentView一同滑动。
class DependBehavior @JvmOverloads constructor(context: Context, attributeSet: AttributeSet) :
CoordinatorLayout.Behavior<View>(context, attributeSet) {
override fun layoutDependsOn(
parent: CoordinatorLayout,
child: View,
dependency: View
): Boolean {
return dependency is DependentView
}
override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: View,
dependency: View
): Boolean {
//获取dependency的方位
child.x = dependency.x
child.y = dependency.bottom.toFloat()
return true
}
}
假如想要到达随手的作用,那么就需求给TextView设置一个Behavior,上面咱们界说了一个Behavior,它的首要作用便是,当DependentView滑动的时分,经过CoordinatorLayout来告诉一切的DependBehavior修饰的View。
在DependBehavior中,咱们看首要有两个办法:layoutDependsOn和onDependentViewChanged,这两个办法之前在原理中说到过,layoutDependsOn首要是用来决定依靠关系,看child依靠的是不是DependentView;假如依靠的是DependentView,那么在DependentView滑动的时分,就会经过回调onDependentViewChanged,奉告子View当时dependency的方位信息,然后完结联动。
2.2 CoordinatorLayout交互依靠的源码剖析
那么接下来,咱们看下CoordinatorLayout是怎么完结这个作用的。
在看CoordinatorLayout源码之前,咱们首要需求知道View的生命周期,咱们知道在onCreate的时分经过setContentView设置布局文件,如下所示:
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.lay.learn.asm.DependentView
android:layout_width="200dp"
android:layout_height="200dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我是跟从者"
app:layout_behavior="com.lay.learn.asm.behavior.DependBehavior"
android:textStyle="bold"
android:textColor="#000000"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
假如咱们了解setContentView的源码,系统是经过Inflate的办法解析布局文件,然后在onResume的时分显现布局,然后随之会调用onAttachedToWindow将布局显现在Window上,咱们看下onAttachedToWindow这个办法。
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
resetTouchBehaviors(false);
if (mNeedsPreDrawListener) {
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
}
final ViewTreeObserver vto = getViewTreeObserver();
vto.addOnPreDrawListener(mOnPreDrawListener);
}
if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {
// We're set to fitSystemWindows but we haven't had any insets yet...
// We should request a new dispatch of window insets
ViewCompat.requestApplyInsets(this);
}
mIsAttachedToWindow = true;
}
在这个办法中,设置了addOnPreDrawListener监听,此监听在页面产生改变(滑动、旋转、从头获取焦点)会产生回调;
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
@Override
public boolean onPreDraw() {
onChildViewsChanged(EVENT_PRE_DRAW);
return true;
}
}
final void onChildViewsChanged(@DispatchChangeEvent final int type) {
final int layoutDirection = ViewCompat.getLayoutDirection(this);
final int childCount = mDependencySortedChildren.size();
final Rect inset = acquireTempRect();
final Rect drawRect = acquireTempRect();
final Rect lastDrawRect = acquireTempRect();
for (int i = 0; i < childCount; i++) {
final View child = mDependencySortedChildren.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (type == EVENT_PRE_DRAW && child.getVisibility() == View.GONE) {
// Do not try to update GONE child views in pre draw updates.
continue;
}
// Update any behavior-dependent views for the change
for (int j = i + 1; j < childCount; j++) {
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
final Behavior b = checkLp.getBehavior();
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
// If this is from a pre-draw and we have already been changed
// from a nested scroll, skip the dispatch and reset the flag
checkLp.resetChangedAfterNestedScroll();
continue;
}
final boolean handled;
switch (type) {
case EVENT_VIEW_REMOVED:
// EVENT_VIEW_REMOVED means that we need to dispatch
// onDependentViewRemoved() instead
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
// Otherwise we dispatch onDependentViewChanged()
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
if (type == EVENT_NESTED_SCROLL) {
// If this is from a nested scroll, set the flag so that we may skip
// any resulting onPreDraw dispatch (if needed)
checkLp.setChangedAfterNestedScroll(handled);
}
}
}
}
releaseTempRect(inset);
releaseTempRect(drawRect);
releaseTempRect(lastDrawRect);
}
在onChildViewsChanged这个办法中,咱们看到有两个for循环,从mDependencySortedChildren中取出元素,首要咱们先不需求关怀mDependencySortedChildren这个数组,这个双循环的意图便是用来判别View之间是否存在绑定关系。
首要咱们看下第二个循环,当拿到LayoutParams中的Behavior之后,就会调用Behavior的layoutDependsOn办法,假设此时child为DependentView,checkChild为TextView;
for (int j = i + 1; j < childCount; j++) {
final View checkChild = mDependencySortedChildren.get(j);
final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
final Behavior b = checkLp.getBehavior();
if (b != null && b.layoutDependsOn(this, checkChild, child)) {
if (type == EVENT_PRE_DRAW && checkLp.getChangedAfterNestedScroll()) {
// If this is from a pre-draw and we have already been changed
// from a nested scroll, skip the dispatch and reset the flag
checkLp.resetChangedAfterNestedScroll();
continue;
}
final boolean handled;
switch (type) {
case EVENT_VIEW_REMOVED:
// EVENT_VIEW_REMOVED means that we need to dispatch
// onDependentViewRemoved() instead
b.onDependentViewRemoved(this, checkChild, child);
handled = true;
break;
default:
// Otherwise we dispatch onDependentViewChanged()
handled = b.onDependentViewChanged(this, checkChild, child);
break;
}
if (type == EVENT_NESTED_SCROLL) {
// If this is from a nested scroll, set the flag so that we may skip
// any resulting onPreDraw dispatch (if needed)
checkLp.setChangedAfterNestedScroll(handled);
}
}
}
从上面的布局文件中看,TextView的Behavior中,layoutDependsOn返回的便是true,那么此时可以进入到代码块中,这儿会判别type类型:EVENT_VIEW_REMOVED和其他type,由于此时的type不是REMOVE,所以就会调用BeHavior的onDependentViewChanged办法。
由于在onAttachedToWindow中,对View树中一切的元素都设置了OnPreDrawListener的监听,所以只需某个View产生了改变,都会走到onChildViewsChanged办法中,进行相应的Behavior查看并完结联动。
所以第2节开头的第一个问题,当DependentView产生方位改变时,是怎么通信到child中的,这儿便是经过设置了onPreDrawListener来监听。
第二个问题,Behavior是怎么被初始化的?假如自界说过XML特点,那么大概就能了解,一般都是在布局初始化的时分,拿到layout_behavior特点初始化,咱们看下源码。
if (mBehaviorResolved) {
mBehavior = parseBehavior(context, attrs, a.getString(
R.styleable.CoordinatorLayout_Layout_layout_behavior));
}
static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
if (TextUtils.isEmpty(name)) {
return null;
}
final String fullName;
if (name.startsWith(".")) {
// Relative to the app package. Prepend the app package name.
fullName = context.getPackageName() + name;
} else if (name.indexOf('.') >= 0) {
// Fully qualified package name.
fullName = name;
} else {
// Assume stock behavior in this package (if we have one)
fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
? (WIDGET_PACKAGE_NAME + '.' + name)
: name;
}
try {
Map<String, Constructor<Behavior>> constructors = sConstructors.get();
if (constructors == null) {
constructors = new HashMap<>();
sConstructors.set(constructors);
}
Constructor<Behavior> c = constructors.get(fullName);
if (c == null) {
final Class<Behavior> clazz =
(Class<Behavior>) Class.forName(fullName, false, context.getClassLoader());
c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
c.setAccessible(true);
constructors.put(fullName, c);
}
return c.newInstance(context, attrs);
} catch (Exception e) {
throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
}
}
经过源码咱们可以看到,拿到全类名之后,经过反射的办法来创建Behavior,这儿需求注意一点,在自界说Behavior的时分,需求两个结构参数CONSTRUCTOR_PARAMS,否则在创建Behavior的时分会报错,由于在反射创建Behavior的时分需求获取这两个结构参数。
static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] {
Context.class,
AttributeSet.class
};
报错类型便是:
Could not inflate Behavior subclass com.lay.learn.asm.behavior.DependBehavior
2.3 CoordinatorLayout子控件阻拦事情源码剖析
其实只需了解了其中一个功用的原理之后,其他功用都是类似的。关于CoordinatorLayout中的子View阻拦事情,咱们可以先看看CoordinatorLayout中的onInterceptTouchEvent办法。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getActionMasked();
// Make sure we reset in case we had missed a previous important event.
if (action == MotionEvent.ACTION_DOWN) {
resetTouchBehaviors(true);
}
final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
resetTouchBehaviors(true);
}
return intercepted;
}
其中有一个核心办法performIntercept办法,这个办法中咱们可以看到,相同也是拿到了Behavior的onInterceptTouchEvent办法,来优先判别子View是否需求阻拦这个事情,假如不阻拦,那么交给父容器消费,当时一般Behavior中也不会阻拦。
private boolean performIntercept(MotionEvent ev, final int type) {
boolean intercepted = false;
boolean newBlock = false;
MotionEvent cancelEvent = null;
final int action = ev.getActionMasked();
final List<View> topmostChildList = mTempList1;
getTopSortedChildren(topmostChildList);
// Let topmost child views inspect first
final int childCount = topmostChildList.size();
for (int i = 0; i < childCount; i++) {
final View child = topmostChildList.get(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior b = lp.getBehavior();
if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {
// Cancel all behaviors beneath the one that intercepted.
// If the event is "down" then we don't have anything to cancel yet.
if (b != null) {
if (cancelEvent == null) {
final long now = SystemClock.uptimeMillis();
cancelEvent = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
}
switch (type) {
case TYPE_ON_INTERCEPT:
b.onInterceptTouchEvent(this, child, cancelEvent);
break;
case TYPE_ON_TOUCH:
b.onTouchEvent(this, child, cancelEvent);
break;
}
}
continue;
}
if (!intercepted && b != null) {
switch (type) {
case TYPE_ON_INTERCEPT:
intercepted = b.onInterceptTouchEvent(this, child, ev);
break;
case TYPE_ON_TOUCH:
intercepted = b.onTouchEvent(this, child, ev);
break;
}
if (intercepted) {
mBehaviorTouchView = child;
}
}
// Don't keep going if we're not allowing interaction below this.
// Setting newBlock will make sure we cancel the rest of the behaviors.
final boolean wasBlocking = lp.didBlockInteraction();
final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);
newBlock = isBlocking && !wasBlocking;
if (isBlocking && !newBlock) {
// Stop here since we don't have anything more to cancel - we already did
// when the behavior first started blocking things below this point.
break;
}
}
topmostChildList.clear();
return intercepted;
}
2.4 CoordinatorLayout嵌套滑动原理剖析
关于嵌套滑动,其实在上一篇文章中现已介绍的很清楚了,加上CoordinatorLayout本身的特性,咱们知道当子View(指的是完结了nestscrollchild接口的View)嵌套滑动的时分,那么首要会将事情向上分发到CoordinatorLayout中,所以在parent中的onNestedPreScroll的办法中会拿到回调。
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
int xConsumed = 0;
int yConsumed = 0;
boolean accepted = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == GONE) {
// If the child is GONE, skip...
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted(type)) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mBehaviorConsumed[0] = 0;
mBehaviorConsumed[1] = 0;
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mBehaviorConsumed, type);
xConsumed = dx > 0 ? Math.max(xConsumed, mBehaviorConsumed[0])
: Math.min(xConsumed, mBehaviorConsumed[0]);
yConsumed = dy > 0 ? Math.max(yConsumed, mBehaviorConsumed[1])
: Math.min(yConsumed, mBehaviorConsumed[1]);
accepted = true;
}
}
consumed[0] = xConsumed;
consumed[1] = yConsumed;
if (accepted) {
onChildViewsChanged(EVENT_NESTED_SCROLL);
}
}
咱们详细看下这个办法,关于parent的onNestedPreScroll办法,当然也是会获取到Behavior,这儿也是拿到了子View的Behavior之后,调用其onNestedPreScroll办法,会把手指滑动的间隔传递到子View的Behavior中。
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="80dp"
android:background="#2196F3"
android:text="这是顶部TextView"
android:gravity="center"
android:textColor="#FFFFFF"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_child"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
所以这儿咱们先界说一个Behavior,这个Behavior是用来接纳滑动事情分发的。当手指向上滑动的时分,首要将TextView躲藏,然后才能滑动RecyclerView。
class ScrollBehavior @JvmOverloads constructor(
val mContext: Context,
val attributeSet: AttributeSet
) : CoordinatorLayout.Behavior<TextView>(mContext, attributeSet) {
//相关于y轴滑动的间隔
private var mScrollY = 0
//一共滑动的间隔
private var totalScroll = 0
override fun onLayoutChild(
parent: CoordinatorLayout,
child: TextView,
layoutDirection: Int
): Boolean {
Log.e("TAG", "onLayoutChild----")
//实时测量
parent.onLayoutChild(child, layoutDirection)
return true
}
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: TextView,
directTargetChild: View,
target: View,
axes: Int,
type: Int
): Boolean {
//意图为了dispatch成功
return true
}
override fun onNestedPreScroll(
coordinatorLayout: CoordinatorLayout,
child: TextView,
target: View,
dx: Int,
dy: Int,
consumed: IntArray,
type: Int
) {
//鸿沟处理
var cosumedy = dy
Log.e("TAG","onNestedPreScroll $totalScroll dy $dy")
var scroll = totalScroll + dy
if (abs(scroll) > getMaxScroll(child)) {
cosumedy = getMaxScroll(child) - abs(totalScroll)
} else if (scroll < 0) {
cosumedy = 0
}
//在这儿进行事情消费,咱们只需求关怀竖向滑动
ViewCompat.offsetTopAndBottom(child, -cosumedy)
//从头赋值
totalScroll += cosumedy
consumed[1] = cosumedy
}
private fun getMaxScroll(child: TextView): Int {
return child.height
}
}
对应的布局文件,区别在于TextView设置了ScrollBehavior。
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="80dp"
android:background="#2196F3"
android:text="这是顶部TextView"
android:gravity="center"
android:textColor="#FFFFFF"
app:layout_behavior=".behavior.ScrollBehavior"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_child"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
当翻滚RecyclerView的时分,由于RecyclerView归于nestscrollchild,所以事情先被传递到了CoordinatorLayout中,然后经过分发调用了TextView中的Behavior中的onNestedPreScroll,在这个办法中,咱们是进行了TextView的上下滑动(鸿沟处理我这边就不说了,其实还蛮简略的),看下作用。
咱们发现有个问题,便是在TextView上滑离开的之后,RecyclerView上方有一处空白,这个便是由于在TextView滑动的时分,RecyclerView没有跟从TextView一同滑动。
这个不便是咱们在2.1中说到的这个作用吗,所以RecyclerView是需求依靠TextView的,咱们需求再次自界说一个Behavior,完结这种联动作用。
class RecyclerViewBehavior @JvmOverloads constructor(
val context: Context,
val attributeSet: AttributeSet
) : CoordinatorLayout.Behavior<RecyclerView>(context, attributeSet) {
override fun layoutDependsOn(
parent: CoordinatorLayout,
child: RecyclerView,
dependency: View
): Boolean {
return dependency is TextView
}
override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: RecyclerView,
dependency: View
): Boolean {
Log.e("TAG","onDependentViewChanged ${dependency.bottom} ${child.top}")
ViewCompat.offsetTopAndBottom(child,(dependency.bottom - child.top))
return true
}
}
对应的布局文件,区别在于RecyclerView设置了RecyclerViewBehavior。
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="80dp"
android:background="#2196F3"
android:text="这是顶部TextView"
android:gravity="center"
android:textColor="#FFFFFF"
app:layout_behavior=".behavior.ScrollBehavior"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_child"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"
app:layout_behavior=".behavior.RecyclerViewBehavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
这儿我设置了RecyclerView依靠于TextView,当TextView的方位产生改变的时分,就会告诉RecyclerView的Behavior中的onDependentViewChanged办法,在这个办法中可以设置RecyclerView竖直方向上的偏移量。
详细的偏移量计算,可以依据上图自行推理,由于TextView移动的时分,会跟RecyclerView产生一块位移,RecyclerView需求补上这块,在onDependentViewChanged办法中。
这时分咱们会发现,即便最外层没有运用可滑动的布局,仍然可以完结吸顶的作用,这就显现了CoordinatorLayout的强壮之处,当然除了移动之外,控制View的显现与躲藏、动画作用等等都可以完结,只需了解了CoordinatorLayout内部的原理,就不怕UI跟设计老师的任意需求了。