它做什么

TouchDelegate 让祖先把一块矩形上的触摸转发给某个子 View。视觉大小不变,变的是命中测试。官方叫被转发的那个 View 为 delegate。

量的是 child 的 layout 矩形,扩的是这份拷贝,挂的是 parent。parent 只是场地,不是热区本身。

怎么扩大

必须等 layout 完成再量,所以丢进 parent.post。见 View.post(Runnable)

View parent = (View) child.getParent();
parent.post(() -> {
    Rect area = new Rect();
    child.getHitRect(area);          // 原来的区域:相对 parent 的 mLeft/mTop/mRight/mBottom
    area.inset(-extra, -extra);      // 只改拷贝,四边各扩 extra
    parent.setTouchDelegate(new TouchDelegate(area, child));
});

new Rect() 是空壳。原尺寸来自 getHitRect:无变换时就是 outRect.set(mLeft, mTop, mRight, mBottom)。后面的 inset 不改 child 的宽高。

假设 child 相对 parent 是 [54, 0, 306, 33]extra = 12

lefttoprightbottom
child 真实 layout54030633
委托 Rect42-1231845
热区基于 child 扩大,挂在 parent 上
是谁干什么
child被点的那个 Viewclick 仍设在它身上
RectgetHitRect 后再外扩的拷贝决定热区有多大
parentchild.getParent()拿着 Rect 做命中,命中后转给 child

传入「扩大点击」的那个 View 是 delegate,不是挂载点。挂载永远在 getParent()。有效热区是 扩大后的 Rect ∩ parent.bounds:parent 再大,点在 Rect 外也不转。top = -12 而 parent 高只有 33 时,上边扩出去的部分点不到。

为什么挂在 parent 上

child 看不见自己外面的事件。分发按真实 bounds 找人:点在 child 里,走正常 child 分发,TouchDelegate 不参与;点在外面那圈,child 不是 touch target,事件落到 parent 的 onTouchEvent,这里才问 mTouchDelegate

设在 child 自己身上没用:空白本来就不属于它,onTouchEvent 到不了。

TouchDelegate 触摸转发路径

命中后 TouchDelegate 会把坐标改到 child 中心再 dispatchTouchEvent,所以 pressed / click / ripple 还能成立。滑出 slop 后坐标改到 child 外,click 通常取消。parent 的 OnTouchListener 若返回 true,事件到不了 onTouchEvent,热区失效。见 setOnClickListener和setOnTouchListener

调试与限制

委托 Rect 不进 onDraw。「显示布局边界」、Layout Inspector、uiautomator dump 画的都是 layout 框。要看热区,把同一块 Rect 画到 parent 的 ViewOverlay 上,或 API 29+ 用 getTouchDelegateInfo()

  • 热区不能超出 parent。clipChildren="false" 只管绘制。
  • 一个 parent 只能挂一个 TouchDelegate,后写覆盖先写。多个 child 得自己做列表转发,或拆 parent。
  • 和可点击 sibling 重叠时,sibling 先吃事件。
  • layout 变了 Rect 就过期,要再 post 一次。

适合视觉必须小、手要容易点到的图标。热区必须超出当前 parent、或同一层要挂多个互不覆盖的热区时,换容器更直接。


相关笔记