自定义 View 基础概念
1. View 与 ViewGroup 的区别
- View:UI 的最小单位,负责绘制和交互(如 TextView、Button)。
- ViewGroup:容器类,可包含多个 View(如 LinearLayout、RecyclerView)。
2. 自定义 View 的三种类型
类型 | 适用场景 | 实现复杂度 |
---|---|---|
继承 View | 简单图形绘制(如进度条、图表) | 高(需自绘) |
继承 ViewGroup | 自定义布局(如流式布局) | 中 |
组合 View | 组合已有控件(如带图标的按钮) | 低 |
public class MyView extends View {
// 必写构造方法(从XML加载或代码创建时调用)
public MyView(Context context) {
super(context);
}
// 从XML加载时调用(需声明自定义属性)
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
// 解析自定义属性
}
// 从XML加载且指定样式时调用
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
// API 21+ 支持样式主题时调用
public MyView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}
自定义 View 三大流程:测量、布局、绘制
1. 测量(onMeasure)
-
核心目标:确定 View 的宽高尺寸。
-
关键类:
MeasureSpec
(包含测量模式和尺寸),三种模式:UNSPECIFIED
:未指定尺寸(父容器不限制)。EXACTLY
:精确尺寸(如match_parent
或固定值)。AT_MOST
:最大尺寸(如wrap_content
)。
-
实现步骤:
- 调用
super.onMeasure()
获取父容器的测量规格。 - 根据
MeasureSpec
计算自定义 View 的宽高(需处理 padding 和子 View 测量)。 - 调用
setMeasuredDimension(width, height)
保存结果。
- 调用
2. 布局(onLayout)
- 适用场景:仅 ViewGroup 需要重写,确定子 View 的位置。
- 核心方法:
child.layout(left, top, right, bottom)
(设置子 View 的四个顶点坐标)。
3. 绘制(onDraw)
-
绘制流程:
- Canvas:绘制画布,提供
drawRect()
、drawCircle()
、drawText()
等方法。 - Paint:绘制画笔,设置颜色、粗细、抗锯齿等属性。
- Path:绘制复杂路径(如贝塞尔曲线、圆弧)。
- Canvas:绘制画布,提供
事件处理与交互
1. 事件分发机制
-
三层方法:
dispatchTouchEvent(MotionEvent)
:事件分发入口,决定事件是否传递给子 View。onInterceptTouchEvent(MotionEvent)
:ViewGroup 用于拦截事件(View 无此方法)。onTouchEvent(MotionEvent)
:处理点击、滑动等事件。
-
经典原则:事件先由父容器分发,子 View 可通过返回
true
消耗事件。
2. 触摸反馈与滑动
-
触摸反馈:通过
invalidate()
刷新视图实现按压效果。 -
滑动实现:
scrollTo/scrollBy
:滚动自身内容(不改变子 View 位置)。layout()
:重新布局子 View(改变位置)。Scroller
:实现平滑滚动(需配合computeScroll()
)。
自定义属性与样式
1. 声明自定义属性
- 在
res/values/attrs.xml
中定义:
<resources>
<declare-styleable name="MyView">
<attr name="custom_color" format="color" />
<attr name="custom_text" format="string" />
<attr name="custom_radius" format="dimension" />
</declare-styleable>
</resources>
- 在 XML 中使用(需添加命名空间):
<com.example.MyView
xmlns:app="http://schemas.android.com/apk/res-auto"
app:custom_color="#FF0000"
app:custom_text="Hello"
... />
- 在构造方法中解析:
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyView);
int color = ta.getColor(R.styleable.MyView_custom_color, Color.BLACK);
String text = ta.getString(R.styleable.MyView_custom_text);
ta.recycle(); // 必须回收TypedArray
2. 样式(Style)与主题(Theme)
- 样式:定义控件外观(如
android:style="@style/MyButtonStyle"
)。 - 主题:定义应用或 Activity 的全局风格(通过
AndroidManifest.xml
设置)。
性能优化与高级技巧
1. 性能优化要点
- 避免过度绘制:通过
Hierarchy Viewer
工具检查,减少透明层和重叠绘制。 - 硬件加速:在 Manifest 中开启
android:hardwareAccelerated="true"
。 - 缓存机制:使用
setDrawingCacheEnabled(true)
缓存绘制结果。 - 减少 onDraw 调用:合并绘制操作,避免频繁调用
invalidate()
。
2. 动画与过渡效果
- 属性动画(ObjectAnimator) :直接操作 View 属性(如
translationX
、alpha
)。 - 自定义动画:重写
Animation
类或使用ValueAnimator
监听数值变化驱动绘制。 - 触摸反馈动画:通过
StateListAnimator
实现按压、选中状态的动画。
3. 适配与兼容性
- 屏幕适配:使用
dp
、sp
单位,或通过Configuration
监听屏幕旋转。 - API 版本兼容:使用
ViewCompat
工具类处理不同版本差异(如 AndroidX 库)。
实战案例
- 自定义进度条:通过
onDraw
绘制圆弧进度,配合ValueAnimator
动态更新。 - 流式布局(FlowLayout) :重写
onMeasure
和onLayout
实现子 View 自动换行。 - 可拖拽 View:结合
onTouchEvent
和Scroller
实现拖拽滑动效果。
1. 自定义进度条
自定义圆形进度条View
/**
* 自定义圆形进度条视图
*/
public class CircularProgressView extends View {
// 绘图相关属性
private Paint paint; // 用于绘制的画笔对象
// 进度相关属性
private float progress = 0f; // 当前进度,默认为0
private float maxProgress = 100f; // 最大进度,默认为100
private int progressColor = Color.BLUE; // 进度条颜色,默认为蓝色
private int backgroundColor = Color.GRAY; // 背景颜色,默认为灰色
private float progressWidth = 10f; // 进度条宽度,默认为10像素
private float startAngle = -90f; // 圆弧起始角度,默认从顶部开始(-90度)
// 构造方法
public CircularProgressView(Context context) {
this(context, null);
}
public CircularProgressView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CircularProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 初始化画笔
paint = new Paint(Paint.ANTI_ALIAS_FLAG); // 创建画笔对象,开启抗锯齿
paint.setStyle(Paint.Style.STROKE); // 设置为描边模式
paint.setStrokeCap(Paint.Cap.ROUND); // 设置描边端点为圆形
// 从自定义属性获取值
if (attrs != null) {
// 获取自定义属性集合
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.CircularProgressView,
0, 0);
try {
// 从自定义属性中获取进度条颜色,未设置时使用默认值
progressColor = a.getColor(R.styleable.CircularProgressView_progressColor, Color.BLUE);
// 从自定义属性中获取背景颜色,未设置时使用默认值
backgroundColor = a.getColor(R.styleable.CircularProgressView_backgroundColor, Color.GRAY);
// 从自定义属性中获取进度条宽度,未设置时使用默认值
progressWidth = a.getDimension(R.styleable.CircularProgressView_progressWidth, 10f);
} finally {
// 回收TypedArray资源
a.recycle();
}
}
// 设置画笔宽度
paint.setStrokeWidth(progressWidth);
}
/**
* 设置当前进度
* @param progress 当前进度值
*/
public void setProgress(float progress) {
// 确保进度值在合法范围内(0到maxProgress之间)
this.progress = Math.max(0f, Math.min(progress, maxProgress));
invalidate(); // 触发视图重绘
}
/**
* 设置最大进度
* @param maxProgress 最大进度值
*/
public void setMaxProgress(float maxProgress) {
// 确保最大进度值至少为1
this.maxProgress = Math.max(1f, maxProgress);
invalidate(); // 触发视图重绘
}
/**
* 设置进度条颜色
* @param color 进度条颜色
*/
public void setProgressColor(int color) {
this.progressColor = color;
invalidate(); // 触发视图重绘
}
/**
* 设置背景颜色
* @param color 背景颜色
*/
public void setBackgroundColor(int color) {
this.backgroundColor = color;
invalidate(); // 触发视图重绘
}
/**
* 设置进度条宽度
* @param width 进度条宽度
*/
public void setProgressWidth(float width) {
this.progressWidth = width;
// 更新画笔宽度
paint.setStrokeWidth(width);
invalidate(); // 触发视图重绘
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 计算圆心坐标和半径
float centerX = getWidth() / 2f; // 获取视图宽度的一半作为圆心x坐标
float centerY = getHeight() / 2f; // 获取视图高度的一半作为圆心y坐标
float radius = Math.min(centerX, centerY) - progressWidth / 2; // 计算圆弧半径
// 绘制背景圆环
paint.setColor(backgroundColor); // 设置画笔颜色为背景色
// 绘制一个完整的圆作为背景
canvas.drawCircle(centerX, centerY, radius, paint);
// 绘制进度圆弧
paint.setColor(progressColor); // 设置画笔颜色为进度条颜色
// 计算当前进度对应的圆心角
float sweepAngle = 360f * (progress / maxProgress);
// 定义一个矩形区域用于绘制圆弧
RectF oval = new RectF(
centerX - radius, // 左坐标
centerY - radius, // 顶坐标
centerX + radius, // 右坐标
centerY + radius // 底坐标
);
// 绘制圆弧,参数包括:绘制区域、起始角度、扫过的角度、是否闭合、画笔
canvas.drawArc(oval, startAngle, sweepAngle, false, paint);
}
}
在布局文件中使用
首先在res/values/attrs.xml
中定义自定义属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircularProgressView">
<attr name="progressColor" format="color"/>
<attr name="backgroundColor" format="color"/>
<attr name="progressWidth" format="dimension"/>
</declare-styleable>
</resources>
然后在布局文件中使用:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
tools:context=".MainActivity">
<com.example.myview.CircularProgressView
android:id="@+id/circularProgressView"
android:layout_width="200dp"
android:layout_height="200dp"
app:progressColor="@color/colorPrimary"
app:backgroundColor="@color/colorGray"
app:progressWidth="15dp" />
</LinearLayout>
使用ValueAnimator实现动态更新
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
CircularProgressView progressView = findViewById(R.id.circularProgressView);
// 设置初始进度
progressView.setProgress(0);
// 创建ValueAnimator
ValueAnimator animator = ValueAnimator.ofFloat(0, 100);
animator.setDuration(3000); // 动画时长3秒
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float currentValue = (float) animation.getAnimatedValue();
progressView.setProgress(currentValue);
}
});
// 启动动画
animator.start();
}
}
2. 流式布局
自定义流式布局 View
/**
* 自定义流式布局(FlowLayout)
*/
public class CustomFlowLayout extends ViewGroup {
private int lineSpacing = 0; // 行间距
private int columnSpacing = 0; // 列间距
public CustomFlowLayout(Context context) {
this(context, null);
}
public CustomFlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 可以在这里解析自定义属性,设置行间距和列间距
}
/**
* 设置行间距
* @param lineSpacing 行间距
*/
public void setLineSpacing(int lineSpacing) {
this.lineSpacing = lineSpacing;
requestLayout(); // 请求重新布局
}
/**
* 设置列间距
* @param columnSpacing 列间距
*/
public void setColumnSpacing(int columnSpacing) {
this.columnSpacing = columnSpacing;
requestLayout(); // 请求重新布局
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int maxLineWidth = 0; // 最大行宽
int totalHeight = 0; // 总高度
int currentLineWidth = 0; // 当前行的宽度
int currentLineHeight = 0; // 当前行的高度
// 遍历所有子视图
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
// 测量子视图
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight);
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
// 如果当前行的宽度加上子视图的宽度超过了父视图的宽度,则换行
if (currentLineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight()) {
// 更新最大行宽
maxLineWidth = Math.max(maxLineWidth, currentLineWidth);
// 更新总高度
totalHeight += currentLineHeight + lineSpacing;
// 重置当前行的宽度和高度
currentLineWidth = childWidth;
currentLineHeight = childHeight;
} else {
// 累加当前行的宽度
if (currentLineWidth != 0) {
currentLineWidth += columnSpacing;
}
currentLineWidth += childWidth;
// 更新当前行的最大高度
currentLineHeight = Math.max(currentLineHeight, childHeight);
}
}
// 添加最后一行的高度
totalHeight += currentLineHeight;
// 考虑padding
totalHeight += getPaddingTop() + getPaddingBottom();
maxLineWidth += getPaddingLeft() + getPaddingRight();
// 计算最终的宽高
int measuredWidth = widthMode == MeasureSpec.EXACTLY ? sizeWidth : maxLineWidth;
int measuredHeight = heightMode == MeasureSpec.EXACTLY ? sizeHeight : totalHeight;
setMeasuredDimension(measuredWidth, measuredHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int width = r - l;
int currentX = getPaddingLeft();
int currentY = getPaddingTop();
int currentLineHeight = 0;
// 遍历所有子视图
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
int childWidthWithMargin = childWidth + lp.leftMargin + lp.rightMargin;
int childHeightWithMargin = childHeight + lp.topMargin + lp.bottomMargin;
// 如果当前行的宽度加上子视图的宽度超过了父视图的宽度,则换行
if (currentX + childWidthWithMargin > width - getPaddingRight()) {
currentX = getPaddingLeft();
currentY += currentLineHeight + lineSpacing;
currentLineHeight = 0;
}
// 计算子视图的实际位置
int left = currentX + lp.leftMargin;
int top = currentY + lp.topMargin;
int right = left + childWidth;
int bottom = top + childHeight;
// 布置子视图
child.layout(left, top, right, bottom);
// 更新当前行的宽度和高度
currentX += childWidthWithMargin + columnSpacing;
currentLineHeight = Math.max(currentLineHeight, childHeightWithMargin);
}
}
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof MarginLayoutParams;
}
}
布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="默认间距的流式布局:"
android:textSize="16sp"
android:layout_marginBottom="8dp"/>
<com.example.myview.CustomFlowLayout
android:id="@+id/customFlowLayout1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#EEEEEE"
android:padding="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="标签1"
android:padding="8dp"
android:background="#3F51B5"
android:textColor="#FFFFFF"
android:layout_margin="4dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="这是一个较长的标签2"
android:padding="8dp"
android:background="#3F51B5"
android:textColor="#FFFFFF"
android:layout_margin="4dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="短标签3"
android:padding="8dp"
android:background="#3F51B5"
android:textColor="#FFFFFF"
android:layout_margin="4dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="标签4"
android:padding="8dp"
android:background="#3F51B5"
android:textColor="#FFFFFF"
android:layout_margin="4dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="这是一个非常非常长的标签5"
android:padding="8dp"
android:background="#3F51B5"
android:textColor="#FFFFFF"
android:layout_margin="4dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="中等长度的标签6"
android:padding="8dp"
android:background="#3F51B5"
android:textColor="#FFFFFF"
android:layout_margin="4dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="标签7"
android:padding="8dp"
android:background="#3F51B5"
android:textColor="#FFFFFF"
android:layout_margin="4dp"/>
</com.example.myview.CustomFlowLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="自定义间距的流式布局:"
android:textSize="16sp"
android:layout_marginTop="24dp"
android:layout_marginBottom="8dp"/>
<com.example.myview.CustomFlowLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#EEEEEE"
android:padding="8dp"
android:id="@+id/customFlowLayout2">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="标签1"
android:padding="8dp"
android:background="#FF5722"
android:textColor="#FFFFFF"
android:layout_margin="4dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="这是一个较长的标签2"
android:padding="8dp"
android:background="#FF5722"
android:textColor="#FFFFFF"
android:layout_margin="4dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="短标签3"
android:padding="8dp"
android:background="#FF5722"
android:textColor="#FFFFFF"
android:layout_margin="4dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="标签4"
android:padding="8dp"
android:background="#FF5722"
android:textColor="#FFFFFF"
android:layout_margin="4dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="这是一个非常非常长的标签5"
android:padding="8dp"
android:background="#FF5722"
android:textColor="#FFFFFF"
android:layout_margin="4dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="中等长度的标签6"
android:padding="8dp"
android:background="#FF5722"
android:textColor="#FFFFFF"
android:layout_margin="4dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="标签7"
android:padding="8dp"
android:background="#FF5722"
android:textColor="#FFFFFF"
android:layout_margin="4dp"/>
</com.example.myview.CustomFlowLayout>
</LinearLayout>
应用布局设置参数
public class CustomFlowLayoutActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_custom_flow_layout);
CustomFlowLayout flowLayout1 = findViewById(R.id.customFlowLayout1);
CustomFlowLayout flowLayout2 = findViewById(R.id.customFlowLayout2);
flowLayout2.setLineSpacing(8); // 设置行间距为16dp
flowLayout2.setColumnSpacing(16); // 设置列间距为12dp
}
}
3. 可拖拽 View
自定义可拖拽 View
/**
* 支持拖拽和回弹效果的自定义View
* 通过Scroller类实现平滑的动画效果,限制在父容器范围内移动
*/
public class DraggableCustomView extends View {
// 处理平滑滚动的辅助类
private Scroller mScroller;
// 记录触摸事件的上一个坐标
private int mLastX;
private int mLastY;
// 拖拽边界
private int mLeftBound;
private int mRightBound;
private int mTopBound;
private int mBottomBound;
public DraggableCustomView(Context context) {
this(context, null);
}
public DraggableCustomView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DraggableCustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
/**
* 初始化View
* @param context 上下文
*/
private void init(Context context) {
// 初始化Scroller,用于实现平滑滚动效果
mScroller = new Scroller(context);
}
/**
* 当View尺寸变化时调用,初始化边界值
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// 设置拖拽边界,默认为父容器的边界
mLeftBound = 0;
mRightBound = getParent() != null ?
((View) getParent()).getWidth() - getWidth() : w;
mTopBound = 0;
mBottomBound = getParent() != null ?
((View) getParent()).getHeight() - getHeight() : h;
}
/**
* 处理触摸事件实现拖拽功能
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
// 获取当前触摸点坐标
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 手指按下时终止任何正在进行的滚动动画
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
// 记录初始触摸位置
mLastX = x;
mLastY = y;
// 返回true表示消费该触摸事件
return true;
case MotionEvent.ACTION_MOVE:
// 计算与上次触摸点的偏移量
int dx = x - mLastX;
int dy = y - mLastY;
// 计算移动后的新边界位置
int newLeft = getLeft() + dx;
int newTop = getTop() + dy;
int newRight = getRight() + dx;
int newBottom = getBottom() + dy;
// 边界检查和修正:如果超出边界则调整偏移量
if (newLeft < mLeftBound) {
dx = mLeftBound - getLeft();
}
if (newRight > mRightBound) {
dx = mRightBound - getRight();
}
if (newTop < mTopBound) {
dy = mTopBound - getTop();
}
if (newBottom > mBottomBound) {
dy = mBottomBound - getBottom();
}
// 如果有有效的偏移量,则移动View并刷新
if (dx != 0 || dy != 0) {
// 调整View的位置
offsetLeftAndRight(dx);
offsetTopAndBottom(dy);
// 触发重绘
invalidate();
}
// 更新最后触摸位置
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// 手指抬起或取消触摸时,处理边界回弹效果
int deltaX = 0;
int deltaY = 0;
// 计算需要回弹的距离
if (getLeft() < mLeftBound) {
deltaX = mLeftBound - getLeft();
} else if (getRight() > mRightBound) {
deltaX = mRightBound - getRight();
}
if (getTop() < mTopBound) {
deltaY = mTopBound - getTop();
} else if (getBottom() > mBottomBound) {
deltaY = mBottomBound - getBottom();
}
// 如果需要回弹,则启动Scroller动画
if (deltaX != 0 || deltaY != 0) {
mScroller.startScroll(
getScrollX(),
getScrollY(),
-deltaX,
-deltaY,
500 // 动画持续时间(毫秒)
);
// 触发重绘以执行动画
invalidate();
}
break;
}
// 调用父类方法处理其他触摸事件
return super.onTouchEvent(event);
}
/**
* Scroller动画的回调方法,更新View的滚动位置
*/
@Override
public void computeScroll() {
// 如果Scroller动画还在进行中
if (mScroller.computeScrollOffset()) {
// 更新View的滚动位置
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
// 继续触发重绘以继续动画
postInvalidate();
}
}
}
常见问题与避坑指南
- 测量尺寸异常:未正确处理
wrap_content
和padding
,需在onMeasure
中手动计算。 - 事件冲突:父子 View 同时处理事件时,需合理设置
onInterceptTouchEvent
返回值。 - 内存泄漏:动画未停止时销毁 View,需在
onDetachedFromWindow
中调用animator.cancel()
。 - ANR 风险:避免在
onDraw
中执行耗时操作(如创建新对象),可使用postDelay
异步处理。