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 开始练习,逐步掌握复杂控件的开发。

相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone4 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen4 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone4 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui