自定义 View 真正难的地方,很少是 onDraw

难的是两件事:把测量和布局算对,以及和父布局协商谁来消费滚动。前者决定你的容器在各种尺寸下会不会崩,后者决定它能不能和 AppBar、BottomSheet、下拉刷新这些系统组件配合。

这篇把 12 个原本散落的实战问题串成一条线:先立契约(LayoutParams),再讲三类自定义容器(换行、瀑布流、吸顶),然后是这套体系里最关键的滚动协议,最后是依赖它的协作组件(CoordinatorLayout、折叠标题栏、PopupWindow)。

每一节都按同一个结构写:机制是什么 → 最容易踩的三个坑 → 什么时候不该用它


一、先立契约:LayoutParams

LayoutParams 是子 View 交给父布局的布局契约——宽高、margin,以及具体父类型的附加字段都在里面。LinearLayout.LayoutParams 有 weight,RelativeLayout 有规则,ConstraintLayout 有约束。XML 里写的属性,最终都会变成对应类型的 LayoutParams。

理解它,才能正确做动画占位和运行时改布局。

三个容易踩的坑

  • 类型必须等于直接父布局。往 LinearLayout 里塞 RelativeLayout 的 params,会崩溃或被静默忽略。
  • MATCH_PARENTWRAP_CONTENT 是特殊常量。测量时父布局按 Mode 解释,不是 "0" 和 "-2" 的字面意思。
  • 改 params 要通知布局。只改字段不 set 回去,界面不会动;频繁改应合并成一次 requestLayout()

什么时候用:运行时增删控件、改宽高,或对接自定义 ViewGroup 时必须正确构造 LayoutParams。纯静态 XML 页面很少直接碰。遇到 ClassCastException,先看父布局类型,再看 inflate 时有没有带正确的 generateLayoutParams


二、换行类容器:一个算法,三种场景

这是最常被重复造轮子的一类。流式布局、标签换行、子 View 自动排列容器,底层是同一个算法:从左到右填充,行满换行,类似 CSS 的 flex-wrap

它比线性布局灵活,比网格布局更能吃进不等宽的孩子。

测量阶段:收集每个孩子的尺寸并模拟折行。要量两次——先让 child measure,再根据折行结果定自身高度;widthmatch_parent 时用剩余空间。

布局阶段:按模拟结果摆放。记录每行起点和行高,GONE 跳过,margin 加进占用宽度。

四个容易踩的坑

  • 单标签要限制最大宽度。超过父宽度减去 padding 就必须 ellipsize,否则一行被撑破。这是标签场景独有的必修项。
  • 行高用该行最高的孩子。混排图标和文字时,不能按第一个标签的高度一刀切。
  • 不要在 onLayout 里改 LayoutParams。那会触发新一轮请求,容易死循环;动态增删应该在数据层做。
  • 数据源保持线性。换行只是视觉,点击和增删仍按一维列表处理,不要按行列二维存。

几个具体场景的注意点

  • 搜索历史、商品标签、兴趣选择:短文本集合,先满足换行和间距,再加选中态。
  • 单标签过宽和 RTL 方向:这是自己写时最大的两个坑,容易被忽略。
  • 对齐是独立规则:左对齐、两端对齐和居中,需要在行内二次分配剩余空间。
  • GONE 和加权要定义清楚:流式布局一般不搞 weight,硬加会让折行行为难以理解。
  • 最后一行是否拉伸、divider 怎么处理,都要事先定好。

什么时候不该自己写:Google 的 FlexboxLayout 已经覆盖大多数需求。自己写主要用于裁剪依赖、教学,或要极致包体积与特殊手势。规则网格用 GridLayout,可滚动的大量标签用 RecyclerView + Flex 或自定义 LayoutManager。


三、瀑布流:把列高当成状态

瀑布流要把不同高度的卡片落到最短列上,形成参差布局。

核心是维护一个每列当前底部坐标的数组。onLayout 时把下一个 child 放到最短列,并更新该列高度。

三个容易踩的坑

  • 列高数组是状态,不是索引轮询。每放一个孩子就更新最短列;如果按 index % 列数 轮询,结果会退化成整齐网格,失去瀑布流的意义。
  • 自身高度取最长列wrap_content 时必须用各列最大值,漏算会导致底部被裁切。
  • 图片高度异步回来要重新 layout。占位高度一变就会打乱后面所有列,需要稳定的测量缓存,或主动通知父布局。

什么时候不该自己写:展示少量图文卡片、不需要复用时,可以自定义瀑布容器。信息流、无限加载必须StaggeredGridLayoutManager——条目一多,自定义 ViewGroup 的测量会在主线程爆炸。实现前先定列数和间距,再写测量,最后才接图片加载。


