CollapsingToolbarLayout源碼分析

version: 26.1.0

Demo

CollapsingToolbarLayout構(gòu)造器

//檢查當(dāng)前的activity是否引用AppCompat的主題
ThemeUtils.checkAppCompatTheme(context);
//文字收縮的幫助類
mCollapsingTextHelper = new CollapsingTextHelper(this);
....
// 保證調(diào)用invalidate()時(shí), 該viewgroup的 draw, drawChild的方法能調(diào)用
setWillNotDraw(false);

// 設(shè)置OnApplyWindowInsetsListener, 用于監(jiān)聽(tīng)WindowInsets的狀態(tài), WindowInsets是指狀態(tài)欄, 導(dǎo)航欄.
ViewCompat.setOnApplyWindowInsetsListener(this,
        new android.support.v4.view.OnApplyWindowInsetsListener() {
            @Override
            public WindowInsetsCompat onApplyWindowInsets(View v,
                    WindowInsetsCompat insets) {
                // 當(dāng)前activity的高度 = 手機(jī)屏幕 - 狀態(tài)欄 - 導(dǎo)航欄,突然間請(qǐng)求activity的視圖嵌入到狀態(tài)欄或者導(dǎo)航欄里,
                // 這時(shí)activity的高度 = 手機(jī)屏幕。 這種情況下就會(huì)觸發(fā)onWindowInsetChanged
                // onWindowInsetChanged方法的邏輯是:當(dāng)前insets不一致時(shí)就會(huì)回調(diào),調(diào)用reqeustLayout請(qǐng)求重新布局
                return onWindowInsetChanged(insets);
            }
        });

onAttachedToWindow與onDetachedFromWindow

CollapsingToolbarLayout的收縮動(dòng)畫需要他的父類是AppBarLayout,而且還要依賴CoordinatorLayout,Behavior.
onAttachedToWindow()就做了兩件事:1. 獲取父別布局AppBarLayout添加OnOffsetChangedListener監(jiān)聽(tīng)
2. ViewCompat.requestApplyInsets(this) 請(qǐng)求安裝WindowInsets
onDetachedFromWindow():移除OnOffsetChangedListener監(jiān)聽(tīng)
簡(jiǎn)單講一下WindowInsets相關(guān)幾個(gè)方法: requestApplyInsets, setOnApplyWindowInsetsListener, setFitsSystemWindows
狀態(tài)欄只有一個(gè),只能被一個(gè)View消耗掉,當(dāng)調(diào)用requestApplyInsets 就會(huì)重新分配一次WindowInsets, OnApplyWindowInsetsListener就會(huì)被回調(diào)
setFitsSystemWindows: 給當(dāng)前View設(shè)置了一個(gè)標(biāo)志

final ViewParent parent = getParent();
if (parent instanceof AppBarLayout) {
    // Copy over from the ABL whether we should fit system windows
    ViewCompat.setFitsSystemWindows(this, ViewCompat.getFitsSystemWindows((View) parent));

    if (mOnOffsetChangedListener == null) {
        mOnOffsetChangedListener = new OffsetUpdateListener();
    }
    ((AppBarLayout) parent).addOnOffsetChangedListener(mOnOffsetChangedListener);

    // We're attached, so lets request an inset dispatch
    ViewCompat.requestApplyInsets(this);
}

CollapsingToolbarLayout.LayoutParams

  1. mCollapseMode:
  • COLLAPSE_MODE_OFF 關(guān)閉收縮(默認(rèn))
  • COLLAPSE_MODE_PIN 別針模式
  • COLLAPSE_MODE_PARALLAX 視差模式
  1. mParallaxMult: 視差因數(shù) (默認(rèn)是0.5f)
TypedArray a = c.obtainStyledAttributes(attrs,
                    R.styleable.CollapsingToolbarLayout_Layout);
            mCollapseMode = a.getInt(
                    R.styleable.CollapsingToolbarLayout_Layout_layout_collapseMode,
                    COLLAPSE_MODE_OFF);
            setParallaxMultiplier(a.getFloat(
                    R.styleable.CollapsingToolbarLayout_Layout_layout_collapseParallaxMultiplier,
                    DEFAULT_PARALLAX_MULTIPLIER));
            a.recycle();

