CollapsingToolbarLayout折叠过程源码解读和实现自定义吸顶RecyclerView|吸顶标题栏

篇章目标介绍

CollapsingToolbarLayout提供的原始的样式可能不能满足实际开发的要求,因为本文希望通过了解其内部源码,实现自定义吸顶Toolbar来满足开发的要求。

源码理解

这部分将从折叠布局的构成和源码分析两个角度展开

1.折叠布局的构成

整个折叠布局的构成是通过AppBarLayout包裹CollapsingToolbarLayout折叠布局,CollapsingToolbarLayout是FrameLayout类型的布局,在其子视图的布局中,针对可折叠的部分设置折叠模式为parallax,折叠过程中将跟随平行移动;针对需要吸顶的部分设置折叠模式为pin,折叠过程中相对全局位置不变,这部分通常用来设置吸顶布局。
在这里插入图片描述

2.折叠过程滑动位移的计算

由于整体布局上包裹在AppBarLayout内部中,可以在其内部确认折叠布局的最大滑动范围,其内部累加全部可滑动子视图的高度,然后扣除折叠后的最小高度。

  //计算全部子视图的可滑动范围
  public final int getTotalScrollRange() {
    if (totalScrollRange != INVALID_SCROLL_RANGE) {
      return totalScrollRange;
    }

    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.scrollFlags;

      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 (i == 0 && ViewCompat.getFitsSystemWindows(child)) {
          // If this is the first child and it wants to handle system windows, we need to make
          // sure we don't scroll it past the inset
          range -= getTopInset();
        }
        //如果设置了退出时折叠,那么滑动范围扣除折叠后的最小高度,即内部的Toolbar的高度
        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
          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 totalScrollRange = Math.max(0, range);
  }

那么我们再看看Scroll过程中的滑动位移是如何传递的,其在用于解决嵌套滑动的onNestedScroll方法中调用,当发生了y向滑动,但是没有发生y向滑动的消耗,说明当前正处于头部内容的位置

    @Override
    public void onNestedScroll(
        CoordinatorLayout coordinatorLayout,
        @NonNull T child,
        View target,
        int dxConsumed,
        int dyConsumed,
        int dxUnconsumed,
        int dyUnconsumed,
        int type,
        int[] consumed) {
      if (dyUnconsumed < 0) {
      //当发生了y向滑动,但是没有发生y向滑动的消耗,说明当前正处于头部内容的位置
        // If the scrolling view is scrolling down but not consuming, it's probably be at
        // the top of it's content
        consumed[1] =
            scroll(coordinatorLayout, child, dyUnconsumed, -child.getDownNestedScrollRange(), 0);
      }
    }
...
  //计算滑动位移Offset
  final int scroll(
      CoordinatorLayout coordinatorLayout, V header, int dy, int minOffset, int maxOffset) {
    return setHeaderTopBottomOffset(
        coordinatorLayout,
        header,
        getTopBottomOffsetForScrollingSibling() - dy,
        minOffset,
        maxOffset);
  }

计算滑动位移的详细代码如下

    @Override
    int setHeaderTopBottomOffset(
        @NonNull CoordinatorLayout coordinatorLayout,
        @NonNull T appBarLayout,
        int newOffset,
        int minOffset,
        int maxOffset) {
      final int curOffset = getTopBottomOffsetForScrollingSibling();
      int consumed = 0;

      if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
        // If we have some scrolling range, and we're currently within the min and max
        //确保newOffset不超出最大值和最小值范围
        // offsets, calculate a new offset
        newOffset = MathUtils.clamp(newOffset, minOffset, maxOffset);
        if (curOffset != newOffset) {
          final int interpolatedOffset =
              appBarLayout.hasChildWithInterpolator()
                  ? interpolateOffset(appBarLayout, newOffset)
                  : newOffset;

          final boolean offsetChanged = setTopAndBottomOffset(interpolatedOffset);
         //消耗位移即dy,并更新全部变量
          // Update how much dy we have consumed
          consumed = curOffset - newOffset;
          // Update the stored sibling offset
          offsetDelta = newOffset - interpolatedOffset;

          if (!offsetChanged && appBarLayout.hasChildWithInterpolator()) {
            // If the offset hasn't changed and we're using an interpolated scroll
            // then we need to keep any dependent views updated. CoL will do this for
            // us when we move, but we need to do it manually when we don't (as an
            // interpolated scroll may finish early).
            coordinatorLayout.dispatchDependentViewsChanged(appBarLayout);
          }
          //给所有观察者通知滑动位移offset变化
          // Dispatch the updates to any listeners
          appBarLayout.onOffsetChanged(getTopAndBottomOffset());

          // Update the AppBarLayout's drawable state (for any elevation changes)
          updateAppBarLayoutDrawableState(
              coordinatorLayout,
              appBarLayout,
              newOffset,
              newOffset < curOffset ? -1 : 1,
              false /* forceJump */);
        }
      } else {
        // Reset the offset delta
        offsetDelta = 0;
      }

      return consumed;
    }
    ...