四、吸顶:画悬停层,不要搬 View

吸顶布局让某一组的标题在滚动时钉在顶部,直到下一组标题顶上来替换。通讯录、订单分组、商品类目都常用。

RecyclerView 可以用 ItemDecoration 绘制吸顶,也可以用 Sticky 第三方库,或 ConcatAdapter 把标题当独立 item 再配合自定义 LayoutManager。CoordinatorLayout 的 pin 是另一种吸顶,面向 AppBar。

三个容易踩的坑

  • 数据要带分组锚点。每个 item 得知道自己的组头位置,Decoration 才能算出该钉哪一个标题。
  • 绘制吸顶不要改真实 item 位置。用 canvas 画悬停层即可,把 View 从树上撕下来会引发一堆问题。
  • 点击要单独分发。悬停层接收点击跳转组头,不能点到下面正在滚的 item 上,否则会出现"点击穿透"。

什么时候用:长列表分组浏览、需要持续看到当前组名时做吸顶。短列表或单组用普通标题就够。先保证分组数据正确,再画悬停,最后加动画。


五、嵌套滚动:Android 滚动的底层协议

到这一节才进入正题。前面几个容器一旦要和外部滚动协作,就绕不开这套协议。

NestedScrollingParentNestedScrollingChild 是嵌套滚动协议的两端。子 View 在触摸时先问父要不要预消耗,自己滚完再把剩余量交给父。CoordinatorLayout、RecyclerView 和 NestedScrollView 都实现了这套接口。

自己做头部跟随、底部面板和自定义吸顶,往往要当 Parent 或 ChildNestedScrollingChild3 还区分触摸和非触摸滚动,惯性也能传递。

三个容易踩的坑

  • 顺序是 start → preScroll → scroll → preFling → fling。漏掉 start 或类型不匹配,父布局就收不到增量。
  • consumed 数组是双向账本。子要如实填写已消耗的像素,父才能决定 AppBar 收起多少。
  • 用 Helper 类能少写错NestedScrollingChildHelperNestedScrollingParentHelper 帮你维护窗口偏移和类型。

什么时候用:自定义可滚动容器要和系统 AppBar、BottomSheet 合作时,必须实现这对接口。普通业务页面用现成控件即可。调试时打印 type 和 dy,确认到底是谁把滚动吃掉了。


六、NestedScrollView:什么时候不该用

NestedScrollView 是支持嵌套滚动协议的 ScrollView,可以把未消耗的滚动量交给父布局——比如 CoordinatorLayout 里的 AppBar。

普通 ScrollView 在新版本也实现了嵌套接口,但老代码和部分场景仍要显式用 NestedScrollView。它适合中等长度的表单和详情,不适合超长列表。

三个容易踩的坑

  • 嵌套滚动靠接口,不是靠名字。父要是 NestedScrollingParent,子要 dispatch 嵌套滚动,否则 AppBar 不跟手。
  • 不要把无限列表塞进去。ScrollView 会让 RecyclerView 量出全部高度,回收直接失效,同时会报高度测量为 UNSPECIFIED 的性能问题。内部再嵌 RecyclerView 时,要明确谁来滚动,避免双重滚动条。
  • fillViewport 用于内容不足一屏。表单底部按钮贴底时用它,而不是写死屏幕高度。

什么时候用:详情页、设置页,以及要配合折叠头图的中等内容。Feed 流用 RecyclerView。出现滑动传递失败时,先查父布局是不是 Coordinator,再查有没有中间层拦截。


七、联动滚动:比例映射与防回环

联动滚动指两个可滚动区域共享进度——左侧目录跟右侧内容、头部 banner 跟列表、横向指示器跟 ViewPager。

实现可以监听一方的 scroll 再滚动另一方,也可以用 NestedScrolling 把增量交给父布局统一分发。

三个容易踩的坑

  • 用标志位防回环。A 滚动带动 B 之后,B 的回调不能再反过来驱动 A,否则会抖动或飞出边界。
  • 进度用比例而不是像素。两边内容高度不同时,应对齐百分比或锚点,而不是 1:1 平移。
  • 惯性与吸附分开处理。手指抬起后要对齐到 item,不能只在 onScrollsetScrollY

什么时候用:双栏目录、沉浸头图和指示器同步时做联动滚动。单列表内部吸顶用 LayoutManager 更合适。先把锚点列表定义清楚,再写监听,最后加动画。


八、CoordinatorLayout:Behavior 决定一切

