【Android】从复用到重绘的控件定制化方式

发布于:2025-09-04 ⋅ 阅读:(20) ⋅ 点赞:(0)

自定义 View 通常分为三类:组合控件、继承控件和重绘控件。

类型 定义
组合控件 基于已有系统控件,通过布局文件或代码组合得到的新控件,适合快速复用。
继承控件 通过继承系统控件类,保留或扩展其功能,适合对单一控件进行定制化。
重绘控件 直接继承自 View,依靠 CanvasPaint 完全自绘,灵活性最高。

组合控件

组合控件的实现思路是:将多个现有系统控件组织到一个布局文件中,再通过继承布局类(如 FrameLayoutLinearLayout 等)包装为一个独立的自定义控件。这样可以对内部控件的样式和交互进行统一管理。

XML 布局示例:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/title"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#FEFEFE"><Button
        android:id="@+id/button_left"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="center_vertical"
        android:backgroundTint="@android:color/transparent"
        android:text="Button"
        android:textColor="@color/black" /><TextView
        android:id="@+id/title_text"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center"
        android:gravity="center"
        android:text="TextView"
        android:textColor="@color/black" /></LinearLayout>

在这里插入图片描述

public class TitleView extends FrameLayout {
 
    private Button leftButton;
    private TextView titleText;
 
    public TitleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        LayoutInflater.from(context).inflate(R.layout.title, this);
        titleText = findViewById(R.id.title_text);
        leftButton = findViewById(R.id.button_left);
    }
 
    public void setTitleText(String text) {
        titleText.setText(text);
    }
 
    public void setLeftButtonText(String text) {
        leftButton.setText(text);
    }
 
    public void setLeftButtonListener(OnClickListener listener) {
        leftButton.setOnClickListener(listener);
    }
}

在使用时,只需要像调用系统控件一样,在 XML 中通过包名+类名引入:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
    <com.example.android.TitleView
        android:id="@+id/title_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</FrameLayout>

自定义属性

系统控件的属性分为 android:app: 两类,开发者也可以通过定义 declare-styleable 来添加属于自定义控件的 app: 属性。

定义属性

res/values/attrs.xml

<resources>
    <declare-styleable name="TitleView">
        <attr name="titleText" format="string" />
        <attr name="leftButtonText" format="string" />
        <attr name="titleColor" format="color" />
    </declare-styleable>
</resources>

declare-styleablename 对应控件类名,内部的 <attr> 用于声明属性名和可接受的数据类型。

使用属性

在布局根节点引入命名空间:

xmlns:app="http://schemas.android.com/apk/res-auto"

命名空间的 schemas.android.com 作为占位符用于放置 res-auto 自动检测的包名,所以想要引入 app: 作用域,在根布局加入固定语句即可,然后在自定义控件中使用:

<com.example.myapp.TitleView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:titleText="首页"
    app:leftButtonText="返回"
    app:titleColor="@color/black"/>

解析属性

在控件的构造方法中使用 TypedArray 解析属性值:

public class TitleView extends FrameLayout {
 
    private Button leftButton;
    private TextView titleText;
 
    public TitleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        LayoutInflater.from(context).inflate(R.layout.title, this);
        titleText = findViewById(R.id.title_text);
        leftButton = findViewById(R.id.button_left);TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TitleView);String textTitle = ta.getString(R.styleable.TitleView_titleText);
        String textLeftButton = ta.getString(R.styleable.TitleView_leftButtonText);
        int color = ta.getColor(R.styleable.TitleView_titleColor, Color.BLACK);
​
        ta.recycle(); // 回收TypedArray的方法,避免内存泄漏
​
        titleText.setText(textTitle);
        leftButton.setText(textLeftButton);
        titleText.setTextColor(color);
    }
}

因为控件类实例化时调用构造方法来读取 XML 的属性资源,所以这种方式称为 静态声明;而在 Java 代码中通过调用方法设置属性称为 动态声明


继承控件

继承控件通过扩展现有控件类,在保留其基础功能的同时,加入额外逻辑。例如通过重写 onDraw,在 TextView 上绘制辅助线。

关键回调方法:

  • 构造方法:初始化对象并读取 XML 属性
  • onMeasure(int widthMeasureSpec, int heightMeasureSpec):确定控件尺寸
  • onSizeChanged(int w, int h, int old, int oldh):控件大小首次确定或发生变化时触发
  • onDraw(Canvas canvas):实际的绘制逻辑
  • onLayout(boolean changed, int left, int top, int right, int bottom):View 内用于确定控件本身在父布局中的位置;ViewGroup 则用于计算和设置所有子控件的位置

