📱 一、为什么需要自定义 View?
系统 View 的局限性
// 系统控件无法满足这些需求:
1. 特殊形状的按钮(圆形、星形、多边形)
2. 自定义进度条(环形进度、波浪进度)
3. 复杂图表(股票K线图、心电图)
4. 特殊动画效果(粒子效果、3D变换)
5. 定制绘图(游戏界面、特效)
⚙️ 二、自定义 View 的三种方式
1. 继承现有 View(最常用)
// 对现有控件进行扩展
public class CustomButton extends AppCompatButton {
// 在现有按钮基础上添加新功能
}
2. 继承 View(从头绘制)
// 完全自己绘制
public class CustomView extends View {
// 需要重写 onDraw() 等方法
}
3. 继承 ViewGroup(组合控件)
// 组合多个现有控件
public class CustomLayout extends LinearLayout {
// 管理多个子 View
}
🎨 三、自定义 View 核心流程
完整生命周期与调用顺序
构造函数(初始化) →
onAttachedToWindow()(附加到窗口) →
onMeasure()(测量) →
onLayout()(布局) →
onDraw()(绘制) →
onDetachedFromWindow()(从窗口分离)
🚀 四、详细实现步骤(Java 代码)
第一步:创建自定义 View 类
1. 基础框架
// CustomView.java
public class CustomView extends View {
// 定义属性
private Paint paint;
private int backgroundColor;
private int foregroundColor;
private float radius;
// 1. 构造函数(必须实现)
public CustomView(Context context) {
this(context, null);
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 初始化
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
// 初始化画笔
paint = new Paint();
paint.setAntiAlias(true); // 抗锯齿
paint.setStyle(Paint.Style.FILL);
// 从 XML 属性中读取值
if (attrs != null) {
TypedArray ta = context.obtainStyledAttributes(
attrs, R.styleable.CustomView, 0, 0);
try {
backgroundColor = ta.getColor(
R.styleable.CustomView_backgroundColor,
Color.GRAY);
foregroundColor = ta.getColor(
R.styleable.CustomView_foregroundColor,
Color.BLUE);
radius = ta.getDimension(
R.styleable.CustomView_radius,
50f);
} finally {
ta.recycle(); // 必须回收
}
}
}
}
2. 定义自定义属性
<!-- res/values/attrs.xml -->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CustomView">
<!-- 背景颜色 -->
<attr name="backgroundColor" format="color" />
<!-- 前景颜色 -->
<attr name="foregroundColor" format="color" />
<!-- 半径 -->
<attr name="radius" format="dimension" />
<!-- 显示文本 -->
<attr name="text" format="string" />
<!-- 文本大小 -->
<attr name="textSize" format="dimension" />
<!-- 显示进度 -->
<attr name="progress" format="float" />
</declare-styleable>
</resources>
第二步:实现测量(onMeasure)
// 测量 View 的大小
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 获取测量模式和尺寸
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 默认大小(200dp)
int defaultSize = dp2px(200);
int width, height;
// 处理宽度
switch (widthMode) {
case MeasureSpec.EXACTLY: // match_parent 或具体数值
width = widthSize;
break;
case MeasureSpec.AT_MOST: // wrap_content
width = Math.min(defaultSize, widthSize);
break;
case MeasureSpec.UNSPECIFIED: // 未指定
default:
width = defaultSize;
break;
}
// 处理高度(类似宽度)
switch (heightMode) {
case MeasureSpec.EXACTLY:
height = heightSize;
break;
case MeasureSpec.AT_MOST:
height = Math.min(defaultSize, heightSize);
break;
case MeasureSpec.UNSPECIFIED:
default:
height = defaultSize;
break;
}
// 设置测量结果(必须调用)
setMeasuredDimension(width, height);
}
// dp 转 px 工具方法
private int dp2px(float dp) {
return (int) (dp * getResources().getDisplayMetrics().density + 0.5f);
}
第三步:实现绘制(onDraw)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 获取 View 的宽高
int width = getWidth();
int height = getHeight();
// 1. 绘制背景
paint.setColor(backgroundColor);
canvas.drawRect(0, 0, width, height, paint);
// 2. 绘制圆形
paint.setColor(foregroundColor);
float centerX = width / 2f;
float centerY = height / 2f;
canvas.drawCircle(centerX, centerY, radius, paint);
// 3. 绘制文本
paint.setColor(Color.WHITE);
paint.setTextSize(dp2px(20));
paint.setTextAlign(Paint.Align.CENTER);
String text = "自定义View";
Rect bounds = new Rect();
paint.getTextBounds(text, 0, text.length(), bounds);
// 垂直居中绘制文本
float textY = centerY - (bounds.top + bounds.bottom) / 2f;
canvas.drawText(text, centerX, textY, paint);
// 4. 绘制进度(示例)
drawProgress(canvas, centerX, centerY);
}
// 绘制进度环
private void drawProgress(Canvas canvas, float centerX, float centerY) {
Paint progressPaint = new Paint();
progressPaint.setAntiAlias(true);
progressPaint.setStyle(Paint.Style.STROKE);
progressPaint.setStrokeWidth(dp2px(5));
progressPaint.setColor(Color.RED);
// 计算进度角度(假设 progress 是 0-100 的值)
float progress = 75; // 示例进度值
float sweepAngle = 360 * progress / 100;
// 定义矩形区域
RectF rectF = new RectF(
centerX - radius * 1.2f,
centerY - radius * 1.2f,
centerX + radius * 1.2f,
centerY + radius * 1.2f
);
// 绘制进度圆弧(从 -90 度开始,顺时针绘制)
canvas.drawArc(rectF, -90, sweepAngle, false, progressPaint);
}
第四步:处理触摸事件
// 实现触摸交互
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
float x = event.getX();
float y = event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
// 手指按下
handleTouchDown(x, y);
break;
case MotionEvent.ACTION_MOVE:
// 手指移动
handleTouchMove(x, y);
break;
case MotionEvent.ACTION_UP:
// 手指抬起
handleTouchUp(x, y);
break;
case MotionEvent.ACTION_CANCEL:
// 触摸取消
handleTouchCancel();
break;
}
return true; // 消费触摸事件
}
private void handleTouchDown(float x, float y) {
// 判断是否点击在圆形区域内
float centerX = getWidth() / 2f;
float centerY = getHeight() / 2f;
float distance = (float) Math.sqrt(
Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2));
if (distance <= radius) {
// 点击在圆形内
performClick(); // 触发点击事件
animateClickEffect();
}
}
// 必须重写此方法以支持点击事件
@Override
public boolean performClick() {
super.performClick();
// 处理点击逻辑
if (onClickListener != null) {
onClickListener.onClick(this);
}
return true;
}
// 点击动画效果
private void animateClickEffect() {
ValueAnimator animator = ValueAnimator.ofFloat(1.0f, 0.8f, 1.0f);
animator.setDuration(200);
animator.addUpdateListener(animation -> {
float scale = (float) animation.getAnimatedValue();
setScaleX(scale);
setScaleY(scale);
});
animator.start();
}
第五步:实现动画效果
// 属性动画示例
public void startRotateAnimation() {
ObjectAnimator rotateAnim = ObjectAnimator.ofFloat(
this, "rotation", 0f, 360f);
rotateAnim.setDuration(1000);
rotateAnim.setRepeatCount(ValueAnimator.INFINITE);
rotateAnim.setInterpolator(new LinearInterpolator());
rotateAnim.start();
}
// 自定义动画
private ValueAnimator progressAnimator;
private float currentProgress = 0;
public void animateProgress(float targetProgress) {
if (progressAnimator != null && progressAnimator.isRunning()) {
progressAnimator.cancel();
}
progressAnimator = ValueAnimator.ofFloat(currentProgress, targetProgress);
progressAnimator.setDuration(500);
progressAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
progressAnimator.addUpdateListener(animation -> {
currentProgress = (float) animation.getAnimatedValue();
invalidate(); // 重绘 View
});
progressAnimator.start();
}
🎯 五、完整示例:圆形进度条
1. 完整实现代码
// CircleProgressView.java
public class CircleProgressView extends View {
// 画笔
private Paint backgroundPaint;
private Paint progressPaint;
private Paint textPaint;
// 属性
private int backgroundColor = Color.LTGRAY;
private int progressColor = Color.BLUE;
private int textColor = Color.BLACK;
private float strokeWidth = dp2px(10);
private float maxProgress = 100;
private float currentProgress = 0;
private String text = "0%";
// 动画相关
private ValueAnimator progressAnimator;
public CircleProgressView(Context context) {
this(context, null);
}
public CircleProgressView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CircleProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
// 从属性中读取值
if (attrs != null) {
TypedArray ta = context.obtainStyledAttributes(
attrs, R.styleable.CircleProgressView);
backgroundColor = ta.getColor(
R.styleable.CircleProgressView_backgroundColor,
backgroundColor);
progressColor = ta.getColor(
R.styleable.CircleProgressView_progressColor,
progressColor);
textColor = ta.getColor(
R.styleable.CircleProgressView_textColor,
textColor);
strokeWidth = ta.getDimension(
R.styleable.CircleProgressView_strokeWidth,
strokeWidth);
maxProgress = ta.getFloat(
R.styleable.CircleProgressView_maxProgress,
maxProgress);
currentProgress = ta.getFloat(
R.styleable.CircleProgressView_progress,
currentProgress);
ta.recycle();
}
// 初始化背景画笔
backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
backgroundPaint.setStyle(Paint.Style.STROKE);
backgroundPaint.setStrokeWidth(strokeWidth);
backgroundPaint.setColor(backgroundColor);
backgroundPaint.setStrokeCap(Paint.Cap.ROUND);
// 初始化进度画笔
progressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
progressPaint.setStyle(Paint.Style.STROKE);
progressPaint.setStrokeWidth(strokeWidth);
progressPaint.setColor(progressColor);
progressPaint.setStrokeCap(Paint.Cap.ROUND);
// 初始化文本画笔
textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(textColor);
textPaint.setTextSize(dp2px(20));
textPaint.setTextAlign(Paint.Align.CENTER);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int defaultSize = dp2px(150);
int width = getMeasurement(widthMeasureSpec, defaultSize);
int height = getMeasurement(heightMeasureSpec, defaultSize);
// 确保宽高相同(圆形)
int size = Math.min(width, height);
setMeasuredDimension(size, size);
}
private int getMeasurement(int measureSpec, int defaultSize) {
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.EXACTLY:
return size;
case MeasureSpec.AT_MOST:
return Math.min(defaultSize, size);
case MeasureSpec.UNSPECIFIED:
default:
return defaultSize;
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
float centerX = width / 2f;
float centerY = height / 2f;
// 计算半径(考虑画笔宽度)
float radius = Math.min(centerX, centerY) - strokeWidth / 2;
// 绘制背景圆环
canvas.drawCircle(centerX, centerY, radius, backgroundPaint);
// 绘制进度圆环
if (currentProgress > 0) {
float sweepAngle = 360 * currentProgress / maxProgress;
RectF rectF = new RectF(
centerX - radius,
centerY - radius,
centerX + radius,
centerY + radius
);
canvas.drawArc(rectF, -90, sweepAngle, false, progressPaint);
}
// 绘制文本
text = String.format("%.0f%%", currentProgress);
Rect bounds = new Rect();
textPaint.getTextBounds(text, 0, text.length(), bounds);
float textY = centerY - (bounds.top + bounds.bottom) / 2f;
canvas.drawText(text, centerX, textY, textPaint);
}
// 设置进度(带动画)
public void setProgress(float progress) {
setProgress(progress, true);
}
public void setProgress(float progress, boolean animate) {
float targetProgress = Math.min(progress, maxProgress);
if (animate) {
animateToProgress(targetProgress);
} else {
currentProgress = targetProgress;
invalidate();
}
}
private void animateToProgress(float targetProgress) {
if (progressAnimator != null && progressAnimator.isRunning()) {
progressAnimator.cancel();
}
progressAnimator = ValueAnimator.ofFloat(currentProgress, targetProgress);
progressAnimator.setDuration(800);
progressAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
progressAnimator.addUpdateListener(animation -> {
currentProgress = (float) animation.getAnimatedValue();
invalidate();
});
progressAnimator.start();
}
// 工具方法
private int dp2px(float dp) {
return (int) (dp * getResources().getDisplayMetrics().density + 0.5f);
}
// Getter 和 Setter
public float getProgress() {
return currentProgress;
}
public void setProgressColor(int color) {
progressPaint.setColor(color);
invalidate();
}
public void setTextSize(float size) {
textPaint.setTextSize(dp2px(size));
invalidate();
}
}
2. 属性定义
<!-- res/values/attrs.xml -->
<declare-styleable name="CircleProgressView">
<attr name="backgroundColor" format="color" />
<attr name="progressColor" format="color" />
<attr name="textColor" format="color" />
<attr name="strokeWidth" format="dimension" />
<attr name="maxProgress" format="float" />
<attr name="progress" format="float" />
</declare-styleable>
3. 在 XML 中使用
<!-- activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<com.example.myapp.view.CircleProgressView
android:id="@+id/progressView"
android:layout_width="200dp"
android:layout_height="200dp"
app:backgroundColor="#E0E0E0"
app:progressColor="#2196F3"
app:textColor="#333333"
app:strokeWidth="12dp"
app:maxProgress="100"
app:progress="75" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="增加进度"
android:onClick="onIncreaseProgress" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="随机进度"
android:onClick="onRandomProgress" />
</LinearLayout>
4. 在 Activity 中使用
// MainActivity.java
public class MainActivity extends AppCompatActivity {
private CircleProgressView progressView;
private Random random = new Random();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
progressView = findViewById(R.id.progressView);
// 动态改变属性
progressView.setProgressColor(Color.RED);
progressView.setTextSize(24);
// 3秒后开始自动增加进度
new Handler().postDelayed(() -> {
animateProgressTo(100);
}, 3000);
}
public void onIncreaseProgress(View view) {
float current = progressView.getProgress();
float newProgress = Math.min(current + 10, 100);
progressView.setProgress(newProgress);
}
public void onRandomProgress(View view) {
float randomProgress = random.nextFloat() * 100;
progressView.setProgress(randomProgress);
}
private void animateProgressTo(float targetProgress) {
progressView.setProgress(targetProgress);
}
}
🔧 六、自定义 ViewGroup 示例
流式布局实现
// FlowLayout.java - 自定义 ViewGroup
public class FlowLayout extends ViewGroup {
private int horizontalSpacing = dp2px(10);
private int verticalSpacing = dp2px(10);
private List<List<View>> allLines = new ArrayList<>(); // 所有行
private List<Integer> lineHeights = new ArrayList<>(); // 每行高度
public FlowLayout(Context context) {
this(context, null);
}
public FlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if (attrs != null) {
TypedArray ta = context.obtainStyledAttributes(
attrs, R.styleable.FlowLayout);
horizontalSpacing = ta.getDimensionPixelSize(
R.styleable.FlowLayout_horizontalSpacing,
horizontalSpacing);
verticalSpacing = ta.getDimensionPixelSize(
R.styleable.FlowLayout_verticalSpacing,
verticalSpacing);
ta.recycle();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 清除之前的数据
allLines.clear();
lineHeights.clear();
// 获取父容器的限制
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 测量子 View
int childCount = getChildCount();
if (childCount == 0) {
setMeasuredDimension(0, 0);
return;
}
List<View> lineViews = new ArrayList<>(); // 当前行的 View
int lineWidthUsed = 0; // 当前行已使用的宽度
int lineHeight = 0; // 当前行的高度
int totalHeight = 0; // 总高度
// 可用宽度(考虑 padding)
int parentWidth = widthSize - getPaddingLeft() - getPaddingRight();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
// 测量子 View
LayoutParams lp = child.getLayoutParams();
int childWidthSpec = getChildMeasureSpec(
widthMeasureSpec,
getPaddingLeft() + getPaddingRight(),
lp.width);
int childHeightSpec = getChildMeasureSpec(
heightMeasureSpec,
getPaddingTop() + getPaddingBottom(),
lp.height);
child.measure(childWidthSpec, childHeightSpec);
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 判断是否需要换行
if (lineWidthUsed + childWidth + horizontalSpacing > parentWidth) {
// 需要换行
allLines.add(lineViews);
lineHeights.add(lineHeight);
totalHeight += lineHeight + verticalSpacing;
// 新起一行
lineViews = new ArrayList<>();
lineWidthUsed = 0;
lineHeight = 0;
}
// 添加当前 View 到行
lineViews.add(child);
lineWidthUsed += childWidth + horizontalSpacing;
lineHeight = Math.max(lineHeight, childHeight);
}
// 添加最后一行
if (!lineViews.isEmpty()) {
allLines.add(lineViews);
lineHeights.add(lineHeight);
totalHeight += lineHeight;
}
// 计算最终尺寸
int measuredWidth = widthSize;
int measuredHeight;
if (heightMode == MeasureSpec.EXACTLY) {
measuredHeight = heightSize;
} else {
measuredHeight = totalHeight + getPaddingTop() + getPaddingBottom();
if (heightMode == MeasureSpec.AT_MOST) {
measuredHeight = Math.min(measuredHeight, heightSize);
}
}
setMeasuredDimension(measuredWidth, measuredHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int currentX = getPaddingLeft();
int currentY = getPaddingTop();
// 遍历所有行
for (int i = 0; i < allLines.size(); i++) {
List<View> lineViews = allLines.get(i);
int lineHeight = lineHeights.get(i);
// 遍历行中的每个 View
for (View child : lineViews) {
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
// 计算 child 的位置(垂直居中)
int childLeft = currentX;
int childTop = currentY + (lineHeight - childHeight) / 2;
int childRight = childLeft + childWidth;
int childBottom = childTop + childHeight;
// 布局 child
child.layout(childLeft, childTop, childRight, childBottom);
// 更新 X 坐标
currentX += childWidth + horizontalSpacing;
}
// 换行
currentX = getPaddingLeft();
currentY += lineHeight + verticalSpacing;
}
}
// 添加 View 的方法
public void addTag(String text) {
TextView textView = new TextView(getContext());
textView.setText(text);
textView.setBackgroundResource(R.drawable.bg_tag);
textView.setPadding(dp2px(10), dp2px(5), dp2px(10), dp2px(5));
textView.setTextSize(14);
addView(textView);
}
private int dp2px(float dp) {
return (int) (dp * getResources().getDisplayMetrics().density + 0.5f);
}
}
流式布局属性定义
<!-- res/values/attrs.xml -->
<declare-styleable name="FlowLayout">
<attr name="horizontalSpacing" format="dimension" />
<attr name="verticalSpacing" format="dimension" />
</declare-styleable>
流式布局使用
<!-- activity_flow.xml -->
<com.example.myapp.view.FlowLayout
android:id="@+id/flowLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
app:horizontalSpacing="8dp"
app:verticalSpacing="8dp" />
// 在代码中添加标签
FlowLayout flowLayout = findViewById(R.id.flowLayout);
String[] tags = {"Android", "Java", "Kotlin", "Flutter",
"React Native", "iOS", "Web", "Python",
"大数据", "人工智能", "机器学习"};
for (String tag : tags) {
flowLayout.addTag(tag);
}
⚡ 七、性能优化技巧
1. 避免过度绘制
public class OptimizedView extends View {
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 1. 使用 canvas.clipRect() 限制绘制区域
canvas.clipRect(0, 0, getWidth(), getHeight());
// 2. 使用 canvas.quickReject() 提前拒绝绘制
if (canvas.quickReject(rect, Canvas.EdgeType.BW)) {
return;
}
// 3. 避免在 onDraw() 中创建对象
// 错误:每次绘制都创建新对象
// Paint paint = new Paint();
// 正确:复用对象
if (paint == null) {
paint = new Paint();
paint.setAntiAlias(true);
}
}
// 4. 使用 View.isHardwareAccelerated() 检查硬件加速
private void checkHardwareAcceleration() {
if (isHardwareAccelerated()) {
// 使用硬件加速的特性
} else {
// 回退方案
}
}
}
2. 使用 Canvas 绘制优化
private void optimizedDrawing(Canvas canvas) {
// 1. 使用 drawPath() 替代多个 drawLine()
Path path = new Path();
path.moveTo(0, 0);
path.lineTo(100, 0);
path.lineTo(100, 100);
path.lineTo(0, 100);
path.close();
canvas.drawPath(path, paint);
// 2. 使用 drawBitmap() 的 Rect 参数控制绘制区域
Rect src = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
Rect dst = new Rect(0, 0, getWidth(), getHeight());
canvas.drawBitmap(bitmap, src, dst, paint);
// 3. 使用 drawTextOnPath() 绘制曲线文字
Path textPath = new Path();
textPath.addCircle(centerX, centerY, radius, Path.Direction.CW);
canvas.drawTextOnPath("圆形文字", textPath, 0, 0, textPaint);
}
3. 内存优化
public class MemoryOptimizedView extends View {
private Bitmap bitmap;
private Paint paint;
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
// 在 View 附加到窗口时初始化资源
initResources();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
// 在 View 分离时释放资源
releaseResources();
}
private void initResources() {
paint = new Paint();
// 加载大图时使用合适的分辨率
bitmap = decodeSampledBitmapFromResource(
getResources(), R.drawable.large_image,
getWidth(), getHeight());
}
private void releaseResources() {
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
bitmap = null;
}
paint = null;
}
// 图片采样加载
private Bitmap decodeSampledBitmapFromResource(Resources res,
int resId,
int reqWidth,
int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
private int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth,
int reqHeight) {
int height = options.outHeight;
int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
int halfHeight = height / 2;
int halfWidth = width / 2;
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
}
🔍 八、调试与测试
1. 开启调试标志
public class DebuggableView extends View {
private boolean debugMode = BuildConfig.DEBUG;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (debugMode) {
// 绘制调试信息
drawDebugInfo(canvas);
}
}
private void drawDebugInfo(Canvas canvas) {
Paint debugPaint = new Paint();
debugPaint.setColor(Color.RED);
debugPaint.setStyle(Paint.Style.STROKE);
debugPaint.setStrokeWidth(2);
// 绘制边界框
canvas.drawRect(0, 0, getWidth(), getHeight(), debugPaint);
// 绘制中心点
canvas.drawCircle(getWidth()/2f, getHeight()/2f, 5, debugPaint);
// 绘制文本信息
debugPaint.setTextSize(dp2px(12));
String info = String.format("Size: %dx%d", getWidth(), getHeight());
canvas.drawText(info, 10, 20, debugPaint);
}
}
2. 性能分析工具
public class PerformanceMonitor {
public static void monitorDrawing(View view) {
view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
// 使用 Choreographer 监控绘制性能
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
private long lastFrameTime = 0;
@Override
public void doFrame(long frameTimeNanos) {
if (lastFrameTime != 0) {
long frameInterval = frameTimeNanos - lastFrameTime;
double fps = 1_000_000_000.0 / frameInterval;
if (fps < 50) {
Log.w("Performance", "Low FPS: " + fps);
}
}
lastFrameTime = frameTimeNanos;
// 继续监听下一帧
Choreographer.getInstance().postFrameCallback(this);
}
});
}
@Override
public void onViewDetachedFromWindow(View v) {
// 清理资源
}
});
}
}
📚 九、最佳实践总结
Do's:
// 1. 使用自定义属性
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
try {
// 读取属性
} finally {
ta.recycle(); // 必须回收
}
// 2. 正确处理 wrap_content
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 处理 AT_MOST 模式
}
// 3. 支持 padding
@Override
protected void onDraw(Canvas canvas) {
// 考虑 getPaddingLeft(), getPaddingTop() 等
}
// 4. 处理点击状态
@Override
public boolean onTouchEvent(MotionEvent event) {
// 更新 View 状态
invalidate();
return true;
}
// 5. 使用 postInvalidate() 在非 UI 线程更新
new Thread(() -> {
// 计算...
postInvalidate();
}).start();
Don'ts:
// 1. 不要在 onDraw() 中分配对象
@Override
protected void onDraw(Canvas canvas) {
// ❌ 错误:每次绘制都创建新对象
Paint paint = new Paint();
// ✅ 正确:复用对象
if (paint == null) {
paint = new Paint();
}
}
// 2. 不要忽略父 View 的限制
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// ❌ 错误:忽略测量模式
setMeasuredDimension(200, 200);
// ✅ 正确:处理所有测量模式
int width = getMeasurement(widthMeasureSpec, 200);
int height = getMeasurement(heightMeasureSpec, 200);
setMeasuredDimension(width, height);
}
// 3. 不要忘记调用 performClick()
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
// 必须调用 performClick() 以支持辅助功能
performClick();
}
return true;
}
// 4. 不要硬编码尺寸
// ❌ 错误:硬编码像素值
float radius = 50;
// ✅ 正确:使用 dp 或从属性读取
float radius = dp2px(50);
// 或
float radius = ta.getDimension(R.styleable.CustomView_radius, dp2px(50));
测试检查清单:
-
- ✅ 在不同屏幕尺寸上测试
-
- ✅ 测试横竖屏切换
-
- ✅ 测试深色模式
-
- ✅ 测试辅助功能(TalkBack)
-
- ✅ 测试内存使用情况
-
- ✅ 测试动画性能
-
- ✅ 测试触摸反馈
-
- ✅ 测试不同 Android 版本
通过以上完整指南,你可以掌握 Android 自定义 View 的核心技术。记住:自定义 View 是 Android 开发的高级技能,需要结合项目需求、性能考虑和用户体验来综合设计。先从简单的自定义 View 开始练习,逐步掌握复杂控件的开发。