欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Android進(jìn)階CoordinatorLayout協(xié)調(diào)者布局實(shí)現(xiàn)吸頂效果

 更新時(shí)間:2023年01月29日 09:54:40   作者:layz4android  
這篇文章主要為大家介紹了Android進(jìn)階CoordinatorLayout協(xié)調(diào)者布局實(shí)現(xiàn)吸頂效果,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

引言

在上一節(jié)Android進(jìn)階寶典 -- NestedScroll嵌套滑動機(jī)制實(shí)現(xiàn)吸頂效果 中,我們通過自定義View的形式完成了TabBar的吸頂效果,其實(shí)除了這種方式之外,MD控件中提供了一個(gè)CoordinatorLayout,協(xié)調(diào)者布局,這種布局同樣可以實(shí)現(xiàn)吸頂效果,但是很多伙伴們對于CoordinatorLayout有點(diǎn)兒陌生,或者認(rèn)為它用起來比較麻煩,其實(shí)大多數(shù)原因是因?yàn)閷τ谒脑聿惶煜?,不知道什么時(shí)候該用什么樣的組件或者behavior,所以首先了解它的原理,就能夠?qū)oordinatorLayout駕輕就熟。

1 CoordinatorLayout功能介紹

首先我們先從源碼中能夠看到,CoordinatorLayout只實(shí)現(xiàn)了parent接口(這里如果不清楚parent接口是干什么的,建議看看前面的文章,不然根本不清楚我講的是什么),說明CoordinatorLayout只能作為父容器來使用。

public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent2,
        NestedScrollingParent3

所以對于CoordinatorLayout來說,它的主要作用就是用來管理子View或者子View之間的聯(lián)動交互。所以在上一篇文章中,我們介紹的NestScroll嵌套滑動機(jī)制,它其實(shí)能夠?qū)崿F(xiàn)child與parent的嵌套滑動,但是是1對1的;而CoordinatorLayout是能夠管理子View之間的交互,屬于1對多的。

那么CoordinatorLayout能夠?qū)崿F(xiàn)哪些功能呢?

(1)子控件之間的交互依賴;

(2)子控件之間的嵌套滑動;

(3)子控件寬高的測量;

(4)子控件事件攔截與響應(yīng);

那么以上所有的功能實(shí)現(xiàn),全部都是依賴于CoordinatorLayout中提供的一個(gè)Behavior插件。CoordinatorLayout將所有的事件交互都扔給了Behavior,目的就是為了解耦;這樣就不需要在父容器中做太多的業(yè)務(wù)邏輯,而是通過不同的Behavior控制子View產(chǎn)生不同的行為

1.1 CoordinatorLayout的依賴交互原理

首先我們先看第一個(gè)功能,處理子控件之間的依賴交互,這種處理方式其實(shí)在很多地方我們都能看到,例如一些小的懸浮窗,你可以拖動它到任何地方,點(diǎn)擊讓其消失的時(shí)候,跟隨這個(gè)View的其他View也會一并消失。

那么如何使用CoordinatorLayout來實(shí)現(xiàn)這個(gè)功能呢?首先我們先看一下CoordinatorLayout處理這種事件的原理。

看一下上面的圖,在協(xié)調(diào)者布局中,有3個(gè)子View:dependcy、child1、child2;當(dāng)dependcy的發(fā)生位移或者消失的時(shí)候,那么CoordinatorLayout會通知所有與dependcy依賴的控件,并且調(diào)用他們內(nèi)部聲明的Behavior,告知其依賴的dependcy發(fā)生變化了。

那么如何判斷依賴哪個(gè)控件,CoordinatorLayout-Behavior提供一個(gè)方法:layoutDependsOn,接收到的通知是什么樣的呢?onDependentViewChanged / onDependentViewRemoved 分別代表依賴的View位置發(fā)生了變化和依賴的View被移除,這些都會交給Behavior來處理。

1.2 CoordinatorLayout的嵌套滑動原理

這部分其實(shí)還是挺簡單的,如果有上一篇文章的基礎(chǔ),那么對于嵌套滑動就非常熟悉了

因?yàn)槲覀兦懊嬲f過, CoordinatorLayout只能作為父容器,因?yàn)橹粚?shí)現(xiàn)了parent接口,所以在CoordinatorLayout內(nèi)部需要有一個(gè)child,那么當(dāng)child滑動時(shí),首先會把實(shí)現(xiàn)傳遞給父容器,也就是CoordinatorLayout,再由CoordinatorLayout分發(fā)給每個(gè)child的Behavior,由Behavior來完成子控件的嵌套滑動。

這里有個(gè)問題,每個(gè)child都一定是CoordinatorLayout的直接子View嗎?

剩下的兩個(gè)功能就比較簡單了,同樣也是在Behavior中進(jìn)行處理,就不做介紹了。