CoordinatorLayout 是 Material 里的超级 FrameLayout,用 Behavior 描述子 View 之间如何响应滚动和依赖。AppBar、FAB、Snackbar 和 BottomSheet 都靠它协作。

它不是普通的垂直线性容器——孩子默认叠放,位置和行为由 Behavior 和锚点决定。

三个容易踩的坑

  • Behavior 是核心 API。自己写时实现 layoutDependsOnonDependentViewChanged,不要靠 margin 硬凑位置。
  • 只保留一个嵌套滚动父。内容滚动应交给 Coordinator,孩子用 NestedScrollingChild 把增量交上去。
  • 锚点用于 FAB 这类悬浮件layout_anchorgravity 决定它跟谁走;换依赖时要同步改 XML。

什么时候用:需要 AppBar、FAB、BottomSheet 协作时用 CoordinatorLayout,普通页面用 ConstraintLayout。滥用多层 Coordinator 会让事件分发很难查——出问题时先看哪个 Behavior 吃掉了滚动,再用 Layout Inspector 看依赖链。


九、折叠标题栏:scrollFlags 要配对

折叠标题栏是 AppBarLayout + CollapsingToolbarLayout 的经典组合:大图或大标题随滚动收缩成 Toolbar。

内容区用 NestedScrollView 或 RecyclerView,通过 CoordinatorLayout 把滚动传给 AppBar。scrollFlags 决定折叠、进场和吸附行为。

它解决的是视觉层次,不是性能问题——重内容仍然要靠列表回收。

三个容易踩的坑

  • scrollFlags 要配对exitUntilCollapsed 适合图片头,snap 会在中途吸附;乱加 flag 会抖。
  • 内容必须是嵌套滚动子。普通 ScrollView 传不动,要用 NestedScrollView 或 RecyclerView。
  • 状态栏和 Toolbar 高度要算进折叠距离fitsSystemWindows 和 margin 配错,会出现露缝或遮挡。

什么时候用:个人主页、文章头图、商品大图详情适合折叠标题栏,普通列表页不必。先把 Coordinator 结构搭对,再调动画和颜色。


十、PopupWindow:附着窗口的生命周期

PopupWindow 是挂在窗口上的浮动面板,用来做下拉菜单、气泡提示和自定义弹出层。

不走 Dialog 的窗口主题,位置相对锚点 View 计算。showAsDropDownshowAtLocation 是两条入口。软键盘、屏幕旋转和锚点被回收,是最常见的崩溃点。

较新的 API 推荐 PopupWindow 配合过渡动画,或改用 PopupMenuListPopupWindow 和 Material 的 Popup。

三个容易踩的坑

  • 宽高不要 wrap 到测量前。内容还没 layout 时 getWidth 为 0,位置会飘——先 measure 再 show。
  • 锚点的生命周期要长于弹窗。Activity 销毁后还 show 会 WindowLeaked,要在 onDestroy 里 dismiss。
  • 焦点与输入法。可输入的弹层要 setFocusable(true),否则 EditText 弹不出键盘。自己写时还要处理外部点击消失和 elevation。

什么时候用:需要贴着某个按钮弹出轻量菜单时用 PopupWindow;模态阻断操作用 Dialog。能用系统 PopupMenu 就不要从零写,除非要完全自定义布局和动画。


一张选型表

需求 用什么 不要用什么
运行时改宽高 / 增删控件 正确构造 LayoutParams 直接改 getWidth
不等宽孩子自动换行 FlexboxLayout 或自定义流式容器 硬塞 LinearLayout
少量图文卡片参差排列 自定义瀑布容器
信息流、无限加载 StaggeredGridLayoutManager 自定义瀑布 ViewGroup
长列表分组浏览 ItemDecoration 画吸顶 把 View 从树上摘下来
详情页 / 设置页(中等内容) NestedScrollView + fillViewport 把 RecyclerView 塞进 ScrollView
AppBar / FAB / BottomSheet 协作 CoordinatorLayout + Behavior 多层 Coordinator 嵌套
大图头收缩成 Toolbar CollapsingToolbarLayout + scrollFlags 手写高度动画
自定义容器要接系统滚动 实现 NestedScrollingParent/Child 只监听 onScroll
贴着按钮弹轻量菜单 PopupMenu / PopupWindow 从零写弹窗
模态阻断操作 Dialog PopupWindow

本文由 12 篇实战笔记合并整理:LayoutParams、流式布局、标签换行、子 View 自动排列、瀑布流容器、吸顶布局、NestedScrolling 协议、NestedScrollView、联动滚动、CoordinatorLayout、折叠标题栏、PopupWindow。