CollapsingToolbarLayout源码分析

version: 26.1.0

Demo

CollapsingToolbarLayout构造器

//检查当前的activity是否引用AppCompat的主题
ThemeUtils.checkAppCompatTheme(context);
//文字收缩的帮助类
mCollapsingTextHelper = new CollapsingTextHelper(this);
....
// 保证调用invalidate()时, 该viewgroup的 draw, drawChild的方法能调用
setWillNotDraw(false);

// 设置OnApplyWindowInsetsListener, 用于监听WindowInsets的状态, WindowInsets是指状态栏, 导航栏.
ViewCompat.setOnApplyWindowInsetsListener(this,
        new android.support.v4.view.OnApplyWindowInsetsListener() {
            @Override
            public WindowInsetsCompat onApplyWindowInsets(View v,
                    WindowInsetsCompat insets) {
                // 当前activity的高度 = 手机屏幕 - 状态栏 - 导航栏,突然间请求activity的视图嵌入到状态栏或者导航栏里,
                // 这时activity的高度 = 手机屏幕。 这种情况下就会触发onWindowInsetChanged
                // onWindowInsetChanged方法的逻辑是:当前insets不一致时就会回调,调用reqeustLayout请求重新布局
                return onWindowInsetChanged(insets);
            }
        });

onAttachedToWindow与onDetachedFromWindow

CollapsingToolbarLayout的收缩动画需要他的父类是AppBarLayout,而且还要依赖CoordinatorLayout,Behavior.
onAttachedToWindow()就做了两件事:1. 获取父别布局AppBarLayout添加OnOffsetChangedListener监听
2. ViewCompat.requestApplyInsets(this) 请求安装WindowInsets
onDetachedFromWindow():移除OnOffsetChangedListener监听
简单讲一下WindowInsets相关几个方法: requestApplyInsets, setOnApplyWindowInsetsListener, setFitsSystemWindows
状态栏只有一个,只能被一个View消耗掉,当调用requestApplyInsets 就会重新分配一次WindowInsets, OnApplyWindowInsetsListener就会被回调
setFitsSystemWindows: 给当前View设置了一个标志

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 关闭收缩(默认)
  • COLLAPSE_MODE_PIN 别针模式
  • COLLAPSE_MODE_PARALLAX 视差模式
  1. mParallaxMult: 视差因数 (默认是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,找到后会调用updateDummyView(), 当mCollapsingTitleEnabled为true时,这个方法给Toolbar添加一个虚拟的View, 覆盖在Toolbar上面.

寻找Toolbar有两种情况:

  • Toolbar是直接子View.
  • Toolbar不是直接子View, 这种情况需要使用app:toolbarId或者代码设置, 并会赋值给mToolbar, 而且通过mToolbar的getParent去遍历,给mToolbarDirectChild赋值.(mToolbarDirectChild是CollapsingToolbarLayout的直接子View)
  1. topInset > 0 时是需要嵌入到状态栏下的情况,如果高度设置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. 当需要嵌入到状态栏下的时,fitsSystemWindows为false的子View向下偏移状态栏的高度。
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时,处理Title的收缩动画,主要是通过mCollapsingTextHelper类来处理
if (mDrawCollapsingTitle) {
    final boolean isRtl = ViewCompat.getLayoutDirection(this)
            == ViewCompat.LAYOUT_DIRECTION_RTL;

    // 获取最大偏移量:这里mToolbarDirectChild判断是处理toolbar是不是直接子View的两种情况
    // 最大偏移量可以简单理解:toolbar的底部到CollapsingToolbarLayout的底部的距离
    final int maxOffset = getMaxOffsetForPinChild(
            mToolbarDirectChild != null ? mToolbarDirectChild : mToolbar);

    //计算收缩和展开的边界, 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的位置,根据偏移量上下移动
for (int i = 0, z = getChildCount(); i < z; i++) {
    getViewOffsetHelper(getChildAt(i)).onViewLayout();
}
  1. 这里调用updateScrimVisibility()就为了更新mContentScrim和mStatusBarScrim,这个下面会讲到。

OnOffsetChangedListener 的onOffsetChanged() - 该类核心方法

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

            //1. verticalOffset: 向上收缩时,从0 到 负数, 当完全收缩后,负数会维持在一个最小值; 向下展开时,从负数到0。

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

            //2. 这里要说明一下,收缩或展开的过程中CollapsingToolbarLayout的高度是没有变化的。收缩或展开的过程本质上是AppBarLayout下向上或向下偏移,verticalOffset就是AppBarLayout的偏移量,AppBarLayout相对原来的位置是向上的,所有verticalOffset一直为负数,要想理解整个联动动画的过程可以需要结合CoordinatorLayout的Behavior, NestedScrollingParent, NestedScrollingChild, AppBarLayout在理解才可以,
            //这里不展开啦,只关注CollapsingToolbarLayout本身

            //下面这个循环目的是根据collpaseMode来更新子View的偏移量
            // 1. PIN 模式: pin是别针的意思,大概意思就是订在这里不动。收缩时AppBarLayout在向上偏移,要想保证child不动,就需要反方向偏移
            // 2. PARALLAX 模式:视差效果,这个效果原理很简单, AppBarLayout在向上偏移,而child向上的偏移量 -verticalOffset 乘上一个因子, 保证不和AppBarLayout偏移量同步就产生了视差效果
            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;
                }
            }


            //更新状态栏和收缩后内容的背景

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

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


            // 根据verticalOffset偏移量和expandRange展开的范围算出因数,交给mCollapsingTextHelper调整title字体的大小,绘制的边界等参数,mCollapsingTextHelper.setExpansionFraction()里面会调用view重绘制的方法,CollapsingToolbarLayout的onDraw会被调用, 将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()

前面也说到updateScrimVisibility()这个方法是更新状态栏和收缩后内容的背景的
mContentScrim 和 mStatusBarScrim 都是Drawable来的,可以通过app:contentScrim和
app:statusBarScrim来设置。看上面的demo, 我把mContentScrim和mStatusBarScrim都设置为粉红色,你看gif会发现,只有向上收缩到一定层度时粉红色背景才会出现,mStatusBarScrim代表状态栏高度下面的背景,其它部分就是mContentScrim。控制这两个背景出现的时机是通过mScrimVisibleHeightTrigger这个变量,也可以通过app:scrimVisibleHeightTrigger来设置。

看这个方法,当getHeight() + mCurrentOffset < getScrimVisibleHeightTrigger()时才生效,当调用setScrimsShown(true)时,会通过改变mScrimAlpha这个透明度和要求重绘制达到效果

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

最后看看onDraw()的实现, 前面已经分析过了,这个方法会分3部分绘制。

  1. mContentScrim的绘制
  2. 通过mCollapsingTextHelper绘制Toolbar的Title
  3. mStatusBarScrim的绘制,只有layout是嵌入到状态栏下才会绘制,通过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);
    }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容