2 CoordinatorLayout源碼分析

首先這里先跟大家說一下,在看源碼的時(shí)候,我們最好依托于一個(gè)實(shí)例的實(shí)現(xiàn),從而帶著問題去源碼中尋找答案,例如我們在第一節(jié)中提到過的CoordinatorLayout的四大功能,可能都會有這些問題:

(1)e.g. 控件之間的交互依賴,為什么在一個(gè)child下設(shè)置一個(gè)Behavior,就能夠跟隨DependentView的位置變化一起變化,他們是如何做依賴通信的?

(2)我們在XML中設(shè)置Behavior,是在什么時(shí)候?qū)嵗模?/p>

(3)我們既然使用了CoordinatorLayout布局,那么內(nèi)部是如何區(qū)分誰依賴誰呢?依賴關(guān)系是如何確定的?

(4)什么時(shí)候需要重新 onMeasureChild?什么時(shí)候需要重新onLayoutChild?

(5)每個(gè)設(shè)置Behavior的子View,一定要是CoordinatorLayout的直接子View嗎?

那么帶著這些問題,我們通過源碼來得到答案。

2.1 CoordinatorLayout的依賴交互實(shí)現(xiàn)

如果要實(shí)現(xiàn)依賴交互效果,首先需要兩個(gè)角色,分別是: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)
    }
}

這里寫了一個(gè)很簡單的View,能夠跟隨手指滑動并一起移動,然后我們在當(dāng)前View下加一個(gè)TextView,并讓這個(gè)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
    }
}

如果想要達(dá)到隨手的效果,那么就需要給TextView設(shè)置一個(gè)Behavior,上面我們定義了一個(gè)Behavior,它的主要作用就是,當(dāng)DependentView滑動的時(shí)候,通過CoordinatorLayout來通知所有的DependBehavior修飾的View。

在DependBehavior中,我們看主要有兩個(gè)方法:layoutDependsOn和onDependentViewChanged,這兩個(gè)方法之前在原理中提到過,layoutDependsOn主要是用來決定依賴關(guān)系,看child依賴的是不是DependentView;如果依賴的是DependentView,那么在DependentView滑動的時(shí)候,就會通過回調(diào)onDependentViewChanged,告知子View當(dāng)前dependency的位置信息,從而完成聯(lián)動。

2.2 CoordinatorLayout交互依賴的源碼分析

那么接下來,我們看下CoordinatorLayout是如何實(shí)現(xiàn)這個(gè)效果的。

在看CoordinatorLayout源碼之前,我們首先需要知道View的生命周期,我們知道在onCreate的時(shí)候通過setContentView設(shè)置布局文件,如下所示:

<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的源碼,系統(tǒng)是通過Inflate的方式解析布局文件,然后在onResume的時(shí)候顯示布局,然后隨之會調(diào)用onAttachedToWindow將布局顯示在Window上,我們看下onAttachedToWindow這個(gè)方法。

@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;
}

在這個(gè)方法中,設(shè)置了addOnPreDrawListener監(jiān)聽,此監(jiān)聽在頁面發(fā)生變化(滑動、旋轉(zhuǎn)、重新獲取焦點(diǎn))會產(chǎn)生回調(diào);

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這個(gè)方法中,我們看到有兩個(gè)for循環(huán),從mDependencySortedChildren中取出元素,首先我們先不需要關(guān)心mDependencySortedChildren這個(gè)數(shù)組,這個(gè)雙循環(huán)的目的就是用來判斷View之間是否存在綁定關(guān)系。

首先我們看下第二個(gè)循環(huán),當(dāng)拿到LayoutParams中的Behavior之后,就會調(diào)用Behavior的layoutDependsOn方法,假設(shè)此時(shí)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,那么此時(shí)可以進(jìn)入到代碼塊中,這里會判斷type類型:EVENT_VIEW_REMOVED和其他type,因?yàn)榇藭r(shí)的type不是REMOVE,所以就會調(diào)用BeHavior的onDependentViewChanged方法。

因?yàn)?strong>在onAttachedToWindow中,對View樹中所有的元素都設(shè)置了OnPreDrawListener的監(jiān)聽,所以只要某個(gè)View發(fā)生了變化,都會走到onChildViewsChanged方法中,進(jìn)行相應(yīng)的Behavior檢查并實(shí)現(xiàn)聯(lián)動。

所以第2節(jié)開頭的第一個(gè)問題,當(dāng)DependentView發(fā)生位置變化時(shí),是如何通信到child中的,這里就是通過設(shè)置了onPreDrawListener來監(jiān)聽。

第二個(gè)問題,Behavior是如何被初始化的?如果自定義過XML屬性,那么大概就能了解,一般都是在布局初始化的時(shí)候,拿到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);
    }
}

