Android 事件分发机制面试题(带答案版)
一、基础概念类
1. 什么是 Android 事件分发机制?
答案: Android 事件分发机制是指触摸事件(MotionEvent)从屏幕产生后,在 View 层级中传递和处理的整个过程。它包括:
- 事件产生:用户触摸屏幕产生 MotionEvent
- 事件传递:从 Activity → Window → ViewGroup → View
- 事件分发:决定哪个 View 处理事件
- 事件处理:View 响应事件(如点击、滑动等)
事件类型:
MotionEvent.ACTION_DOWN // 手指按下
MotionEvent.ACTION_MOVE // 手指移动
MotionEvent.ACTION_UP // 手指抬起
MotionEvent.ACTION_CANCEL // 事件取消
MotionEvent.ACTION_POINTER_DOWN // 多点触摸按下
MotionEvent.ACTION_POINTER_UP // 多点触摸抬起2. 事件分发的三个核心方法?
答案:
dispatchTouchEvent():事件分发
- 决定事件是否继续传递
- 返回 true:事件被消费,停止传递
- 返回 false:事件未被消费,继续向上传递
onInterceptTouchEvent():事件拦截(ViewGroup 特有)
- 决定是否拦截事件,不传递给子 View
- 返回 true:拦截事件,交给自己的 onTouchEvent() 处理
- 返回 false:不拦截,继续传递给子 View
onTouchEvent():事件处理
- 实际处理触摸事件
- 返回 true:事件被消费,不再传递
- 返回 false:事件未消费,向上传递给父 View
方法调用顺序:
Activity.dispatchTouchEvent()
↓
PhoneWindow.superDispatchTouchEvent()
↓
DecorView.dispatchTouchEvent()
↓
ViewGroup.dispatchTouchEvent()
↓
ViewGroup.onInterceptTouchEvent()
↓
View.dispatchTouchEvent()
↓
View.onTouchEvent()3. 事件分发的流程是怎样的?
答案:DOWN 事件流程:
Activity.dispatchTouchEvent()
↓
PhoneWindow.superDispatchTouchEvent()
↓
DecorView(最外层ViewGroup).dispatchTouchEvent()
↓
ViewGroupA.dispatchTouchEvent()
↓
ViewGroupA.onInterceptTouchEvent() // 询问是否拦截
↓ 不拦截,继续向下
ViewGroupB.dispatchTouchEvent()
↓
ViewGroupB.onInterceptTouchEvent()
↓ 不拦截,继续向下
View.dispatchTouchEvent()
↓
View.onTouchEvent()如果所有 View 都不消费 DOWN 事件,事件会反向回溯:
View.onTouchEvent()返回false
↑
ViewGroupB.onTouchEvent()返回false
↑
ViewGroupA.onTouchEvent()返回false
↑
Activity.onTouchEvent()MOVE 和 UP 事件:会沿着 DOWN 事件确定的路径传递,不会重新寻找接收者。
二、核心原理类
4. ViewGroup 的 dispatchTouchEvent() 源码关键逻辑?
答案:关键源码逻辑:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 1. 检查是否需要拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
// 判断是否允许拦截(子View可通过requestDisallowInterceptTouchEvent设置)
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
// 不是DOWN事件且没有找到接收者,直接拦截
intercepted = true;
}
// 2. 如果不拦截,寻找子View处理
if (!intercepted) {
// 遍历所有子View
for (int i = childrenCount - 1; i >= 0; i--) {
final View child = getChildAt(i);
// 判断子View是否能接收事件(可见、在触摸区域内)
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
// 分发事件给子View
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 子View消费了事件,设置mFirstTouchTarget
mFirstTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
// 3. 如果没有子View消费,自己处理
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
// 4. 有子View消费,继续传递
else {
// 将事件传递给mFirstTouchTarget
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
target = next;
}
}
return handled;
}5. mFirstTouchTarget 的作用?
答案:mFirstTouchTarget 是一个链表结构,用于记录消费了 DOWN 事件的子 View。
作用:
- 标识事件接收者:记录哪个子 View 消费了 DOWN 事件
- 确定事件传递路径:后续的 MOVE、UP 事件都传递给这个目标
- 决定是否拦截:当
mFirstTouchTarget != null时,才会询问是否拦截
重要规则:
- 只有消费了 ACTION_DOWN 事件的 View 才会被记录到
mFirstTouchTarget - 如果所有 View 都不消费 DOWN 事件,
mFirstTouchTarget = null - 当
mFirstTouchTarget = null时,ViewGroup 会直接拦截后续所有事件
6. requestDisallowInterceptTouchEvent() 的原理?
答案:作用:子 View 请求父 View 不要拦截事件。
原理:
// 子View调用
parent.requestDisallowInterceptTouchEvent(true);
// ViewGroup中的实现
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept != ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT; // 设置标志位
// 递归向上传递
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
}在 dispatchTouchEvent 中的使用:
// 检查是否允许拦截
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev); // 只有标志为false时才询问拦截
} else {
intercepted = false; // 不允许拦截
}重要限制:
- 只对 ACTION_DOWN 之后的事件有效:在 ACTION_DOWN 时标志位会被重置
- 需要子 View 消费了 DOWN 事件:否则父 View 直接拦截,不会询问
典型应用:HorizontalScrollView 嵌套 ListView,在横向滑动时不让父 View 拦截。
三、拦截机制类
7. onInterceptTouchEvent() 什么时候调用?
答案:调用时机(在 ViewGroup.dispatchTouchEvent() 中):
- ACTION_DOWN 事件:一定会调用
- 后续事件:只有
mFirstTouchTarget != null时才会调用 - 当子 View 调用 requestDisallowInterceptTouchEvent(true) 时,可能不调用
源码逻辑:
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
// 这里才会调用 onInterceptTouchEvent
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
}
} else {
// 没有子View消费DOWN事件,直接拦截
intercepted = true;
}8. 如何正确使用 onInterceptTouchEvent()?
答案:正确使用模式:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// 对于DOWN事件,通常返回false
// 因为需要让子View有机会处理点击
intercepted = false;
// 重置状态
mIsScrolling = false;
break;
case MotionEvent.ACTION_MOVE:
// 判断是否满足拦截条件
if (shouldInterceptMoveEvent(ev)) {
intercepted = true;
mIsScrolling = true;
}
break;
case MotionEvent.ACTION_UP:
// 对于UP事件,如果之前没有拦截,这里也不应该拦截
// 否则子View收不到UP事件,无法触发点击
intercepted = mIsScrolling;
break;
}
return intercepted;
}重要原则:
- ACTION_DOWN 一般返回 false:让子 View 有机会处理
- ACTION_MOVE 根据业务判断:如滑动距离超过阈值
- 一旦拦截,后续事件都交给自己的 onTouchEvent()
- 拦截后要消费所有事件,否则事件会丢失
9. 拦截后还能取消拦截吗?
答案:不能直接取消拦截,但可以通过以下方式实现类似效果:
方法1:重置拦截状态(在 DOWN 事件时)
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
// 每次DOWN事件都重置拦截状态
mHasIntercepted = false;
return false;
}
if (!mHasIntercepted && shouldIntercept(ev)) {
mHasIntercepted = true;
return true;
}
return mHasIntercepted;
}方法2:通过 requestDisallowInterceptTouchEvent() 子 View 可以在 MOVE 事件中请求父 View 不拦截:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
// 判断是否需要请求父View不拦截
if (shouldRequestParentNotIntercept(event)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// 恢复
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
return true;
}四、事件处理类
10. onTouchEvent() 和 OnTouchListener 的优先级?
答案:优先级顺序:
- OnTouchListener.onTouch()
- View.onTouchEvent()
源码分析(View.dispatchTouchEvent()):
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
// 1. 先调用 OnTouchListener
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true; // OnTouchListener 消费了事件
}
// 2. 如果 OnTouchListener 没有消费,调用 onTouchEvent
if (!result && onTouchEvent(event)) {
result = true; // onTouchEvent 消费了事件
}
return result;
}重要规则:
- 如果
OnTouchListener.onTouch()返回 true,onTouchEvent()不会被调用 - 如果
OnTouchListener.onTouch()返回 false,会继续调用onTouchEvent() - 可以通过设置
OnTouchListener来拦截事件
11. onClick() 是在哪里触发的?
答案:onClick() 是在 onTouchEvent() 的 ACTION_UP 事件中触发的。
源码分析(View.onTouchEvent()):
public boolean onTouchEvent(MotionEvent event) {
switch (action) {
case MotionEvent.ACTION_DOWN:
// 记录按下时间、位置等
break;
case MotionEvent.ACTION_MOVE:
// 判断是否滑动出有效区域
if (!pointInView(x, y, touchSlop)) {
// 移除点击状态
removeTapCallback();
}
break;
case MotionEvent.ACTION_UP:
// 检查是否是点击
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 触发点击
performClick();
}
break;
}
return true;
}performClick() 源码:
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this); // 调用 onClick
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}点击条件:
- DOWN 和 UP 事件都在 View 的区域内
- 没有触发长按
- 没有滑动出有效区域(考虑 touchSlop)
- 设置了 OnClickListener
12. 长按事件是如何实现的?
答案: 长按事件通过 postDelayed() 延迟执行实现。
源码分析:
// 在 ACTION_DOWN 中
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
// 检查是否支持长按
if (!clickable) {
checkForLongClick(0, x, y);
break;
}
// 延迟触发长按检查
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout());
break;
// 长按检查 Runnable
private final class CheckForLongPress implements Runnable {
public void run() {
if (performLongClick(x, y)) {
mHasPerformedLongPress = true;
}
}
}
// 在 ACTION_UP 中取消长按
case MotionEvent.ACTION_UP:
removeLongPressCallback(); // 移除长按检查
break;长按触发条件:
- 手指按下超过指定时间(默认 500ms)
- 手指没有移动出有效区域
- 没有提前抬起手指
长按与点击的冲突处理:
- 如果触发了长按,
mHasPerformedLongPress = true - 在 ACTION_UP 时,如果
mHasPerformedLongPress = true,不会触发点击
五、嵌套滑动类
13. 什么是嵌套滑动?如何实现?
答案:嵌套滑动:父 View 和子 View 都可以滑动,协同处理滑动事件。
实现方式:使用 NestedScrolling 相关接口(Android 5.0+)
核心接口:
- NestedScrollingChild:嵌套滑动的子 View(如 RecyclerView)
- NestedScrollingParent:嵌套滑动的父 View(如 CoordinatorLayout)
- NestedScrollingChild2 和 NestedScrollingParent2:改进版本
工作流程:
1. 子View开始滑动
2. 子View询问父View是否要消费滑动距离
3. 父View消费部分滑动距离
4. 子View消费剩余滑动距离
5. 滑动结束,通知父View代码示例(父 View):
public class NestedParentLayout extends ViewGroup implements NestedScrollingParent2 {
@Override
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
// 决定是否参与嵌套滑动
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScrollAccepted(View child, View target, int axes, int type) {
// 嵌套滑动被接受
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed, int type) {
// 子View滑动前,父View先消费部分距离
if (dy > 0 && getScrollY() < mMaxScrollY) {
int consumeY = Math.min(dy, mMaxScrollY - getScrollY());
scrollBy(0, consumeY);
consumed[1] = consumeY; // 告诉子View已消费的距离
}
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int type) {
// 子View滑动后,父View消费剩余距离
if (dyUnconsumed < 0) {
int consumeY = Math.max(dyUnconsumed, -getScrollY());
scrollBy(0, consumeY);
}
}
@Override
public void onStopNestedScroll(View target, int type) {
// 嵌套滑动结束
}
}14. 传统滑动冲突 vs 嵌套滑动?
答案:传统滑动冲突解决方案:
- 外部拦截法:父 View 的
onInterceptTouchEvent()中判断 - 内部拦截法:子 View 的
dispatchTouchEvent()中配合requestDisallowInterceptTouchEvent()
问题:
- 逻辑复杂,容易出错
- 只能一个 View 消费事件
- 滑动不连贯
嵌套滑动的优势:
- 协同工作:父 View 和子 View 可以同时消费滑动距离
- 滑动连贯:用户体验更好
- 解耦:父 View 和子 View 独立实现
- 标准化:Android 官方支持
使用建议:
- Android 5.0+:使用嵌套滑动
- 低版本:使用传统解决方案
- 复杂场景:CoordinatorLayout + Behavior
六、滑动冲突类
15. 常见的滑动冲突场景?
答案:场景1:外部滑动方向与内部滑动方向不一致
- 例如:ViewPager 嵌套 ListView(横向 vs 纵向)
- 解决方案:根据滑动角度判断方向
场景2:外部滑动方向与内部滑动方向一致
- 例如:ScrollView 嵌套 ListView(都是纵向)
- 解决方案:根据业务逻辑分配滑动距离
场景3:多层嵌套滑动
- 例如:DrawerLayout + ViewPager + RecyclerView
- 解决方案:使用嵌套滑动或事件分发协调
16. 如何解决 ViewPager 嵌套 ListView 的滑动冲突?
答案:方案1:外部拦截法(推荐)
public class CustomViewPager extends ViewPager {
private float mLastX;
private float mLastY;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
float x = ev.getX();
float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// 不拦截DOWN,让子View有机会处理点击
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
float deltaX = x - mLastX;
float deltaY = y - mLastY;
// 判断滑动方向
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// 横向滑动,拦截
intercepted = true;
} else {
// 纵向滑动,不拦截
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
mLastX = x;
mLastY = y;
return intercepted;
}
}方案2:内部拦截法
public class CustomListView extends ListView {
private float mLastX;
private float mLastY;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
float x = ev.getX();
float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// 请求父View不拦截DOWN事件
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
float deltaX = x - mLastX;
float deltaY = y - mLastY;
// 如果是纵向滑动,自己处理
// 如果是横向滑动,让父View处理
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// 横向滑动,允许父View拦截
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
}17. 如何解决 ScrollView 嵌套 ListView 的滑动冲突?
答案:问题:两者都是纵向滑动,ListView 无法完整滑动。
解决方案1:自定义 ListView(测量所有 Item)
public class NoScrollListView extends ListView {
public NoScrollListView(Context context) {
super(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 测量所有Item的高度
int expandSpec = MeasureSpec.makeMeasureSpec(
Integer.MAX_VALUE >> 2,
MeasureSpec.AT_MOST
);
super.onMeasure(widthMeasureSpec, expandSpec);
}
}解决方案2:禁用 ScrollView 的滑动
public class CustomScrollView extends ScrollView {
private boolean mEnableScroll = true;
public void setEnableScroll(boolean enable) {
mEnableScroll = enable;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (!mEnableScroll) {
return false; // 不拦截,交给子View处理
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (!mEnableScroll) {
return false; // 不处理触摸事件
}
return super.onTouchEvent(ev);
}
}解决方案3:使用 NestedScrollView(推荐,Android 5.0+)
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</androidx.core.widget.NestedScrollView>七、高级话题类
18. ACTION_CANCEL 什么时候触发?
答案:触发时机:
- 父 View 拦截事件:子 View 收到 ACTION_DOWN 后,父 View 拦截后续事件
- 触摸区域改变:手指移出 View 边界
- 系统事件:来电、弹出对话框等系统事件中断
源码触发:
// 在 ViewGroup.dispatchTouchEvent() 中
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 当父View拦截时,给子View发送CANCEL
if (mFirstTouchTarget != null) {
// 发送CANCEL给之前接收事件的子View
MotionEvent cancelEvent = MotionEvent.obtain(ev);
cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
mFirstTouchTarget.child.dispatchTouchEvent(cancelEvent);
cancelEvent.recycle();
}处理 ACTION_CANCEL:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_CANCEL:
// 重置状态,如取消高亮、停止动画等
resetState();
return true;
}
return false;
}19. 多点触控的事件分发?
答案:事件类型:
ACTION_POINTER_DOWN:非第一个手指按下ACTION_POINTER_UP:非最后一个手指抬起
获取手指信息:
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getActionMasked(); // 使用getActionMasked()
int pointerIndex = event.getActionIndex(); // 触发事件的手指索引
int pointerId = event.getPointerId(pointerIndex); // 手指ID
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
// 新手指按下
handlePointerDown(pointerId,
event.getX(pointerIndex),
event.getY(pointerIndex));
break;
case MotionEvent.ACTION_MOVE:
// 可能有多个手指移动
for (int i = 0; i < event.getPointerCount(); i++) {
int pid = event.getPointerId(i);
float x = event.getX(i);
float y = event.getY(i);
handlePointerMove(pid, x, y);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
// 手指抬起
handlePointerUp(pointerId);
break;
}
return true;
}追踪手指:使用 SparseArray 或 Map 存储每个手指的状态。
20. 如何实现双击事件?
答案:实现原理:通过记录两次点击的时间间隔和位置。
public class DoubleClickView extends View {
private long mFirstClickTime = 0;
private long mSecondClickTime = 0;
private float mFirstClickX, mFirstClickY;
private static final int DOUBLE_CLICK_INTERVAL = 300; // 双击间隔(ms)
private static final int DOUBLE_CLICK_DISTANCE = 20; // 双击最大距离(像素)
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
long currentTime = System.currentTimeMillis();
float currentX = event.getX();
float currentY = event.getY();
if (mFirstClickTime == 0) {
// 第一次点击
mFirstClickTime = currentTime;
mFirstClickX = currentX;
mFirstClickY = currentY;
} else {
// 第二次点击
mSecondClickTime = currentTime;
// 检查是否满足双击条件
if (mSecondClickTime - mFirstClickTime < DOUBLE_CLICK_INTERVAL
&& Math.abs(currentX - mFirstClickX) < DOUBLE_CLICK_DISTANCE
&& Math.abs(currentY - mFirstClickY) < DOUBLE_CLICK_DISTANCE) {
// 触发双击
performDoubleClick();
// 重置
resetClickState();
} else {
// 不是有效的双击,重新开始
mFirstClickTime = currentTime;
mFirstClickX = currentX;
mFirstClickY = currentY;
}
}
// 延迟重置(防止间隔时间过长)
postDelayed(() -> {
if (System.currentTimeMillis() - mFirstClickTime > DOUBLE_CLICK_INTERVAL) {
resetClickState();
}
}, DOUBLE_CLICK_INTERVAL);
}
return true;
}
private void resetClickState() {
mFirstClickTime = 0;
mSecondClickTime = 0;
}
private void performDoubleClick() {
// 触发双击事件
if (mOnDoubleClickListener != null) {
mOnDoubleClickListener.onDoubleClick(this);
}
}
}八、性能优化类
21. 事件分发中的性能优化?
答案:优化点1:减少 onInterceptTouchEvent() 的计算
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 快速判断,减少计算
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return false; // 快速返回
}
// 复杂计算放在MOVE事件中
if (ev.getAction() == MotionEvent.ACTION_MOVE) {
if (shouldIntercept(ev)) {
return true;
}
}
return false;
}优化点2:使用 hitRect 进行快速区域检测
// 在自定义ViewGroup中
private Rect mHitRect = new Rect();
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
float x = ev.getX();
float y = ev.getY();
// 快速判断是否在View范围内
getHitRect(mHitRect);
if (!mHitRect.contains((int)x, (int)y)) {
return false;
}
// 继续分发...
return super.dispatchTouchEvent(ev);
}优化点3:避免在事件分发中创建对象
// 错误做法:每次事件都创建新对象
@Override
public boolean onTouchEvent(MotionEvent event) {
PointF point = new PointF(event.getX(), event.getY()); // 创建对象
// ...
}
// 正确做法:复用对象
private PointF mTempPoint = new PointF();
@Override
public boolean onTouchEvent(MotionEvent event) {
mTempPoint.set(event.getX(), event.getY()); // 复用对象
// ...
}优化点4:使用 GestureDetector 处理复杂手势
private GestureDetector mGestureDetector;
public MyView(Context context) {
super(context);
mGestureDetector = new GestureDetector(context, new GestureListener());
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}
private class GestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
// 处理滑动
return true;
}
}九、实战场景类
22. 如何实现一个可拖拽的 View?
答案:
public class DraggableView extends View {
private float mLastX, mLastY;
private boolean mIsDragging = false;
public DraggableView(Context context) {
super(context);
init();
}
private void init() {
// 设置可点击,否则可能接收不到DOWN事件
setClickable(true);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = x;
mLastY = y;
mIsDragging = false;
break;
case MotionEvent.ACTION_MOVE:
float deltaX = x - mLastX;
float deltaY = y - mLastY;
// 判断是否开始拖拽(超过阈值)
if (!mIsDragging) {
float distance = (float) Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance > ViewConfiguration.get(getContext()).getScaledTouchSlop()) {
mIsDragging = true;
// 请求父View不拦截事件
getParent().requestDisallowInterceptTouchEvent(true);
}
}
// 执行拖拽
if (mIsDragging) {
// 更新位置
setTranslationX(getTranslationX() + deltaX);
setTranslationY(getTranslationY() + deltaY);
// 或者通过LayoutParams更新
// ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();
// params.leftMargin += deltaX;
// params.topMargin += deltaY;
// setLayoutParams(params);
}
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (mIsDragging) {
// 拖拽结束处理
handleDragEnd();
mIsDragging = false;
} else {
// 点击处理
performClick();
}
break;
}
return true;
}
@Override
public boolean performClick() {
super.performClick();
// 处理点击
return true;
}
}23. 如何实现一个侧滑删除控件?
答案:
public class SwipeDeleteLayout extends ViewGroup {
private View mContentView; // 内容View
private View mDeleteView; // 删除按钮View
private float mLastX, mLastY;
private boolean mIsSwiping = false;
private int mDeleteWidth;
private Scroller mScroller;
public SwipeDeleteLayout(Context context) {
super(context);
init();
}
private void init() {
mScroller = new Scroller(getContext());
setClickable(true);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
// 假设第一个子View是内容,第二个是删除按钮
mContentView = getChildAt(0);
mDeleteView = getChildAt(1);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 测量所有子View
measureChildren(widthMeasureSpec, heightMeasureSpec);
// 内容View的宽度就是整个View的宽度
int width = mContentView.getMeasuredWidth();
int height = mContentView.getMeasuredHeight();
setMeasuredDimension(width, height);
// 保存删除按钮的宽度
mDeleteWidth = mDeleteView.getMeasuredWidth();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 布局内容View(铺满)
mContentView.layout(0, 0, r - l, b - t);
// 布局删除按钮(在右侧外面)
mDeleteView.layout(r - l, 0, r - l + mDeleteWidth, b - t);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
float x = ev.getX();
float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = x;
mLastY = y;
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
float deltaX = Math.abs(x - mLastX);
float deltaY = Math.abs(y - mLastY);
// 判断是否是横向滑动
if (deltaX > ViewConfiguration.get(getContext()).getScaledTouchSlop()
&& deltaX > deltaY) {
intercepted = true; // 拦截事件
mIsSwiping = true;
}
break;
}
return intercepted;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
if (mIsSwiping) {
float deltaX = x - mLastX;
float scrollX = getScrollX() - deltaX;
// 限制滑动范围
scrollX = Math.max(0, Math.min(scrollX, mDeleteWidth));
scrollTo((int) scrollX, 0);
mLastX = x;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (mIsSwiping) {
// 根据滑动距离决定打开还是关闭
int scrollX = getScrollX();
if (scrollX > mDeleteWidth / 2) {
// 打开删除按钮
mScroller.startScroll(scrollX, 0,
mDeleteWidth - scrollX, 0, 300);
} else {
// 关闭删除按钮
mScroller.startScroll(scrollX, 0,
-scrollX, 0, 300);
}
invalidate();
mIsSwiping = false;
}
break;
}
return true;
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
// 关闭删除按钮
public void closeDelete() {
mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0, 300);
invalidate();
}
}十、总结
事件分发机制的核心要点:
- 理解三个核心方法:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent
- 掌握事件传递流程:从 Activity 到 View 的传递路径
- 理解拦截机制:onInterceptTouchEvent 的作用和调用时机
- 熟悉滑动冲突解决方案:外部拦截、内部拦截、嵌套滑动
- 掌握常见手势实现:点击、长按、双击、拖拽等
- 注意性能优化:减少对象创建、快速区域检测等
这些知识点涵盖了事件分发机制的各个方面,掌握后可以应对绝大多数面试问题。