在通知观察者的方法中我们看到其类型为BaseOnOffsetChangedListener,我们参照AppBarLayout当中的OffsetUpdateListener(为其子类)进行实现即可,后文的定义中基本就是参照这个思路

3.滑动过程的标题缩放的实现

我们可以看到效果中针对CollapsingToolbarLayout设置的标题在滑动过程中是可以实现缩放和移动的,其内部绘制文字是放在了CollapsingTextHelper类中

 //创建包含文字的Bitmap,存放在全局变量expandedTitleTexture中
  private void ensureExpandedTexture() {
    if (expandedTitleTexture != null || expandedBounds.isEmpty() || TextUtils.isEmpty(textToDraw)) {
      return;
    }

    calculateOffsets(0f);
    textureAscent = textPaint.ascent();
    textureDescent = textPaint.descent();
    //计算待绘制的文字的宽度和高度
    final int w = Math.round(textPaint.measureText(textToDraw, 0, textToDraw.length()));
    final int h = Math.round(textureDescent - textureAscent);

    if (w <= 0 || h <= 0) {
      return; // If the width or height are 0, return
    }
   //创建一个Bitmap用于布置画布Canvas绘制文字
    expandedTitleTexture = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);

    Canvas c = new Canvas(expandedTitleTexture);
    //绘制文字至特定的框范围内
    c.drawText(textToDraw, 0, textToDraw.length(), 0, h - textPaint.descent(), textPaint);

    if (texturePaint == null) {
      // Make sure we have a paint
      texturePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
    }
  }