通過源碼我們可以看到,拿到全類名之后,通過反射的方式來創(chuàng)建Behavior,這里需要注意一點(diǎn),在自定義Behavior的時(shí)候,需要兩個(gè)構(gòu)造參數(shù)CONSTRUCTOR_PARAMS,否則在創(chuàng)建Behavior的時(shí)候會報(bào)錯(cuò),因?yàn)樵诜瓷鋭?chuàng)建Behavior的時(shí)候需要獲取這兩個(gè)構(gòu)造參數(shù)。

static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] {
        Context.class,
        AttributeSet.class
};

報(bào)錯(cuò)類型就是:

Could not inflate Behavior subclass com.lay.learn.asm.behavior.DependBehavior

2.3 CoordinatorLayout子控件攔截事件源碼分析

其實(shí)只要了解了其中一個(gè)功能的原理之后,其他功能都是類似的。對于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;
}

其中有一個(gè)核心方法performIntercept方法,這個(gè)方法中我們可以看到,同樣也是拿到了Behavior的onInterceptTouchEvent方法,來優(yōu)先判斷子View是否需要攔截這個(gè)事件,如果不攔截,那么交給父容器消費(fèi),當(dāng)前一般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嵌套滑動原理分析

對于嵌套滑動,其實(shí)在上一篇文章中已經(jīng)介紹的很清楚了,加上CoordinatorLayout自身的特性,我們知道當(dāng)子View(指的是實(shí)現(xiàn)了nestscrollchild接口的View)嵌套滑動的時(shí)候,那么首先會將事件向上分發(fā)到CoordinatorLayout中,所以在parent中的onNestedPreScroll的方法中會拿到回調(diào)。

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);
    }
}

我們詳細(xì)看下這個(gè)方法,對于parent的onNestedPreScroll方法,當(dāng)然也是會獲取到Behavior,這里也是拿到了子View的Behavior之后,調(diào)用其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>

所以這里我們先定義一個(gè)Behavior,這個(gè)Behavior是用來接收滑動事件分發(fā)的。當(dāng)手指向上滑動的時(shí)候,首先將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----")
        //實(shí)時(shí)測量
        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
        }
        //在這里進(jìn)行事件消費(fèi),我們只需要關(guān)心豎向滑動
        ViewCompat.offsetTopAndBottom(child, -cosumedy)
        //重新賦值
        totalScroll += cosumedy
        consumed[1] = cosumedy
    }
    private fun getMaxScroll(child: TextView): Int {
        return child.height
    }
}

對應(yīng)的布局文件,區(qū)別在于TextView設(shè)置了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>

當(dāng)滾動RecyclerView的時(shí)候,因?yàn)镽ecyclerView屬于nestscrollchild,所以事件先被傳遞到了CoordinatorLayout中,然后通過分發(fā)調(diào)用了TextView中的Behavior中的onNestedPreScroll,在這個(gè)方法中,我們是進(jìn)行了TextView的上下滑動(邊界處理我這邊就不說了,其實(shí)還蠻簡單的),看下效果。

我們發(fā)現(xiàn)有個(gè)問題,就是在TextView上滑離開的之后,RecyclerView上方有一處空白,這個(gè)就是因?yàn)樵赥extView滑動的時(shí)候,RecyclerView沒有跟隨TextView一起滑動。

這個(gè)不就是我們在2.1中提到的這個(gè)效果嗎,所以RecyclerView是需要依賴TextView的,我們需要再次自定義一個(gè)Behavior,完成這種聯(lián)動效果。

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
    }
}

對應(yīng)的布局文件,區(qū)別在于RecyclerView設(shè)置了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>

這里我設(shè)置了RecyclerView依賴于TextView,當(dāng)TextView的位置發(fā)生變化的時(shí)候,就會通知RecyclerView的Behavior中的onDependentViewChanged方法,在這個(gè)方法中可以設(shè)置RecyclerView豎直方向上的偏移量。

具體的偏移量計(jì)算,可以根據(jù)上圖自行推理,因?yàn)門extView移動的時(shí)候,會跟RecyclerView產(chǎn)生一塊位移,RecyclerView需要補(bǔ)上這塊,在onDependentViewChanged方法中。

這時(shí)候我們會發(fā)現(xiàn),即便最外層沒有使用可滑動的布局,依然能夠完成吸頂?shù)男Ч?,這就顯示了CoordinatorLayout的強(qiáng)大之處,當(dāng)然除了移動之外,控制View的顯示與隱藏、動畫效果等等都可以完成,只要熟悉了CoordinatorLayout內(nèi)部的原理,就不怕UI跟設(shè)計(jì)老師的任意需求了。

以上就是Android進(jìn)階CoordinatorLayout協(xié)調(diào)者布局實(shí)現(xiàn)吸頂效果的詳細(xì)內(nèi)容,更多關(guān)于Android CoordinatorLayout吸頂?shù)馁Y料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論