当前位置: 首页 > news >正文

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

自定义 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"><Buttonandroid: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" /><TextViewandroid: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.TitleViewandroid: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.TitleViewandroid: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);}@Overrideprotected 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; // herebreak;}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"));}@Overrideprotected 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 指定的位置绘制圆点,可以用于表示某一指标在区间中的程度。

在这里插入图片描述

http://www.xdnf.cn/news/1438453.html

相关文章:

  • HarmonyOS 应用开发深度解析:基于 ArkTS 的声明式 UI 与状态管理艺术
  • HarmonyOS安装以及遇到的问题
  • Jenkins-Ansible部署discuz论坛
  • 38.Ansible判断+实例
  • PINN物理信息神经网络用于求解二阶常微分方程(ODE)的边值问题,Matlab实现
  • 力扣hot100:缺失的第一个正数(哈希思想)(41)
  • Qwen3-30B-A3B 模型解析
  • 【C++】迭代器详解与失效机制
  • # Shell 文本处理三剑客:awk、sed 与常用小工具详解
  • 【前端面试题✨】Vue篇(一)
  • Linux网络序列化与反序列化(6)
  • Linux文本处理——awk
  • 飞牛OS Nas,SSH安装宝塔后,smb文件不能共享问题
  • STM32——串口
  • 2025年- H109-Lc1493. 删掉一个元素以后全为 1 的最长子数组(双指针)--Java版
  • 别再误会了!Redis 6.0 的多线程,和你想象的完全不一样
  • 从入门到实战:Linux sed命令全攻略,文本处理效率翻倍
  • 【机器学习深度学习】向量模型与重排序模型:RAG 的双引擎解析
  • 使用DataLoader加载本地数据 食物分类案例
  • GitHub Classroom:编程教育的高效协作方案
  • MySQL查询limit 0,100和limit 10000000,100有什么区别?
  • Shell编程从入门到实践:基础语法与正则表达式文本处理指南
  • 如何在部署模型前训练出完美的AI提示词
  • C# 中这几个主流的 ORM(对象关系映射器):Dapper、Entity Framework (EF) Core 和 EF 6
  • 11.《简单的路由重分布基础知识探秘》
  • 硬件:51单片机
  • 为什么需要锁——多线程的数据竞争是怎么引发错误的
  • 系统架构——过度设计
  • YOLOv8改进有效系列大全:从卷积到检测头的百种创新机制解析
  • 【C++上岸】C++常见面试题目--数据结构篇(第十七期)