前言

CollapsingToolbarLayout是谷歌官方提供的Material的组件之一,实现了可折叠的Toolbar,效果还不错,具体怎么使用已经有了很多不错的文章,请善用搜索功能,这里就不在赘述了。而我们今天要实现的是往ToolBar`中添加一个头像,

完成的效果如下图


文章会比较长,感兴趣的可以前往项目地址


一、CollapsingToolbarLayout分析

正所谓前人栽树,后人乘凉。要想少走点弯路,多copy看看别人的代码不妨是个好方法。谷歌的CollapsingToolbaLayout写得相当的优雅,主要代码不过几百行。先从构造方法入手:

1. 构造方法

public CollapsingToolbarLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(wrap(context, attrs, defStyleAttr, DEF_STYLE_RES), attrs, defStyleAttr);// Ensure we are using the correctly themed context rather than the context that was passed in.context = getContext();collapsingTextHelper = new CollapsingTextHelper(this);collapsingTextHelper.setTextSizeInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR);collapsingTextHelper.setRtlTextDirectionHeuristicsEnabled(false);elevationOverlayProvider = new ElevationOverlayProvider(context);...// 告诉ViewGroup不要跳过draw方法的调用setWillNotDraw(false);ViewCompat.setOnApplyWindowInsetsListener(this,new androidx.core.view.OnApplyWindowInsetsListener() {@Overridepublic WindowInsetsCompat onApplyWindowInsets(View v, @NonNull WindowInsetsCompat insets) {return onWindowInsetChanged(insets);}});}

这里我做了简化处理,挑了主要部分,简化的部分主要完成从xml获取一些资源,比如Toolbar的标题折叠时的字体大小颜色那些。CollapsingTextHelper是谷歌写的一个工具类,用于辅助计算Toolbar的标题,折叠标题主要是它完成的。

2. 如何与AppBarLayout联动

我们知道CollapsingToolbarLayout需要放在AppBarLayout中才能有效果,否则只是一个普通的FrameLayout。看接下来的代码:

 @Overrideprotected void onAttachedToWindow() {super.onAttachedToWindow();// 如果是AppBarLayout的直接子View,那么给AppBarLayout添加一个OnOffsetChangedListener// Add an OnOffsetChangedListener if possiblefinal ViewParent parent = getParent();if (parent instanceof AppBarLayout) {AppBarLayout appBarLayout = (AppBarLayout) parent;if (onOffsetChangedListener == null) {onOffsetChangedListener = new OffsetUpdateListener();}appBarLayout.addOnOffsetChangedListener(onOffsetChangedListener);...}}@Overrideprotected void onDetachedFromWindow() {// 移除监听器// Remove our OnOffsetChangedListener if possible and it existsfinal ViewParent parent = getParent();if (onOffsetChangedListener != null && parent instanceof AppBarLayout) {((AppBarLayout) parent).removeOnOffsetChangedListener(onOffsetChangedListener);}super.onDetachedFromWindow();}

可以看到,在Attached的时候添加了一个OnOffsetChangedListener,这就是联动的关键,我们看它做了什么

private class OffsetUpdateListener implements AppBarLayout.OnOffsetChangedListener {OffsetUpdateListener() {}@Overridepublic void onOffsetChanged(AppBarLayout layout, int verticalOffset) {currentOffset = verticalOffset;final int insetTop = lastInsets != null ? lastInsets.getSystemWindowInsetTop() : 0;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.collapseMode) {case LayoutParams.COLLAPSE_MODE_PIN:// 需要固定的View固定offsetHelper.setTopAndBottomOffset(MathUtils.clamp(-verticalOffset, 0, getMaxOffsetForPinChild(child)));break;case LayoutParams.COLLAPSE_MODE_PARALLAX:// 需要差速滑动的View设置差速值offsetHelper.setTopAndBottomOffset(Math.round(-verticalOffset * lp.parallaxMult));break;default:break;}}// Show or hide the scrims if neededupdateScrimVisibility();if (statusBarScrim != null && insetTop > 0) {ViewCompat.postInvalidateOnAnimation(CollapsingToolbarLayout.this);}// Update the collapsing text's fractionint height = getHeight();final int expandRange =height - ViewCompat.getMinimumHeight(CollapsingToolbarLayout.this) - insetTop;final int scrimRange = height - getScrimVisibleHeightTrigger();collapsingTextHelper.setFadeModeStartFraction(Math.min(1, (float) scrimRange / (float) expandRange));collapsingTextHelper.setCurrentOffsetY(currentOffset + expandRange);collapsingTextHelper.setExpansionFraction(Math.abs(verticalOffset) / (float) expandRange);}}

先说一下onOffsetChanged(AppBarLayout layout, int verticalOffset)的两个参数

参数 含义
layout 那肯定是AppBarLayout
verticalOffset 垂直的滑动偏移,值处于0-AppBarLayout.height ,总是<=0

上面的代码总共就完成了两件事,根据layoutParam移动子View和重绘标题,记住,是重绘标题。因为标题是使用canvas画的,而不是移动某个TextView。ViewOffsetHelper同样是一个工具类,用于辅助计算移动偏移量的,内部调用了ViewCompat.offsetTopAndBottomViewCompat.offsetLeftAndRight

3. 折叠标题的实现

上面说到标题是使用Canvas画的,具体看下面代码

@Overridepublic void draw(@NonNull 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();// 此处省略contentScrim的绘制...// 绘制标题collapsingTextHelper.draw(canvas);// 此处省略statusBarScrim的绘制...}

可以说很清楚了,先确定是否有ToolBar,然后绘制标题,绘制的具体实现就不展开了,大致就是计算出标题的位置,然后使用StaticLayout画的

ensureToolbar()也是想当关键的一步:

private void ensureToolbar() {if (!refreshToolbar) {return;}// First clear out the current Toolbarthis.toolbar = null;toolbarDirectChild = null;if (toolbarId != -1) {// 如果是通过Id指定的Toolbar,那么获取Toolbar的直接父布局// If we have an ID set, try and find it and it's direct parent to usthis.toolbar = findViewById(toolbarId);if (this.toolbar != null) {toolbarDirectChild = findDirectChild(this.toolbar);}}if (this.toolbar == null) {// 如果是没有ID,那么从直接子View中获取ToolBar// If we don't have an ID, or couldn't find a Toolbar with the correct ID, try and find// one from our direct childrenViewGroup toolbar = null;for (int i = 0, count = getChildCount(); i < count; i++) {final View child = getChildAt(i);if (isToolbar(child)) {toolbar = (ViewGroup) child;break;}}this.toolbar = toolbar;}// 更新虚拟ViewupdateDummyView();refreshToolbar = false;}

上面又调用了一个关键方法updateDummyView()DummyView,顾名思义,虚拟视图,这个View不会进行显示

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

可以看到,dummyView被添加成了Toolbar的直接子View,并且占据了整个Toolbar空间,这就意味着Toolbar的所有部件都将被隐藏,除了导航按钮图标和菜单栏。而dummyView同时被拿做标题绘制的参考View,因为dummyView始终在Toolbar内部,它的左侧紧贴导航图标的右边,右侧紧贴菜单栏的左边,只需要拿到它的位置,很容易就计算出标题的相对位置。

小结

CollapsingToolbarLayout完成折叠标题等功能主要是通过监听AppBarLayout的垂直滑动量来计算子View的位置,然后进行子View的偏移标题的重绘


二、实现

通过上面的分析,我们基本知道大致的流程怎么样了:获取想要操控的子View=>监听AppBarLayout=>在滑动回调时更新子View的形状、大小、位置等信息。

再看看效果图

1. 缩放比例的计算

上图可以看出,ToolBar分为两个状态,展开与折叠,标题和头像随着滑动距离的增加和减少进行相应的放大与缩小,而这个缩放比例怎么计算呢?

上面分析了,onOffsetChanged(AppBarLayout layout, int verticalOffset)回调中的第二个参数与AppBarLayout的高度有关,总是小于0,因此我们可以通过滑动偏移量和高度做运算即可获得当前滑动的比例:

     /*** 获取当前的缩放比例*/private fun getOffsetRatio(): Float {if (minimumHeight == height || -mAppBarLayoutOffset < top) return 0Freturn (mAppBarLayoutOffset + top) * -1.00F / (height - minimumHeight)}

minimumHeight为折叠时的高度,在onMeasure() 中计算的,等于Toolbar的高度。top呢是getTop()的kotlin语法糖,自定义View的就应该很清楚,这个值是到父布局顶部的相对距离。

这个方法拿到了当前的滑动比例,通过这个值我们可以计算View各种参数的缩放比例:

拿标题的文字大小举例:

我们假设文字展开后的大小为X,收缩后的大小为Y,比例为m。因为默认状态是展开,所以,缩放后的大小为X-(X-Y)*m,变化一下就是(1-m)*X+mY,因此我们只需要知道文字的开始和结束大小,就可以计算出缩放后的大小。

2. 标题的处理

从效果图可以看出,ToolBar展开时标题横向居中,字体放大,ToolBar收缩时标题与头像齐平,字体与ToolBar默认标题一样大。因此我们可以定义如下几个参数:

     /*** 当前字体大小*/private var mCurrentTextSize = 15F/*** 展开时字体的放大倍数*/private var mExpandedRatio: Float = TEXT_EXPANDED_RATIO/*** 字体折叠时的大小,默认与ToolBar默认标题一样大*/private var mCollapsingSize = 15F/*** 头像折叠时的大小,默认40dp*/private val mAvatarCollapsedSize: Float

3. 头像位置的处理

展开时头像默认居中,那么头像左侧为(width - mImageView.width) / 2,收缩时头像处于原Toolbar标题时的位置,那肯定会想到dummyView,那我直接获取它的left不就好了吗?

通过事时证明,这是完全错误的。具体原因我暂时没分析出来,因为给布局添加左右padding后dummyView的left的值跟没有padding时是一样的。

然而我们能想到的谷歌肯定能想到,谷歌在CollapsingToolbarLayout使用了一个叫DescendantOffsetUtils工具来计算布局的实时显示位置(相对于父布局),这个工具那是相当的厉害了,无论是调用scale,tanslation等不会改变View布局参数的方法都可以用这个来计算显示的位置,最常见的就是调用scale后获取View的显示大小。然而这个工具并没有公开,被注解了只能同库使用,好在这个就是单独的工具,没有其它依赖,那只能使用CV大法了。

/*** 谷歌写的获取view在parent中的偏移Rect,但是被注解了只能同库使用,所以复制到这来*/
object DescendantOffsetUtils {private val matrix = ThreadLocal<Matrix>()private val rectF = ThreadLocal<RectF>()/*** This is a port of the common [ViewGroup.offsetDescendantRectToMyCoords] from* the framework, but adapted to take transformations into account. The result will be the* bounding rect of the real transformed rect.** @param descendant view defining the original coordinate system of rect* @param rect (in/out) the rect to offset from descendant to this view's coordinate system*/private fun offsetDescendantRect(parent: ViewGroup, descendant: View, rect: Rect) {var m = matrix.get()if (m == null) {m = Matrix()matrix.set(m)} else {m.reset()}offsetDescendantMatrix(parent, descendant, m)var rectF = rectF.get()if (rectF == null) {rectF = RectF()rectF.set(rectF)}rectF.set(rect)m.mapRect(rectF)rect[(rectF.left + 0.5f).toInt(), (rectF.top + 0.5f).toInt(), (rectF.right + 0.5f).toInt()] =(rectF.bottom + 0.5f).toInt()}/*** Retrieve the transformed bounding rect of an arbitrary descendant view. This does not need to* be a direct child.** @param descendant descendant view to reference* @param out rect to set to the bounds of the descendant view*/fun getDescendantRect(parent: ViewGroup, descendant: View, out: Rect) {out[0, 0, descendant.width] = descendant.heightoffsetDescendantRect(parent, descendant, out)}private fun offsetDescendantMatrix(target: ViewParent, view: View, m: Matrix) {val parent = view.parentif (parent is View && parent !== target) {val vp = parent as ViewoffsetDescendantMatrix(target, vp, m)m.preTranslate(-vp.scrollX.toFloat(), -vp.scrollY.toFloat())}m.preTranslate(view.left.toFloat(), view.top.toFloat())if (!view.matrix.isIdentity) {m.preConcat(view.matrix)}}
}

有了实时位置,起始位置和结束为,剩下的就是套公式,那么头像处理方法如下:

 private fun offsetAvatar() {if (mImageView != null) {val offsetRatio = getOffsetRatio()val scale =1 - offsetRatio + offsetRatio * mAvatarCollapsedSize / mImageView!!.measuredWidthmImageView!!.scaleX = scalemImageView!!.scaleY = scaleDescendantOffsetUtils.getDescendantRect(this, mImageView!!, mImageViewBounds)DescendantOffsetUtils.getDescendantRect(this, mDummyView!!, mDummyViewBounds)val left =(1 - offsetRatio) * ((width - mImageView!!.width) / 2) + offsetRatio * mDummyViewBounds.leftval top =(1 - offsetRatio) * (height - mDummyViewBounds.bottom) / 2 +offsetRatio * mDummyViewBounds.top +(mDummyViewBounds.height() - mImageViewBounds.height()) / 2ViewCompat.offsetLeftAndRight(mImageView!!, (left - mImageViewBounds.left).toInt())ViewCompat.offsetTopAndBottom(mImageView!!, (top - mImageViewBounds.top).toInt())}}

最后

这里是源码,欢迎提issue,有什么好看的效果也可以多交流交流。

自定义CollapsingToolbaLayout完成可收缩的带头像的Toolbar相关推荐

  1. 【博客美化】09.评论带头像,且支持旋转

    博客园美化相关文章目录: [博客美化]01.推荐和反对炫酷样式 [博客美化]02.公告栏显示个性化时间 [博客美化]03.分享按钮 [博客美化]04.自定义地址栏logo [博客美化]05.添加Git ...

  2. Android之弹幕(一)带头像

    写直播时组长要求要带头像的弹幕,于是写了一个demo,在弹幕的基础上添加了一个布局,直接上效果吧 先看下xml里面的弹幕布局 <LinearLayoutxmlns:android="h ...

  3. iOS 生成二维码 带头像logo 头像logo带边框 圆角

    1 调用的地方 //生成带边框的圆角图片,这个圆角图标可以先生成,如果放在二维码生成时会影响图片生成速度. self.logo= [self createNewlogoViewView:centerL ...

  4. 【博客美化】评论带头像,且支持旋转

    [博客美化]评论带头像,且支持旋转 好久没有更新关于博客园页面美化的文章了,这一次主要是写一下关于评论带头像,且支持旋转的内容,希望各位小伙伴能够喜欢!!! 1.效果图 2.添加CSS代码 设置-页面 ...

  5. CSS实现的带头像的彩色垂直菜单源码

    大家好,今天给大家介绍一款,用CSS实现的带头像的彩色垂直菜单源码(图1).送给大家哦,获取方式在本文末尾. 图1 鼠标悬停在相应区域,就会出现头像 图2 带切换动画(图3) 图4 部分代码: * { ...

  6. Tp5生成带头像二维码海报(带文字描述,居中调整)

    Tp5生成带头像二维码海报(带文字描述,居中调整) 三张海报中随机生成一张展现 /*** 获取随机海报* Author: yanjie <823986855@qq.com>* Date: ...

  7. 免费生成早安问候图片,在线生成晚安问候图片,带头像。

    最近发现一个小程序挺不错的,可以免费生成带头像的 问候图片.使用也是很方便.推荐给大家. 小程序蚂蚁关怀界面,还挺简洁的. 早安问候,晚安问候,节日问候效果图,非常不错.. 喜欢的话赶紧来体验一下吧. ...

  8. Java 代码基于开源组件生成带头像的二维码

    二维码在我们目前的生活工作中,随处可见,日常开发中难免会遇到需要生成二维码的场景,网上也有很多开源的平台可以使用,不过这里我们可以通过几个开源组件,自己来实现一下. 在动手之前我们先思考一下需要进行的 ...

  9. Java 代码基于开源组件生成带头像的二维码,推荐收藏

    二维码在我们目前的生活工作中,随处可见,日常开发中难免会遇到需要生成二维码的场景,网上也有很多开源的平台可以使用,不过这里我们可以通过几个开源组件,自己来实现一下. 在动手之前我们先思考一下需要进行的 ...

最新文章

  1. C++中的虚函数表介绍
  2. 借助二分法匹配时间戳实现快速查找日志内容
  3. c字符串分割成数组_leetcode第31双周赛第三题leetcode1525. 字符串的好分割数目
  4. 中石油训练赛 - Gone Fishing(固定大小的圆可以覆盖最多的点)
  5. java中接口私有反方_Java 8:在接口中声明私有和受保护的方法
  6. python argvparser_Python ArgumentParse的subparser用法说明
  7. LeetCode 257 二叉树的所有路径
  8. [leetcode]746. 使用最小花费爬楼梯
  9. 如何将背景音乐添加到iMovie?
  10. php如何无水印解析快手,快手短视频无水印解析过程及代码
  11. wps excel 插入公式 整列
  12. 列车停车控制算法及仿真研究
  13. selenium3 设置浏览器安装的位置
  14. JAVA基础(for语句的统计思想)
  15. WebRTC源码下载与编译
  16. mysql capi函数详解_CAPI函数描述(G-N)
  17. Scala基础知识(个人总结)
  18. Effective C++连载
  19. markdown贴gif图片
  20. dsoframer-在线编辑office文档,一款开源的由微软提供

热门文章

  1. 平面设计学习之四(PS-计算磨皮法)
  2. ps 粗糙的练习磨皮小结实现的步骤---粗略的版本
  3. Capture One 22 最新推出全景拼接功能
  4. hdf5-java_Java HDF5LibraryException類代碼示例
  5. ECharts series动态加载 可执行方案
  6. Robocup3D项目搭建
  7. ISP-坏点校正(DPC)
  8. 7家自媒体创业项目平台收益技巧和差异对比,你适合哪个?
  9. 什么是多芯光纤?软光纤、集束光纤、紧套光纤是光纤吗?
  10. Bootrom -> bootloader -> kernel -> init >android