Android 自定义 View 完全指南

📱 一、为什么需要自定义 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));

测试检查清单:

    1. ✅ 在不同屏幕尺寸上测试
    1. ✅ 测试横竖屏切换
    1. ✅ 测试深色模式
    1. ✅ 测试辅助功能(TalkBack)
    1. ✅ 测试内存使用情况
    1. ✅ 测试动画性能
    1. ✅ 测试触摸反馈
    1. ✅ 测试不同 Android 版本

通过以上完整指南,你可以掌握 Android 自定义 View 的核心技术。记住:自定义 View 是 Android 开发的高级技能,需要结合项目需求、性能考虑和用户体验来综合设计。先从简单的自定义 View 开始练习,逐步掌握复杂控件的开发。

相关推荐
2601_949833393 小时前
flutter_for_openharmony口腔护理app实战+意见反馈实现
android·javascript·flutter
峥嵘life4 小时前
Android 16 EDLA测试STS模块
android·大数据·linux·学习
TheNextByte14 小时前
如何打印Android手机联系人?
android·智能手机
泡泡以安5 小时前
Android 逆向实战:从零突破某电商 App 登录接口全参数加密
android·爬虫·安卓逆向
2501_944525546 小时前
Flutter for OpenHarmony 个人理财管理App实战 - 预算详情页面
android·开发语言·前端·javascript·flutter·ecmascript
清蒸鳜鱼7 小时前
【Mobile Agent——Droidrun】MacOS+Android配置、使用指南
android·macos·mobileagent
2501_915918417 小时前
HTTPS 代理失效,启用双向认证(mTLS)的 iOS 应用网络怎么抓包调试
android·网络·ios·小程序·https·uni-app·iphone
峥嵘life7 小时前
Android EDLA CTS、GTS等各项测试命令汇总
android·学习·elasticsearch
Cobboo7 小时前
i单词上架鸿蒙应用市场之路:一次从 Android 到 HarmonyOS 的完整实战
android·华为·harmonyos