onMeasure

  1. ensureToolbar():尋找子View的里Toolbar,并賦值給mToolbar,找到后會(huì)調(diào)用updateDummyView(), 當(dāng)mCollapsingTitleEnabled為true時(shí),這個(gè)方法給Toolbar添加一個(gè)虛擬的View, 覆蓋在Toolbar上面.

尋找Toolbar有兩種情況:

  • Toolbar是直接子View.
  • Toolbar不是直接子View, 這種情況需要使用app:toolbarId或者代碼設(shè)置, 并會(huì)賦值給mToolbar, 而且通過(guò)mToolbar的getParent去遍歷,給mToolbarDirectChild賦值.(mToolbarDirectChild是CollapsingToolbarLayout的直接子View)
  1. topInset > 0 時(shí)是需要嵌入到狀態(tài)欄下的情況,如果高度設(shè)置wrap_content, CollapsingToolbarLayout的高度需要增加topInset.
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   ensureToolbar();
   super.onMeasure(widthMeasureSpec, heightMeasureSpec);

   final int mode = MeasureSpec.getMode(heightMeasureSpec);
   final int topInset = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
   if (mode == MeasureSpec.UNSPECIFIED && topInset > 0) {
       // If we have a top inset and we're set to wrap_content height we need to make sure
       // we add the top inset to our height, therefore we re-measure
       heightMeasureSpec = MeasureSpec.makeMeasureSpec(
               getMeasuredHeight() + topInset, MeasureSpec.EXACTLY);
       super.onMeasure(widthMeasureSpec, heightMeasureSpec);
   }
}

