它做什么
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:
| left | top | right | bottom | |
|---|---|---|---|---|
| child 真实 layout | 54 | 0 | 306 | 33 |
| 委托 Rect | 42 | -12 | 318 | 45 |
| 是谁 | 干什么 | |
|---|---|---|
| child | 被点的那个 View | click 仍设在它身上 |
| Rect | getHitRect 后再外扩的拷贝 | 决定热区有多大 |
| parent | child.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 会把坐标改到 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、或同一层要挂多个互不覆盖的热区时,换容器更直接。
评论