绘制画布的源码如下

  public void draw(@NonNull Canvas canvas) {
    final int saveCount = canvas.save();

    if (textToDraw != null && drawTitle) {
      float x = currentDrawX;
      float y = currentDrawY;

      final boolean drawTexture = useTexture && expandedTitleTexture != null;

      final float ascent;
      final float descent;
      //需要绘制文字时,需要对其位置进行比例缩放
      if (drawTexture) {
        ascent = textureAscent * scale;
        descent = textureDescent * scale;
      } else {
        ascent = textPaint.ascent() * scale;
        descent = textPaint.descent() * scale;
      }

      if (DEBUG_DRAW) {
        // Just a debug tool, which drawn a magenta rect in the text bounds
        canvas.drawRect(
            currentBounds.left, y + ascent, currentBounds.right, y + descent, DEBUG_DRAW_PAINT);
      }

      if (drawTexture) {
        y += ascent;
      }
     //对画布进行缩放
      if (scale != 1f) {
        canvas.scale(scale, scale, x, y);
      }
      //绘制需要的文本
      if (drawTexture) {
        // If we should use a texture, draw it instead of text
        //基于Bitmap绘制,版本18以下且有缩小的情况使用此方法
        canvas.drawBitmap(expandedTitleTexture, x, y, texturePaint);
      } else {
      //直接绘制文字,除以上情况
        canvas.drawText(textToDraw, 0, textToDraw.length(), x, y, textPaint);
      }
    }

自定义吸顶RecyclerView效果

其实自定义的是AppBarLayout即可实现以下效果,如以下所示标题栏展开时大型专辑封面可以显示,缩小专辑封面不显示;在滑动折叠过程中,缩小专辑逐步显示,大型专辑封面逐步被收起;在完全折叠状态,缩小专辑完全显示,大型专辑封面收起。

展开时

在这里插入图片描述

折叠中

在这里插入图片描述

折叠后

在这里插入图片描述

整体视频

在这里插入图片描述

自定义代码说明

自定义的目标是在处于折叠状态下,能够在顶部的Toolbar中显示专辑图的缩小图片。首先需要能够监听AppBarLayout滑动位移,判断是出于展开状态,折叠状态还是过程中状态,判断的基本规则如下

状态判断方法
展开状态滑动offset == 0
折叠状态滑动offset 超过了AppBarLayout最大可滑动范围
过程中状态介于上述两种状态之间

定义滑动位移监听的实现类,通过实现AppBarLayout.OnOffsetChangedListener来进行,在其内部封装了对Toolbar添加一个缩小的专辑图图片,并且设置不同的状态下该ImageView设置显示,不显示,设置透明度变化来达成示例的效果

/**
 * 监听Toolbar处于展开状态,还是折叠状态,用于在折叠状态时设置Toolbar的微缩图片显示
 */
public class OffsetListener implements AppBarLayout.OnOffsetChangedListener {
    //缩小状态图像
    private ImageView mImageView;
    //Toobar对象
    private Toolbar mToolbar;
    private static final String TAG = "OffsetListener";

    @Override
    public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
        Log.d(TAG,"offset = "+verticalOffset);
        //未发生滑动时为展开状态
        boolean expandStatus = (verticalOffset == 0);
        //滑动位移超过AppBar最大可滑动距离时,视为折叠状态
        boolean collapseStatus = (Math.abs(verticalOffset) >= appBarLayout.getTotalScrollRange());
        if(expandStatus){
            hideImageView();
        }else if(collapseStatus){
            showImageView();
        }else {
            boolean almostCollapseStatus = (Math.abs(verticalOffset) >= (appBarLayout.getTotalScrollRange()/2));
            if(almostCollapseStatus){
                //折叠中状态
                showImageView();
                int alpha = 255 * Math.abs(verticalOffset) / appBarLayout.getTotalScrollRange();
                setAlpha(alpha);
            }else {
                hideImageView();
            }
        }
    }

    //初始化缩小状态图像
    private void addImageView(){
        if(null == mImageView){
            mImageView = new ImageView(mToolbar.getContext());
        }
        mImageView.setLayoutParams(new ViewGroup.LayoutParams(50,50));
        mImageView.setImageResource(R.drawable.picture_example);
        Glide.with(mImageView).load(R.drawable.picture_example).apply(RequestOptions.circleCropTransform()).into(mImageView);
        mToolbar.addView(mImageView);
        //初始状态设置微缩图片不显示
        mImageView.setVisibility(View.INVISIBLE);
    }

    //设置图像透明度
    private void setAlpha(int alpha){
        if(null == mImageView){
            return;
        }
        mImageView.setAlpha(alpha);
    }

    //显示缩小状态图像
    private void showImageView(){
        if(null == mImageView){
            return;
        }
        mImageView.setVisibility(View.VISIBLE);
    }

    //隐藏缩小状态图像
    private void hideImageView(){
        if(null == mImageView){
            return;
        }
        mImageView.setVisibility(View.INVISIBLE);
    }

    public void addToolbar(Toolbar toolbar){
        this.mToolbar = toolbar;
        addImageView();
    }


    public void removeToolbar(Toolbar toolbar){
        this.mImageView = null;
        this.mToolbar = null;
    }
}

然后通过AppBarLayout对象注册监听

    //可折叠标题栏
    private AppBarLayout mAppBar;
    mAppBar = findViewById(R.id.app_bar);
    mOffsetListener = new OffsetListener();
    //注意此处应在调用setSupportActionBar之前完成
    mOffsetListener.addToolbar(mToolbar);
    setSupportActionBar(mToolbar);
    mAppBar.addOnOffsetChangedListener(mOffsetListener);

也可以对Toolbar中的返回按键定义相应的返回逻辑,其基本代码如下

    //设置Toobar按键监听
    private void initListener(){
        mToolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this,"点击了Toolbar" ,Toast.LENGTH_SHORT).show();
            }
        });
    }