private void updateDummyView() {
  if (!mCollapsingTitleEnabled && mDummyView != null) {
      // If we have a dummy view and we have our title disabled, remove it from its parent
      final ViewParent parent = mDummyView.getParent();
      if (parent instanceof ViewGroup) {
          ((ViewGroup) parent).removeView(mDummyView);
      }
  }
  if (mCollapsingTitleEnabled && mToolbar != null) {
      if (mDummyView == null) {
          mDummyView = new View(getContext());
      }
      if (mDummyView.getParent() == null) {
          mToolbar.addView(mDummyView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
      }
  }
}

onLayout

  1. 當(dāng)需要嵌入到狀態(tài)欄下的時(shí),fitsSystemWindows為false的子View向下偏移狀態(tài)欄的高度。
if (mLastInsets != null) {
    // Shift down any views which are not set to fit system windows
    final int insetTop = mLastInsets.getSystemWindowInsetTop();
    for (int i = 0, z = getChildCount(); i < z; i++) {
        final View child = getChildAt(i);
        if (!ViewCompat.getFitsSystemWindows(child)) {
            if (child.getTop() < insetTop) {
                // If the child isn't set to fit system windows but is drawing within
                // the inset offset it down
                ViewCompat.offsetTopAndBottom(child, insetTop);
            }
        }
    }
}

  1. mCollapsingTitleEnabled為true時(shí),處理Title的收縮動(dòng)畫,主要是通過(guò)mCollapsingTextHelper類來(lái)處理
if (mDrawCollapsingTitle) {
    final boolean isRtl = ViewCompat.getLayoutDirection(this)
            == ViewCompat.LAYOUT_DIRECTION_RTL;

    // 獲取最大偏移量:這里mToolbarDirectChild判斷是處理toolbar是不是直接子View的兩種情況
    // 最大偏移量可以簡(jiǎn)單理解:toolbar的底部到CollapsingToolbarLayout的底部的距離
    final int maxOffset = getMaxOffsetForPinChild(
            mToolbarDirectChild != null ? mToolbarDirectChild : mToolbar);

    //計(jì)算收縮和展開(kāi)的邊界, mDummyView的位置剛好toolbar的位置,用于定位置的      
    ViewGroupUtils.getDescendantRect(this, mDummyView, mTmpRect);
    mCollapsingTextHelper.setCollapsedBounds(
            mTmpRect.left + (isRtl
                    ? mToolbar.getTitleMarginEnd()
                    : mToolbar.getTitleMarginStart()),
            mTmpRect.top + maxOffset + mToolbar.getTitleMarginTop(),
            mTmpRect.right + (isRtl
                    ? mToolbar.getTitleMarginStart()
                    : mToolbar.getTitleMarginEnd()),
            mTmpRect.bottom + maxOffset - mToolbar.getTitleMarginBottom());

    // Update the expanded bounds
    mCollapsingTextHelper.setExpandedBounds(
            isRtl ? mExpandedMarginEnd : mExpandedMarginStart,
            mTmpRect.top + mExpandedMarginTop,
            right - left - (isRtl ? mExpandedMarginStart : mExpandedMarginEnd),
            bottom - top - mExpandedMarginBottom);
    // Now recalculate using the new bounds
    mCollapsingTextHelper.recalculate();
}

  1. 更新子View的位置,根據(jù)偏移量上下移動(dòng)
for (int i = 0, z = getChildCount(); i < z; i++) {
    getViewOffsetHelper(getChildAt(i)).onViewLayout();
}
  1. 這里調(diào)用updateScrimVisibility()就為了更新mContentScrim和mStatusBarScrim,這個(gè)下面會(huì)講到。

OnOffsetChangedListener 的onOffsetChanged() - 該類核心方法

@Override
        public void onOffsetChanged(AppBarLayout layout, int verticalOffset) {
            mCurrentOffset = verticalOffset;

            //1. verticalOffset: 向上收縮時(shí),從0 到 負(fù)數(shù), 當(dāng)完全收縮后,負(fù)數(shù)會(huì)維持在一個(gè)最小值; 向下展開(kāi)時(shí),從負(fù)數(shù)到0。

            final int insetTop = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;

            //2. 這里要說(shuō)明一下,收縮或展開(kāi)的過(guò)程中CollapsingToolbarLayout的高度是沒(méi)有變化的。收縮或展開(kāi)的過(guò)程本質(zhì)上是AppBarLayout下向上或向下偏移,verticalOffset就是AppBarLayout的偏移量,AppBarLayout相對(duì)原來(lái)的位置是向上的,所有verticalOffset一直為負(fù)數(shù),要想理解整個(gè)聯(lián)動(dòng)動(dòng)畫的過(guò)程可以需要結(jié)合CoordinatorLayout的Behavior, NestedScrollingParent, NestedScrollingChild, AppBarLayout在理解才可以,
            //這里不展開(kāi)啦,只關(guān)注CollapsingToolbarLayout本身

            //下面這個(gè)循環(huán)目的是根據(jù)collpaseMode來(lái)更新子View的偏移量
            // 1. PIN 模式: pin是別針的意思,大概意思就是訂在這里不動(dòng)。收縮時(shí)AppBarLayout在向上偏移,要想保證child不動(dòng),就需要反方向偏移
            // 2. PARALLAX 模式:視差效果,這個(gè)效果原理很簡(jiǎn)單, AppBarLayout在向上偏移,而child向上的偏移量 -verticalOffset 乘上一個(gè)因子, 保證不和AppBarLayout偏移量同步就產(chǎn)生了視差效果
            for (int i = 0, z = getChildCount(); i < z; i++) {
                final View child = getChildAt(i);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child);

                switch (lp.mCollapseMode) {
                    case LayoutParams.COLLAPSE_MODE_PIN:
                        offsetHelper.setTopAndBottomOffset(MathUtils.clamp(
                                -verticalOffset, 0, getMaxOffsetForPinChild(child)));
                        break;
                    case LayoutParams.COLLAPSE_MODE_PARALLAX:
                        offsetHelper.setTopAndBottomOffset(
                                Math.round(-verticalOffset * lp.mParallaxMult));
                        break;
                }
            }


            //更新?tīng)顟B(tài)欄和收縮后內(nèi)容的背景

            // Show or hide the scrims if needed
            updateScrimVisibility();

            if (mStatusBarScrim != null && insetTop > 0) {
                ViewCompat.postInvalidateOnAnimation(CollapsingToolbarLayout.this);
            }


            // 根據(jù)verticalOffset偏移量和expandRange展開(kāi)的范圍算出因數(shù),交給mCollapsingTextHelper調(diào)整title字體的大小,繪制的邊界等參數(shù),mCollapsingTextHelper.setExpansionFraction()里面會(huì)調(diào)用view重繪制的方法,CollapsingToolbarLayout的onDraw會(huì)被調(diào)用, 將title繪制畫布上

            // Update the collapsing text's fraction
            final int expandRange = getHeight() - ViewCompat.getMinimumHeight(
                    CollapsingToolbarLayout.this) - insetTop;
            mCollapsingTextHelper.setExpansionFraction(
                    Math.abs(verticalOffset) / (float) expandRange);
        }

