material-scrolling源码精读:RecyclerViewHolder如何桥接ObservableRecyclerView滚动状态
material-scrolling源码精读:RecyclerViewHolder如何桥接ObservableRecyclerView滚动状态
【免费下载链接】material-scrollingAndroid library for material scrolling techniques.项目地址: https://gitcode.com/gh_mirrors/ma/material-scrolling
material-scrolling 是一款 Android Material 滚动技术方案库,把 Google 设计规范的滚动交互变成几行 API。本文带你逐段精读它的核心源码:RecyclerViewHolder究竟如何桥接ObservableRecyclerView的滚动状态,把一次触摸手势变成标题平移、背景视差、FAB 缩放的完整链路。
一句话总览:ObservableRecyclerView 感知手势 → RecyclerViewHolder 归一化坐标 → BehaviorDispatcher 广播 → Behavior 们各自动画。
模块导航:先看地图再读代码 🗺️
| 模块文件 | 角色 |
|---|---|
| library/src/main/java/jp/satorufujiwara/scrolling/RecyclerViewHolder.java | 本文主角:滚动状态桥接器 |
| library/src/main/java/jp/satorufujiwara/scrolling/MaterialScrollingLayout.java | 容器布局,负责"接线" |
| library/src/main/java/jp/satorufujiwara/scrolling/MaterialScrollingViewPager.java | ViewPager 多页联动版本 |
| library/src/main/java/jp/satorufujiwara/scrolling/BehaviorDispatcher.java | 事件广播中心 |
| library/src/main/java/jp/satorufujiwara/scrolling/Behavior.java | 所有滚动行为的抽象基类 |
| library/src/main/java/jp/satorufujiwara/scrolling/Utils.java | 递归查找 ObservableRecyclerView 的工具 |
| sample/src/main/java/jp/satorufujiwara/scrolling/sample/MainActivity.java | 示例入口,看真实用法 |
想看完整演示?README 在
arts/目录下附了三段 GIF(viewpager.gif、imagefab.gif、viewpager_land.gif),分别展示横向翻页、大图 FAB、横屏效果。
核心机制:滚动事件如何"过桥"
RecyclerView 本身只回调"我滚动了",不关心页面上其他 View 怎么动。material-scrolling 的答案是中间层桥接:
触摸手势 │ ▼ ObservableRecyclerView ── 注册回调 ──▶ RecyclerViewHolder (桥) │ 归一化坐标 scrollY + 增量 dy │ 支持开关 / 缓存 / 同步滚动 ▼ BehaviorDispatcher (广播) │ ┌────────────────────────┼────────────────────┐ ▼ ▼ ▼ ScrollBehavior ParallaxBehavior FabBehavior (标题跟手平移) (背景视差偏移) (FAB 缩放消失)逐段精读:RecyclerViewHolder 的 7 个关键设计
以下路径均相对仓库根目录,核心文件为 library/src/main/java/jp/satorufujiwara/scrolling/RecyclerViewHolder.java。
1️⃣ 构造即接线:两行代码完成桥接
this.recyclerView = recyclerView; this.recyclerView.setClipToPadding(false); recyclerView.setScrollViewCallbacks(new OnScrollDelegate(this));- 持有
ObservableRecyclerView引用,并把回调"插线"到内部类OnScrollDelegate(第 72–94 行)——它只干一件事:把onScrollChanged、onDownMotionEvent、onUpOrCancelMotionEvent三个事件原样转发给 holder 自己。 setClipToPadding(false):允许列表内容画进 padding 区,顶部大图的视觉空间由此而来。
2️⃣ flexibleHeight:让 RecyclerView "自带头部留白"
setFlexibleHeight()(第 28–31 行)把布局属性ms_flexible_height转成RecyclerView 顶部 padding:
app:ms_flexible_height="240dp"于是"标题区"不再是一个独立 View,而是列表自带的留白区域——这是整个方案的坐标系基准。
3️⃣ 坐标归一化:把"列表内部位置"换算成"页面滚动量"
getScrollY()(第 37–39 行)是桥的核心:
return isDirty ? scrollY : recyclerView.getCurrentScrollY() + flexibleHeight;- 实时位置 =
getCurrentScrollY() + flexibleHeight,这样 scrollY 在"大图完全收起"时恰好等于 flexibleHeight,后续所有 Behavior 都用同一把尺子; isDirty标志:当scrollTo()主动滚动后,回调还没来得及回来,就优先返回缓存值,保证 ViewPager 同步多页时坐标不穿帮(见第 4 节)。
4️⃣ 增量事件:一次滚动只发"差值"
void onScrolled(final int y) { isDirty = false; if (!isDispatchScroll) return; scrollY = y + flexibleHeight; behaviorDispatcher.onScrolled(scrollY, scrollY - preScrollY); preScrollY = scrollY; }发给 Behavior 的是(scrollY, dy)两个参数——绝对位置 + 本次增量。dy让行为可以写"跟手回弹""惯性感知"这类依赖速度的效果。
5️⃣ 主动滚动:scrollTo 的反向桥
isDirty = true; scrollY = y; final LinearLayoutManager lm = (LinearLayoutManager) recyclerView.getLayoutManager(); lm.scrollToPositionWithOffset(0, -y);桥是双向的:MaterialScrollingViewPager翻页时会用holder.scrollTo(...)把非当前页同步到当前页位置(见 MaterialScrollingViewPager.java 的onPageScrollStateChanged),这就是多页"齐刷刷一起收起"的效果来源。
6️⃣ 事件开关:setIsDispatchScroll
MaterialScrollingLayout在addView时创建 holder、removeView时关闭开关(MaterialScrollingLayout.java 第 50–67 行)。ViewPager 场景更讲究:onPageSelected时先关闭所有 holder 的开关,只打开当前页那个——彻底杜绝旧页残留回调污染动画。
7️⃣ 手势生命周期:按下 / 抬起同样被桥接
onDownMotionEvent与onUpOrCancelMotionEvent(ScrollState)也全部透传给BehaviorDispatcher。Behavior 拿到ScrollState(FLING / DRAG / IDLE)就能做"抬手后回弹"的决策,桥接的不仅是位置,还有手势语义。
三个最容易踩坑的细节 ⚠️
isDirty不是脏检查:它标记的是"我刚执行过主动scrollTo,回调值不可信",读源码时别按字面理解。- 开关先于缓存:
setIsDispatchScroll(false)只拦事件,getScrollY()缓存值仍然有效,两者职责分离。 - 坐标系偏移:所有 Behavior 收到的 scrollY 都已
+ flexibleHeight,写自定义行为时直接用getFlexibleHeight()做上限即可,例如内置的 ScrollBehavior.java:
ViewCompat.setTranslationY(target, -Math.min(scrollY, getFlexibleHeight()));上手只需两步 🚀
第一步,布局里包一层容器并声明 flexible 区域(见 library/src/main/res/values/attrs.xml 定义的属性):
<jp.satorufujiwara.scrolling.MaterialScrollingLayout app:ms_flexible_height="240dp"> <com.github.ksoichiro.android.observablescrollview.ObservableRecyclerView android:layout_width="match_parent" android:layout_height="match_parent"/> </jp.satorufujiwara.scrolling.MaterialScrollingLayout>第二步,代码里注册行为,一行一个效果:
materialScrollingLayout.addBehavior(bgImageView, new ParallaxBehavior()); materialScrollingLayout.addBehavior(titleTextView, new ScrollingBehavior());内置行为都在 library/src/main/java/jp/satorufujiwara/scrolling/behavior/ 下,自定义只需继承 Behavior.java 重写onScrolled,范例参考 sample 的 FabBehavior 与 TitleBehavior。
依赖(jcenter):jp.satorufujiwara:material-scrolling:1.2.1+com.github.ksoichiro:android-observablescrollview:1.6.0。
总结:这张"桥"值得抄的设计
- 归一化:把"列表内部滚动"统一换算成"页面滚动量",一套坐标打天下;
- 可插拔:回调、开关、缓存三个正交机制,让单页布局和多页 ViewPager 共用同一个 holder;
- 广播式:一个 Dispatcher 一对多分发,Behavior 之间零耦合。
想动手验证,先 clone 仓库https://gitcode.com/gh_mirrors/ma/material-scrolling,跑 sample 工程,对照本文的 7 个设计点逐行读,你会发现不到 100 行的RecyclerViewHolder才是整套 Material 滚动魔法的引擎。
【免费下载链接】material-scrollingAndroid library for material scrolling techniques.项目地址: https://gitcode.com/gh_mirrors/ma/material-scrolling
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