为了避免内存泄漏,最后注意需要在视图回收时移除监听

    @Override
    protected void onDestroy() {
        mOffsetListener.removeToolbar(mToolbar);
        super.onDestroy();
    }

学习心得

本文是以要通过自定义在Toolbar中添加一个缩小的专辑图为例来阐述折叠过程。在实际开发需要中我们仍然会有很多多样化的需求,比如我们需要实现被折叠的控件不是这里的大专辑图ImageView,而是其他控件,该怎么实现呢。其后续的拓展思路仍然是在layout布局中CollapsingToolbarLayout子视图中添加需要折叠的布局,注意要设置app:layout_collapseMode="parallax"的属性,CollapsingToolbarLayout是FrameLayout子类,按照FrameLayout的布局要求定义好要求的属性,再配合以合适的尺寸和间距定义即可实现你需要的任何效果。其布局基本样式如下,可以在示例的可折叠的ImageView之前设置需要添加的任意Layout,在布局中设置属性为可折叠即可。
在这里插入图片描述

热门文章

暂无图片
编程学习 ·

C语言二分查找详解

二分查找是一种知名度很高的查找算法&#xff0c;在对有序数列进行查找时效率远高于传统的顺序查找。 下面这张动图对比了二者的效率差距。 二分查找的基本思想就是通过把目标数和当前数列的中间数进行比较&#xff0c;从而确定目标数是在中间数的左边还是右边&#xff0c;将查…
暂无图片
编程学习 ·

GMX 命令分类列表

建模和计算操作命令&#xff1a; 1.1 . 创建拓扑与坐标文件 gmx editconf - 编辑模拟盒子以及写入子组(subgroups) gmx protonate - 结构质子化 gmx x2top - 根据坐标生成原始拓扑文件 gmx solvate - 体系溶剂化 gmx insert-molecules - 将分子插入已有空位 gmx genconf - 增加…
暂无图片
编程学习 ·

一文高效回顾研究生课程《数值分析》重点

数值分析这门课的本质就是用离散的已知点去估计整体&#xff0c;就是由黑盒子产生的结果去估计这个黑盒子。在数学里这个黑盒子就是一个函数嘛&#xff0c;这门课会介绍许多方法去利用离散点最大化地逼近这个函数&#xff0c;甚至它的导数、积分&#xff0c;甚至微分方程的解。…
暂无图片
编程学习 ·

在职阿里5年,一个28岁女软测工程师的心声

简单的先说一下&#xff0c;坐标杭州&#xff0c;14届本科毕业&#xff0c;算上年前在阿里巴巴的面试&#xff0c;一共有面试了有6家公司&#xff08;因为不想请假&#xff0c;因此只是每个晚上去其他公司面试&#xff0c;所以面试的公司比较少&#xff09; ​ 编辑切换为居中…
暂无图片
编程学习 ·

字符串左旋c语言

目录 题目&#xff1a; 解题思路&#xff1a; 第一步&#xff1a; 第二步&#xff1a; 第三步&#xff1a; 总代码&#xff1a; 题目&#xff1a; 实现一个函数&#xff0c;可以左旋字符串中的k个字符。 例如&#xff1a; ABCD左旋一个字符得到BCDA ABCD左旋两个字符…
暂无图片
编程学习 ·

设计模式--观察者模式笔记

模式的定义与特点 观察者&#xff08;Observer&#xff09;模式的定义&#xff1a;指多个对象间存在一对多的依赖关系&#xff0c;当一个对象的状态发生改变时&#xff0c;所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式&#xf…
暂无图片
编程学习 ·

睡觉突然身体动不了,什么是睡眠痽痪症

很多朋友可能有这样的体验&#xff0c;睡觉过程中突然意识清醒&#xff0c;身体却动弹不了。这时候感觉非常恐怖&#xff0c;希望旁边有一个人推自己一下。阳光以前也经常会碰到这样的情况&#xff0c;一年有一百多次&#xff0c;那时候很害怕晚上到来&#xff0c;睡觉了就会出…
暂无图片
编程学习 ·

深入理解C++智能指针——浅析MSVC源码