因为大多数情况下我们使用继承控件的形式都是希望复用系统的 onMeasure 和 onLayout 流程,所以我们只需要重写 onDraw 方法。实现非常简单:

public class LineTextView extends androidx.appcompat.widget.AppCompatTextView {private Paint mPaint;public LineTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }private void init() {
        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
    }@Override
    protected void onDraw(Canvas canvas) {
        int width = getWidth();
        int height = getHeight();// 背景
        mPaint.setColor(Color.parseColor("#FEFEFE"));
        RectF rectF = new RectF(0, 0, width, height);
        canvas.drawRect(rectF, mPaint);// 中线
        mPaint.setColor(Color.BLACK);
        mPaint.setStrokeWidth(5);
        canvas.drawLine(0, height / 2, width, height / 2, mPaint);super.onDraw(canvas); // 保持原有文字绘制
    }
}

在这里插入图片描述


重绘控件

重绘控件是完全基于 View 的高定制化自定义控件实现,适合从零绘制特殊效果。相比继承控件,它需要开发者自己实现 onMeasureonDraw,从而精确控制尺寸和外观。

测量逻辑

MeasureSpec 提供三种模式:

  • UNSPECIFIED:父容器对子控件大小不做限制
  • EXACTLY:精确值(对应 match_parent 或固定值)
  • AT_MOST:最大值(对应 wrap_content

默认实现 getDefaultSize 中,AT_MOSTEXACTLY 被同等处理,这就是为什么如果不重写 onMeasurewrap_content 会表现为填满父容器,所以我们重绘控件时必须要重写 onMeasure 方法。

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpe);
    
    switch (specMode) {
    	case MeasureSpec.UNSPECIFIED:
            result = size;
        	break;
    	case MeasureSpec.AT_MOST:
    	case MeasureSpec.EXACTLY:
        	result = specSize; // here
        	break;
    }
    return result;
}

示例:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(300, 300);
    } else if (widthMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(300, heightSize);
    } else if (heightMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(widthSize, 300);
    }
}

绘制逻辑

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    int paddingLeft = getPaddingLeft();
    int paddingRight = getPaddingRight();
    int paddingTop = getPaddingTop();
    int paddingBottom = getPaddingBottom();

    int width = getWidth() - paddingLeft - paddingRight;
    int height = getHeight() - paddingTop - paddingBottom;

    paint.setColor(Color.parseColor("#FEFEFE"));
    canvas.drawRect(paddingLeft, paddingTop, width + paddingLeft, height + paddingTop, paint);
}

这样我们得到了一个矩形空白内容的控件。


渐变程度条实践

在天气应用中,用于表示指数程度的控件往往需要比系统控件更灵活。以下是一个重绘控件示例:

public class RainbowLineView extends View {

    private Paint linePaint;
    private Paint circlePaint;
    private float[] dataPoints = {0.2f};

    public RainbowLineView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        linePaint.setStyle(Paint.Style.STROKE);
        linePaint.setStrokeWidth(13);

        circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        circlePaint.setStyle(Paint.Style.FILL);
        circlePaint.setColor(Color.parseColor("#FEFEFE"));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int width = getWidth();
        int centerY = getHeight() / 2;

        LinearGradient gradient = new LinearGradient( // 颜色渐变过渡器
                0, centerY, width, centerY,
                new int[]{
                        Color.parseColor("#009FFB"), // 蓝
                        Color.parseColor("#55D2CC"), // 青
                        Color.parseColor("#FBD449"), // 黄
                        Color.parseColor("#FEA736"), // 橙
                        Color.parseColor("#FE3F4F")  // 红
                },
                null, Shader.TileMode.CLAMP
        );
        linePaint.setShader(gradient);

        // 渐变线
        canvas.drawLine(0, centerY, width, centerY, linePaint);

        // 标记点
        for (float value : dataPoints) {
            float x = value * width;
            canvas.drawCircle(x, centerY, 15, circlePaint);
        }
    }

    public void setDataPoints(float[] dataPoints) {
        this.dataPoints = dataPoints;
        invalidate(); // 请求重绘,回调onDraw方法
    }
}

该控件通过 LinearGradient 绘制水平渐变色条,并在 dataPoints 指定的位置绘制圆点,可以用于表示某一指标在区间中的程度。

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到