8CollapsingToolbarLayout源码分析
2016-09-28 17:13
330 查看
8CollapsingToolbarLayout源码分析
本文针对上篇文章进行源码分析纯色Toolbar滑动
最简单代码
先从最简单的看起<!--这里必须要写fitsSystemWindows,不然上滑会出现statusbar占2份高度问题--> <android.support.design.widget.AppBarLayout android:fitsSystemWindows="true" android:layout_width="match_parent" android:layout_height="256dp"> <android.support.design.widget.CollapsingToolbarLayout android:id="@+id/collapsingToolbarLayout" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_scrollFlags="scroll"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" android:minHeight="?attr/actionBarSize" app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout>
效果如下所示,toolbar可以伸展
AppBarLayout里有个接口,叫做OnOffsetChangedListener,如果AppBarLayout滑动了就会触发里面的回调onOffsetChanged
/** * Interface definition for a callback to be invoked when an {@link AppBarLayout}'s vertical * offset changes. */ public interface OnOffsetChangedListener { /** * Called when the {@link AppBarLayout}'s layout offset has been changed. This allows * child views to implement custom behavior based on the offset (for instance pinning a * view at a certain y value). * * @param appBarLayout the {@link AppBarLayout} which offset has changed * @param verticalOffset the vertical offset for the parent {@link AppBarLayout}, in px */ void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset); }
AppBarLayout滑动的时候会调用setHeaderTopBottomOffset,里面调用dispatchOffsetUpdates(appBarLayout),如下所示,会把移动的消息发给listeners
private void dispatchOffsetUpdates(AppBarLayout layout) { final List<OnOffsetChangedListener> listeners = layout.mListeners; // Iterate backwards through the list so that most recently added listeners // get the first chance to decide for (int i = 0, z = listeners.size(); i < z; i++) { final OnOffsetChangedListener listener = listeners.get(i); if (listener != null) { listener.onOffsetChanged(layout, getTopAndBottomOffset()); } } }
而CollapsingToolbarLayout在onAttachedToWindow的时候加入
((AppBarLayout) parent).addOnOffsetChangedListener(mOnOffsetChangedListener);
其实就是注册了一个listener,AppBarLayout滑动了,CollapsingToolbarLayout 内的mOnOffsetChangedListener就会知道并作出相应动画,这里其实就是文字的缩小。主要代码在CollapsingTextHelper内,主要就是根据当前AppBarLayout的offset来修改mScale。
此时CollapsingToolbarLayout和AppBarLayout一样大小,包含statusbar 大小为256dp
mTotalScrollRange=range - getTopInset()=256dp-S=609
mDownPreScrollRange 0
mDownScrollRange =256dp=672
我曾经以为mTotalScrollRange= mDownPreScrollRange+ mDownScrollRange,这里不成立了。
我试着把mDownScrollRange强行改为609,滑动依然正常,因为下滑的时候offset是在变大的,所以不会到-672.
为何AppBarLayout内要设置fitsSystemWindows
注意,这里我们在AppBarLayout内要设置fitsSystemWindows为true,为什么呢?如果不设置,在收缩态的时候会出现2条statubar。加上这行代码后,在布局AppBarLayout的时候不会往下偏移一个statusbar的高度,看下边代码,只有child无法fitsSystemWindows, CoordinatorLayout才会考虑往下偏移一个statubar的高度(63).所以加上这行代码之后,AppBarLayout的top会和CoordinatorLayout一样。没有这行代码,AppBarLayout的top会比CoordinatorLayout的top低63.加入这行代码,就是把AppBarLayout拉高,有什么用呢?CollapsingToolbarLayout在画收缩态的伪statusbar的时候,是根据初始情况下AppBarLayout的顶部位置画的。所以没有这行代码,会画到实际statusbar的下方去,这样就出现了2条statusbar。//CoordinatorLayout#onMeasure if (applyInsets && !ViewCompat.getFitsSystemWindows(child)) { // We're set to handle insets but this child isn't, so we will measure the // child as if there are no insets final int horizInsets = mLastInsets.getSystemWindowInsetLeft() + mLastInsets.getSystemWindowInsetRight(); final int vertInsets = mLastInsets.getSystemWindowInsetTop() + mLastInsets.getSystemWindowInsetBottom(); childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( widthSize - horizInsets, widthMode); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( heightSize - vertInsets, heightMode); }
2条statusbar的效果如下,所示,我故意设置了 app:contentScrim=”@color/color_accent_pink”
app:statusBarScrim=”@color/cardview_light_background”,
让效果明显一点,如下所示顶部蓝色为实际的statubar,下边白色为CollapsingToolbarLayout收缩态的时候内部画的伪状态栏,再下边红色的为收缩态的content。
再回头看看,要想AppBarLayout和CollapsingToolbarLayout顶部对齐,我们还可以把CoordinatorLayout和AppBarLayout的fitsSystemWindows都为false。
exitUntilCollapsed
再看设置了exitUntilCollapsed 之后,exitUntilCollapsed意思就是滑出直到折叠状态,即滑出的时候最多到折叠状态,无法完全滑出exitUntilCollapsed会改变上滑的范围,上滑的范围就是mTotalScrollRange,
private int getUpNestedPreScrollRange() { return getTotalScrollRange(); }
public final int getTotalScrollRange() { if (mTotalScrollRange != INVALID_SCROLL_RANGE) { return mTotalScrollRange; } int range = 0; for (int i = 0, z = getChildCount(); i < z; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final int childHeight = child.getMeasuredHeight(); final int flags = lp.mScrollFlags; if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) { // We're set to scroll so add the child's height range += childHeight + lp.topMargin + lp.bottomMargin; if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) { // For a collapsing scroll, we to take the collapsed height into account. // We also break straight away since later views can't scroll beneath // us //减去标记了SCROLL_FLAG_EXIT_UNTIL_COLLAPSED的child的最小高度 range -= ViewCompat.getMinimumHeight(child); break; } } else { // As soon as a view doesn't have the scroll flag, we end the range calculation. // This is because views below can not scroll under a fixed view. break; } } return mTotalScrollRange = Math.max(0, range - getTopInset()); }
由上可知,在算mTotalScrollRange的时候会减去标记了SCROLL_FLAG_EXIT_UNTIL_COLLAPSED的child的最小高度,这里就是减去CollapsingToolbarLayout的minHeight,但是又有个问题,CollapsingToolbarLayout我们并没有设置minHeight,我们只是在Toolbar里设置了minHeight。CollapsingToolbarLayout在onLayout的时候会调用setMinimumHeight(getHeightWithMargins(mToolbar));,这样CollapsingToolbarLayout就有了minHeight,这个值是toolbar的height加上下margin,跟Toolbar的minHeight没关系。试试看把Toolbar的minHeight去掉,毫不影响。所以此时mTotalScrollRange会减去CollapsingToolbarLayout的minHeight,这样上滑的时候就会留出一部分高度,不全部滑出,留出的高度就是CollapsingToolbarLayout的minHeight=toolbar高度+上下margin
定住toolbar
Toolbar设置app:layout_collapseMode=”pin”这居然可以定住toolbar,和appbarlayout的设计又有点不符合,appbarlayout是认为底部可以存在不滑动的区域,但顶部不可以,那这里怎么做到的,实际上,他是随着appbarlayout往上offset了,然后他自己之后又offset了一次,使得toolbar相对屏幕的位置不变。实际上,假设appbarlayout往上滑了11,那么appbarlayout的offset是-11,此时我们又offset了一次,把toolbar相对CollapsingToolbarLayout的offset设置为11,这样toolbar相对屏幕就相当于没变化,核心代码在android.support.design.widget.CollapsingToolbarLayout.OffsetUpdateListener#onOffsetChanged
//CollapsingToolbarLayout.OffsetUpdateListener#onOffsetChanged 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: //调整offset使得child看起来不动 if (getHeight() - insetTop + verticalOffset >= child.getHeight()) { offsetHelper.setTopAndBottomOffset(-verticalOffset); } break; case LayoutParams.COLLAPSE_MODE_PARALLAX: //调整offset实现视差滑动 offsetHelper.setTopAndBottomOffset( Math.round(-verticalOffset * lp.mParallaxMult)); break; } }
带背景图toolbar
对应case1
上滑的过程中,背景由图片变成纯色,状态栏也由透明变为纯色,这个变化是什么时候呢?这个临界点由getScrimTriggerOffset决定//CollapsingToolbarLayout.OffsetUpdateListener#onOffsetChanged // Show or hide the scrims if needed if (mContentScrim != null || mStatusBarScrim != null) { setScrimsShown(getHeight() + verticalOffset < getScrimTriggerOffset() + insetTop); } /** * The additional offset used to define when to trigger the scrim visibility change. */ final int getScrimTriggerOffset() { return 2 * ViewCompat.getMinimumHeight(this); }
截了个图,大概是这个位置,图片可见部分的高度就是getScrimTriggerOffset的值,下一瞬间图片就会变成纯色。实际上就是在上面盖了个mContentScrim,mContentScrim就是一个ColorDrawable ,颜色为colorPrimary.由此可见修改CollapsingToolbarLayout的minHeight就可以修改变化瞬间的位置
变成纯色的同时,状态栏也从透明变为有颜色colorPrimaryDark。mScrimAlpha由1变为255,状态栏变为纯色,实际上是在状态栏的位置画了一个纯色的矩形,由mStatusBarScrim来实现,mStatusBarScrim的颜色也可以指定。
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); } }
初始态覆盖状态栏
对应case3给ImageView加上fitSystemWindow,为什么就有效果,让初始态覆盖状态栏
不加的话,ImageView会被设置一个offset(insetTop),让他处于状态栏下边,如果加了,那就进不到L7,所以可以覆盖状态栏。
//android.support.design.widget.CollapsingToolbarLayout#onLayout if (mLastInsets != null && !ViewCompat.getFitsSystemWindows(child)) { final int insetTop = mLastInsets.getSystemWindowInsetTop(); 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); } }
enterAlwaysCollapsed
再来看看enterAlwaysCollapsed有什么用我拿CollapsImageActivity3试了一下,app:layout_scrollFlags=”scroll|enterAlways|enterAlwaysCollapsed” 发现有bug,下滑pre的时候只能滑到这个位置,应该是下滑的范围(mDownPreScrollRange)少算了个statubar。
后来发现可以通过修改theme和fitsystemwindow解决这个问题,对应activity:CollapsImageEnterAlwayCollActivity
theme
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <!-- Customize your theme here. --> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> </style>
布局xml不用任何fitsystemwindow
效果如下
滑动过程是否重绘
我们曾经说过AppBarlayout在滑动过程中是不重绘的,但是里面若有CollapsingToolbarLayout,那CollapsingToolbarLayout就会重绘,为什么?在onOffsetChanged内有一句 ViewCompat.postInvalidateOnAnimation(CollapsingToolbarLayout.this);
导致CollapsingToolbarLayout变为invalidate,所以会被重绘,所以draw也会被调用.
再看看draw,在super.draw(canvas);之后,会mContentScrim.draw和 mStatusBarScrim.draw(canvas);,绘制这2部分区域
//CollapsingToolbarLayout @Override public void draw(Canvas canvas) { super.draw(canvas); // If we don't have a toolbar, the scrim will be not be drawn in drawChild() below. // Instead, we draw it here, before our collapsing text. ensureToolbar(); 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); } } }
总结
1、app:layout_collapseMode有pin和parallax,pin表示某个view相对CoordinatorLayout不动;parallax实现视差滑到,可以用layout_collapseParallaxMultiplier来控制滑动的相对速度,CollapsingToolbarLayout的不同子view可以设置不同的layout_collapseParallaxMultiplier值,默认layout_collapseParallaxMultiplier值为0.52、为了让CollapsingToolbarLayout内部的伪状态栏和真正的statusbar重合, CoordinatorLayout和AppBarLayout的fitsSystemWindows应该一样,同时为true或者false。
相关文章推荐
- 00-java实现设计模式-设计模式概述
- 1.求解N以内素数
- Linux驱动调试
- C#枚举中的位运算权限分配浅谈
- CSRF的攻击与防御
- 使用git(三)远程操作
- spring框架学习(七)—Spring实现IoC的多种方式
- PAT(A) - 1012. The Best Rank (25)
- android开发自定义View(四)仿掌上英雄联盟能力值分析效果
- 绘制树结构
- LINQ to XML
- HTML5 video播放视频的方法
- 记录iOS开发中Xcode所见的错
- define总结
- Long short-term memory 论文小记
- 调试大型matlab数值计算程序的经,
- 业务建模利器-业务序列图
- Windows7中IIS简单安装与配置(详细图解)
- 小端字节序和大端字节序
- PHP获取上周、本周、上月、本月、本季度、上季度时间方法大全