一、事件分发三大核心方法
// 1. 分发事件(所有View都有)
public boolean dispatchTouchEvent(MotionEvent ev) {
// 处理事件分发逻辑
return super.dispatchTouchEvent(ev);
}
// 2. 拦截事件(仅ViewGroup有)
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 决定是否拦截事件(默认不拦截)
return super.onInterceptTouchEvent(ev);
}
// 3. 处理事件(所有View都有)
public boolean onTouchEvent(MotionEvent ev) {
// 处理点击、滑动等逻辑
return super.onTouchEvent(ev);
}
二、事件传递流程图解
三、关键执行逻辑
ACTION_DOWN 阶段
- 必须返回 true 才能接收后续事件
- 父 View 拦截后,子 View 不再接收任何事件
// 父View拦截示例
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return false; // 允许子View处理
}
return true; // 拦截后续事件
}
事件处理优先级
OnTouchListener
>onTouchEvent
>OnClickListener
view.setOnTouchListener((v, event) -> {
// 优先级最高
return false; // 返回false才会继续执行onTouchEvent
});
滑动冲突解决对比
方法 | 实现位置 | 适用场景 |
---|---|---|
外部拦截法 | 父 View 的 onInterceptTouchEvent | 父 View 主动控制拦截逻辑 |
内部拦截法 | 子 View 的 dispatchTouchEvent | 子 View 主动请求不拦截 |
四、典型场景解析
场景 1:ViewPager + ListView 滑动冲突
// 外部拦截法实现
public class CustomViewPager extends ViewPager {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (isNeedIntercept(ev)) {
return true;
}
return super.onInterceptTouchEvent(ev);
}
}
场景 2:侧滑菜单与内容区冲突
// 内部拦截法实现
public class ContentView extends LinearLayout {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
getParent().requestDisallowInterceptTouchEvent(true);
return super.dispatchTouchEvent(ev);
}
}
五、高频面试问题
事件分发的三个阶段是什么?
- 事件传递(dispatchTouchEvent)
- 事件拦截(onInterceptTouchEvent)
- 事件处理(onTouchEvent)
为什么有时候点击事件会穿透 View?
- 当前 View 未消费事件(返回 false)
- 子 View 不可点击且未覆盖父 View
如何处理长按事件?
GestureDetector gestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
@Override
public void onLongPress(MotionEvent e) {
// 处理长按逻辑
}
});
@Override
public boolean onTouchEvent(MotionEvent event) {
return gestureDetector.onTouchEvent(event) || super.onTouchEvent(event);
}
扩展内容:
ACTION_CANCEL
事件的触发机制和处理方式,以下是详细解析:
一、触发场景
ACTION_CANCEL
通常在以下两种场景下触发:
1. 父 View 拦截后续事件
当子 View 正在处理事件时,父 View 突然决定拦截后续事件(如ACTION_MOVE
或ACTION_UP
),此时子 View 会收到ACTION_CANCEL
:
// 父ViewGroup拦截事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return false; // 允许子View处理
}
return true; // 拦截后续事件,触发子View的ACTION_CANCEL
}
2. 事件目标发生变化
当事件目标从一个 View 转移到另一个 View 时(如焦点变化或 View 被移除),原目标 View 会收到ACTION_CANCEL
:
// 切换焦点时触发
focusableView.requestFocus();
二、核心执行逻辑
父 View 发送 CANCEL
// ViewGroup.java中的dispatchTransformedTouchEvent方法
if (canceled || oldAction == MotionEvent.ACTION_CANCEL) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.dispatchTouchEvent(ev);
}
子 View 处理 CANCEL
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_CANCEL:
// 清理长按状态/动画等
isLongPressed = false;
invalidate();
break;
}
return true;
}
三、典型应用场景
场景 1:ListView 滚动时 Item 点击取消
// ListView的onInterceptTouchEvent
if (isInScrollingState()) {
// 滚动时拦截事件,触发子Item的ACTION_CANCEL
ev.setAction(MotionEvent.ACTION_CANCEL);
child.dispatchTouchEvent(ev);
return true;
}
场景 2:悬浮菜单的取消
// 自定义ViewGroup处理拖拽
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (isDragging) {
// 拖拽时触发子菜单的ACTION_CANCEL
menuView.dispatchTouchEvent(MotionEvent.obtain(ev, MotionEvent.ACTION_CANCEL));
}
return super.onInterceptTouchEvent(ev);
}
四、关键处理原则
必须消费 CANCEL 事件
- 即使不做处理,也应返回
true
:
- 即使不做处理,也应返回
case MotionEvent.ACTION_CANCEL:
return true; // 避免事件泄露
清理相关状态
- 如长按标记、触摸反馈等:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_CANCEL:
isPressed = false;
invalidate();
break;
}
return true;
}
总结列表:
触发条件 | 典型场景 | 处理要点 |
---|---|---|
父 View 拦截后续事件 | ListView 滚动时 Item 点击 | 清理状态并返回 true |
目标 View 变化 | 焦点切换或 View 移除 | 终止当前事件序列 |
自定义 View 的工作原理
自定义 View 是 Android 开发中非常重要的一部分,它允许开发者创建独特的 UI 组件。其工作原理主要涉及到以下几个关键步骤和机制:
1. 继承与重写
在 Android 里,自定义 View 通常是通过继承自 View
类或者它的子类(像 TextView
、ImageView
等)来实现。继承之后,需要重写一些关键的方法,以此实现自定义的绘制和交互逻辑。
2. 测量(onMeasure
方法)
系统在绘制 View 之前,会先调用 onMeasure
方法来确定 View 的大小。在这个方法里,你需要根据 View 的测量规格(MeasureSpec
)来计算出 View 的宽度和高度,并且调用 setMeasuredDimension
方法来设置测量结果。
3. 布局(onLayout
方法)
当 ViewGroup 对其子 View 进行布局时,会调用 onLayout
方法。在这个方法里,你要确定 View 在父容器中的位置。对于自定义 ViewGroup,还需要对其子 View 进行布局。
4. 绘制(onDraw
方法)
onDraw
方法是用来绘制 View 的外观的。在这个方法里,你可以使用 Canvas
和 Paint
等类来绘制各种图形、文本和图像。
5. 事件处理
自定义 View 可以通过重写 onTouchEvent
方法来处理触摸事件,从而实现交互效果。
具体实现:
场景一:自定义进度条
在很多应用中,我们会看到一些独特样式的进度条,接下来就实现一个简单的圆形进度条。
实现思路
- 继承
View
类。 - 重写
onDraw
方法,在该方法里使用Canvas
和Paint
来绘制圆形进度条。 - 提供更新进度的方法。
示例代码
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
public class CustomProgressBar extends View {
private Paint backgroundPaint;
private Paint progressPaint;
private int progress = 0;
private RectF rectF;
public CustomProgressBar(Context context) {
super(context);
init();
}
public CustomProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CustomProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
backgroundPaint = new Paint();
backgroundPaint.setColor(Color.GRAY);
backgroundPaint.setStyle(Paint.Style.STROKE);
backgroundPaint.setStrokeWidth(20);
backgroundPaint.setAntiAlias(true);
progressPaint = new Paint();
progressPaint.setColor(Color.BLUE);
progressPaint.setStyle(Paint.Style.STROKE);
progressPaint.setStrokeWidth(20);
progressPaint.setAntiAlias(true);
rectF = new RectF();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int centerX = getWidth() / 2;
int centerY = getHeight() / 2;
int radius = Math.min(centerX, centerY) - 20;
rectF.set(centerX - radius, centerY - radius, centerX + radius, centerY + radius);
// 绘制背景圆形
canvas.drawArc(rectF, 0, 360, false, backgroundPaint);
// 绘制进度弧形
float sweepAngle = (progress / 100.0f) * 360;
canvas.drawArc(rectF, -90, sweepAngle, false, progressPaint);
}
public void setProgress(int progress) {
if (progress < 0) {
progress = 0;
} else if (progress > 100) {
progress = 100;
}
this.progress = progress;
invalidate();
}
}
在布局文件中使用
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">
<com.example.customview.CustomProgressBar
android:id="@+id/customProgressBar"
android:layout_width="200dp"
android:layout_height="200dp" />
</LinearLayout>
在 Activity 中更新进度
import android.os.Bundle;
import android.os.Handler;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
private CustomProgressBar customProgressBar;
private int currentProgress = 0;
private Handler handler = new Handler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
customProgressBar = findViewById(R.id.customProgressBar);
// 模拟进度更新
new Thread(() -> {
while (currentProgress < 100) {
currentProgress++;
handler.post(() -> customProgressBar.setProgress(currentProgress));
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
handler.post(() -> Toast.makeText(MainActivity.this, "进度完成", Toast.LENGTH_SHORT).show());
}).start();
}
}
场景二:自定义星级评分控件
在电商、社交等应用里,星级评分控件是很常见的。下面来实现一个简单的星级评分控件。
实现思路
- 继承
View
类。 - 重写
onDraw
方法,绘制星星图标。 - 重写
onTouchEvent
方法,处理触摸事件,实现星级选择。
示例代码
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import androidx.core.content.ContextCompat;
public class CustomStarRating extends View {
private Drawable starFilled;
private Drawable starEmpty;
private int starCount = 5;
private int rating = 0;
private int starSize = 50;
private int starSpacing = 10;
public CustomStarRating(Context context) {
super(context);
init();
}
public CustomStarRating(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CustomStarRating(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
starFilled = ContextCompat.getDrawable(getContext(), android.R.drawable.star_big_on);
starEmpty = ContextCompat.getDrawable(getContext(), android.R.drawable.star_big_off);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < starCount; i++) {
int left = i * (starSize + starSpacing);
int top = 0;
int right = left + starSize;
int bottom = top + starSize;
Drawable drawable = (i < rating) ? starFilled : starEmpty;
drawable.setBounds(left, top, right, bottom);
drawable.draw(canvas);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
float x = event.getX();
int newRating = (int) (x / (starSize + starSpacing)) + 1;
if (newRating > starCount) {
newRating = starCount;
} else if (newRating < 0) {
newRating = 0;
}
rating = newRating;
invalidate();
return true;
}
return super.onTouchEvent(event);
}
public int getRating() {
return rating;
}
public void setRating(int rating) {
if (rating < 0) {
rating = 0;
} else if (rating > starCount) {
rating = starCount;
}
this.rating = rating;
invalidate();
}
}
在布局文件中使用
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">
<com.example.customview.CustomStarRating
android:id="@+id/customStarRating"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
在 Activity 中获取评分
import android.os.Bundle;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
private CustomStarRating customStarRating;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
customStarRating = findViewById(R.id.customStarRating);
// 设置初始评分
customStarRating.setRating(3);
// 获取评分
int rating = customStarRating.getRating();
Toast.makeText(this, "当前评分:" + rating, Toast.LENGTH_SHORT).show();
}
}
总结
在 Android 中,自定义 View 用于创建个性化 UI 组件,通过重写onMeasure
、onLayout
、onDraw
实现布局与绘制逻辑;事件分发机制通过dispatchTouchEvent
、onInterceptTouchEvent
、onTouchEvent
三级处理体系,确保触摸事件在 View 树中有序传递,两者结合可实现复杂交互与视觉效果的定制。
感谢观看!!!