updateScrimVisibility()和onDraw()

前面也說(shuō)到updateScrimVisibility()這個(gè)方法是更新?tīng)顟B(tài)欄和收縮后內(nèi)容的背景的
mContentScrim 和 mStatusBarScrim 都是Drawable來(lái)的,可以通過(guò)app:contentScrim和
app:statusBarScrim來(lái)設(shè)置。看上面的demo, 我把mContentScrim和mStatusBarScrim都設(shè)置為粉紅色,你看gif會(huì)發(fā)現(xiàn),只有向上收縮到一定層度時(shí)粉紅色背景才會(huì)出現(xiàn),mStatusBarScrim代表狀態(tài)欄高度下面的背景,其它部分就是mContentScrim??刂七@兩個(gè)背景出現(xiàn)的時(shí)機(jī)是通過(guò)mScrimVisibleHeightTrigger這個(gè)變量,也可以通過(guò)app:scrimVisibleHeightTrigger來(lái)設(shè)置。

看這個(gè)方法,當(dāng)getHeight() + mCurrentOffset < getScrimVisibleHeightTrigger()時(shí)才生效,當(dāng)調(diào)用setScrimsShown(true)時(shí),會(huì)通過(guò)改變mScrimAlpha這個(gè)透明度和要求重繪制達(dá)到效果

final void updateScrimVisibility() {
    if (mContentScrim != null || mStatusBarScrim != null) {
        setScrimsShown(getHeight() + mCurrentOffset < getScrimVisibleHeightTrigger());
    }
}

最后看看onDraw()的實(shí)現(xiàn), 前面已經(jīng)分析過(guò)了,這個(gè)方法會(huì)分3部分繪制。

  1. mContentScrim的繪制
  2. 通過(guò)mCollapsingTextHelper繪制Toolbar的Title
  3. mStatusBarScrim的繪制,只有l(wèi)ayout是嵌入到狀態(tài)欄下才會(huì)繪制,通過(guò)mLastInsets去判斷是否需要繪制
if (mToolbar == null && mContentScrim != null && mScrimAlpha > 0) {
    mContentScrim.mutate().setAlpha(mScrimAlpha);
    mContentScrim.draw(canvas);
}

// Let the collapsing text helper draw its text
if (mCollapsingTitleEnabled && mDrawCollapsingTitle) {
    mCollapsingTextHelper.draw(canvas);
}

// Now draw the status bar scrim
if (mStatusBarScrim != null && mScrimAlpha > 0) {
    final int topInset = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
    if (topInset > 0) {
        mStatusBarScrim.setBounds(0, -mCurrentOffset, getWidth(),
                topInset - mCurrentOffset);
        mStatusBarScrim.mutate().setAlpha(mScrimAlpha);
        mStatusBarScrim.draw(canvas);
    }
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容