一、Scrollview 嵌套 Button 当按下Button(出现按下效果), 此时滑动Scrollview,Button点击事件是否执行? 并说明事件处理流程?
结论: 当用户在 Button 上按下并滑动 ScrollView 时,Button 的点击事件不会执行 。
虽然按下瞬间会显示按下效果(pressed 状态),但一旦滑动距离超过系统阈值(TouchSlop),ScrollView 会拦截后续触摸事件,Button 收到 ACTION_CANCEL 并取消按下状态,最终 onClick 不会被触发。
事件处理流程详解
-
ACTION_DOWN
- 触摸点落在
Button区域。 ScrollView的dispatchTouchEvent()将事件分发给子 View:调用Button的dispatchTouchEvent()。Button处理ACTION_DOWN,返回true(表示消费事件),同时进入 pressed 状态(显示按下效果)。- 此时
ScrollView的onInterceptTouchEvent()尚未拦截,事件已被子 View 消费。
- 触摸点落在
-
ACTION_MOVE(用户开始滑动)
ScrollView先收到ACTION_MOVE,其onInterceptTouchEvent()被调用。- 检测到垂直滑动距离超过
ViewConfiguration.get(context).getScaledTouchSlop(),判定为滚动操作。 ScrollView返回true拦截事件,并通知父容器(如果存在)停止向子 View 派发后续事件。- 同时,系统会向
Button发送一个ACTION_CANCEL事件(通过Button的dispatchTouchEvent())。 Button收到ACTION_CANCEL后,清除 pressed 状态,不再等待ACTION_UP触发点击。
-
后续 ACTION_MOVE / ACTION_UP
- 事件不再传递给
Button,完全由ScrollView处理,执行滚动操作。 - 最终没有
ACTION_UP到达Button,onClick不会被调用。
- 事件不再传递给
特殊情况
- 如果滑动距离很小 (未超过
TouchSlop),ScrollView不会拦截,ACTION_UP最终会传递给Button,此时点击事件会正常执行。 - 若在
Button上按下后立即水平滑动 (且ScrollView为垂直滚动),垂直方向未超阈值,也可能不拦截,点击仍可能触发。
总结流程图
text
用户触摸 Button (DOWN)
↓
Button 消费 DOWN,显示按下效果
↓
用户开始滑动 (MOVE)
↓
ScrollView 检测滑动距离 > TouchSlop?
├── 是 → ScrollView 拦截事件
│ 向 Button 发送 CANCEL
│ Button 取消按下状态
│ ScrollView 处理滚动
│ → 点击事件不执行
└── 否 → 不拦截
MOVE/UP 继续给 Button
UP 时 Button 执行 onClick
二、 如何检测链表中是否存在环?
方法一:哈希表法(简单直观)
- 思路:遍历链表,把每个节点的地址(或引用)存入哈希表。如果当前节点已经在表中,说明有环。
- 复杂度:时间 O(n),空间 O(n)。
- 适用场景:不限制空间时,代码简单,容易理解。
方法二:快慢指针(Floyd 判圈算法,最优解)
- 思路 :使用两个指针
slow和fast,slow每次走一步,fast每次走两步。如果链表有环,它们最终会在环内相遇;如果fast走到null,则无环。 - 复杂度:时间 O(n),空间 O(1)。
python
def hasCycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
2. 如果存在环,如何找到环的入口点?
在快慢指针相遇后,再使用一个指针从头开始,另一个指针从相遇点开始,每次各走一步,它们再次相遇的地方就是环的入口。
python
def detectCycle(head):
slow = fast = head
# 1. 判断是否有环,并找到相遇点
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
# 2. 找入口
p1 = head
p2 = slow
while p1 != p2:
p1 = p1.next
p2 = p2.next
return p1
return None
原理 :设链表头到环入口距离为 a,环入口到相遇点距离为 b,相遇点到环入口距离为 c(即环长 L = b + c)。快慢指针相遇时,slow 走了 a+b,fast 走了 a+b + kL。因为 fast 速度是 slow 的两倍,所以 2(a+b) = a+b + kL → a+b = kL → a = kL - b = (k-1)L + c。因此从头和相遇点同步走,一定会在入口相遇。
3. 如何"排查" -- 实际调试步骤
如果你在实际编程中遇到了链表环导致的问题(例如死循环、输出异常),可以按以下步骤排查:
-
打印节点地址/唯一标识
遍历链表,打印每个节点的 id(Python 中用
id(node),Java 中用System.identityHashCode(node))。如果某个 id 重复出现,说明有环。 -
使用快慢指针检测
写一个简单的
hasCycle函数,验证是否有环。 -
找到环的入口
使用上面的
detectCycle定位到环开始的位置,然后检查那个节点为什么被错误地指回了前面的节点。 -
检查链表构造代码
回溯是哪个操作导致了环,常见原因:
- 尾节点的
next错误地指向了前面的节点。 - 合并两个链表时,没有处理好复制与引用的关系。
- 节点复用(对象池)导致意外形成环。
- 尾节点的
4. 扩展:求环的长度
在快慢指针相遇后,让 slow 不动,fast 继续每次走一步,再次相遇时走的步数就是环的长度。
总结
| 需求 | 方法 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| 判断是否有环 | 快慢指针 | O(n) | O(1) |
| 找到环的入口 | 快慢指针 + 同步指针 | O(n) | O(1) |
| 打印环上所有节点 | 从入口开始遍历一圈 | O(环长) | O(1) |
面试中推荐使用快慢指针,既能检测环,又能找到入口,且空间效率高。
三、View渲染流程:Measure → Layout → Draw
Android 的 View 渲染流程由 ViewRootImpl 驱动,通过 performTraversals() 方法依次执行 measure 、layout 、draw 三大操作。这个过程决定了每个 View 的大小、位置和最终显示的内容。
总览
- Measure(测量) :确定 View 的宽度和高度。
- Layout(布局) :确定 View 在父容器中的位置(四个顶点坐标)。
- Draw(绘制) :将 View 的内容绘制到屏幕上。
每个阶段都是递归的:父 View 会调用子 View 的对应方法,直到整棵树完成。
流程图
text
ViewRootImpl.performTraversals()
│
├── performMeasure()
│ │
│ └── DecorView.measure() → onMeasure()
│ └── (递归) 子 View.measure() → onMeasure()
│ └── setMeasuredDimension()
│
├── performLayout()
│ │
│ └── DecorView.layout() → onLayout()
│ └── (递归) 子 View.layout() → onLayout()
│ └── setFrame()
│
└── performDraw()
│
└── DecorView.draw() → drawBackground() → onDraw() → dispatchDraw() → onDrawForeground()
└── (递归) 子 View.draw()
四、View渲染相关常见面试问题
-
getMeasuredWidth()和getWidth()有什么区别?getMeasuredWidth()是测量阶段的结果,在onMeasure之后有效。getWidth()是布局阶段实际分配的宽度,在onLayout之后有效。两者一般相等,但在某些容器(如 ScrollView)中可能不同(测量尺寸可能大于实际屏幕,实际通过滚动显示)。
-
requestLayout()和invalidate()的区别?requestLayout()会向上递归标记父 View 需要重新布局,最终触发 measure + layout(以及可能的重绘)。invalidate()只标记视图内容已改变,触发重绘(draw),不会重新测量和布局。
-
自定义 View 为什么一定要重写
onMeasure()处理wrap_content?- 因为
View的默认onMeasure()对AT_MOST模式返回的是父容器允许的最大尺寸(等同于match_parent)。为了正确支持wrap_content,需要在onMeasure中计算内容实际需要的尺寸,并调用setMeasuredDimension()。
- 因为
-
dispatchDraw()的作用是什么?- 在
draw()流程中负责绘制子 View。自定义ViewGroup可以重写它来改变子 View 的绘制顺序或添加额外绘制。
- 在
-
硬件加速下
Canvas有何限制?- 某些操作不支持(如
drawPicture()、复杂的clipPath()等),或性能较差。可通过View.setLayerType()临时切换到软件渲染。
- 某些操作不支持(如