Android 第四次面试总结(自定义 View 与事件分发深度解析)

发布于:2025-03-19 ⋅ 阅读:(15) ⋅ 点赞:(0)

一、事件分发三大核心方法

// 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);
}

二、事件传递流程图解

三、关键执行逻辑

  1. 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);
    }
}

五、高频面试问题

  1. 事件分发的三个阶段是什么?

    • 事件传递(dispatchTouchEvent)
    • 事件拦截(onInterceptTouchEvent)
    • 事件处理(onTouchEvent)
  2. 为什么有时候点击事件会穿透 View?

    • 当前 View 未消费事件(返回 false)
    • 子 View 不可点击且未覆盖父 View
  3. 如何处理长按事件?

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_MOVEACTION_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();

二、核心执行逻辑

  1. 父 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);
}

四、关键处理原则

  1. 必须消费 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 类或者它的子类(像 TextViewImageView 等)来实现。继承之后,需要重写一些关键的方法,以此实现自定义的绘制和交互逻辑。

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 组件,通过重写onMeasureonLayoutonDraw实现布局与绘制逻辑;事件分发机制通过dispatchTouchEventonInterceptTouchEventonTouchEvent三级处理体系,确保触摸事件在 View 树中有序传递,两者结合可实现复杂交互与视觉效果的定制。 

感谢观看!!!