文章目录unique_ptrshared_ptr 与 weak_ptrstd::bad_weak_ptr 异常std::enable_shared_from_thisunique_ptr unique_ptr 是一个只移型别&#xff08;move-only type&#xff0c;只移型别还有std::mutex等&#xff09;。 结合一下工厂模式&#xff0c;看看其基本用法&#xff…
暂无图片
编程学习 ·

@TableField(exist = false)

TableField(exist false) //申明此字段不在数据库存在&#xff0c;但代码中需要用到它&#xff0c;通知Mybatis-plus在做写库操作是忽略它。,.
暂无图片
编程学习 ·

Java Web day15

第十二章文件上传和下载 一、如何实现文件上传 要实现Web开发中的文件上传功能&#xff0c;通常需要完成两步操作&#xff1a;一.是在Web页面中添加上传输入项&#xff1b;二是在Servlet中读取上传文件的数据&#xff0c;并保存到本地硬盘中。 需要使用一个Apache组织提供一个…
暂无图片
编程学习 ·

【51nod 2478】【单调栈】【前缀和】小b接水

小b接水题目解题思路Code51nod 2478 小b接水 题目 输入样例 12 0 1 0 2 1 0 1 3 2 1 2 1输出样例 6解题思路 可以发现最后能拦住水的都是向两边递减高度&#xff08;&#xff1f;&#xff09; 不管两个高积木之间的的积木是怎样乱七八糟的高度&#xff0c;最后能用来装水的…
暂无图片
编程学习 ·

花了大半天写了一个UVC扩展单元调试工具

基于DIRECTSHOW 实现的&#xff0c;用的是MFC VS2019. 详见&#xff1a;http://www.usbzh.com/article/detail-761.html 获取方法 加QQ群:952873936&#xff0c;然后在群文件\USB调试工具&测试软件\UVCXU-V1.0(UVC扩展单元调试工具-USB中文网官方版).exe USB中文网 USB中文…
暂无图片
编程学习 ·

贪心(一):区间问题、Huffman树

区间问题 例题一&#xff1a;区间选点 给定 N 个闭区间 [ai,bi]请你在数轴上选择尽量少的点&#xff0c;使得每个区间内至少包含一个选出的点。 输出选择的点的最小数量。 位于区间端点上的点也算作区间内。 输入格式 第一行包含整数 N&#xff0c;表示区间数。 接下来 …
暂无图片
编程学习 ·

C语言练习实例——费氏数列

目录 题目 解法 输出结果 题目 Fibonacci为1200年代的欧洲数学家&#xff0c;在他的着作中曾经提到&#xff1a;「若有一只免子每个月生一只小免子&#xff0c;一个月后小免子也开始生产。起初只有一只免子&#xff0c;一个月后就有两只免子&#xff0c;二个月后有三只免子…
暂无图片
编程学习 ·

Android开发(2): Android 资源

个人笔记整理 Android 资源 Android中的资源&#xff0c;一般分为两类&#xff1a; 系统内置资源&#xff1a;Android SDK中所提供的已经定义好的资源&#xff0c;用户可以直接拿来使用。 用户自定义资源&#xff1a;用户自己定义或引入的&#xff0c;只适用于当前应用的资源…
暂无图片
编程学习 ·

零基础如何在短时间内拿到算法offer

​算法工程师是利用算法处理事物的职业 算法&#xff08;Algorithm&#xff09;是一系列解决问题的清晰指令&#xff0c;也就是说&#xff0c;能够对一定规范的输入&#xff0c;在有限时间内获得所要求的输出。 如果一个算法有缺陷&#xff0c;或不适合于某个问题&#xff0c;执…
暂无图片
编程学习 ·

人工智能:知识图谱实战总结

人工智能python&#xff0c;NLP&#xff0c;知识图谱&#xff0c;机器学习&#xff0c;深度学习人工智能&#xff1a;知识图谱实战前言一、实体建模工具Protegepython&#xff0c;NLP&#xff0c;知识图谱&#xff0c;机器学习&#xff0c;深度学习 人工智能&#xff1a;知识图…
暂无图片
编程学习 ·

【无标题】

这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注…