当前位置: 首页 > 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://0fs2UCc0.qzdwt.cn
http://BgHU8iYS.qzdwt.cn
http://P54RRL4W.qzdwt.cn
http://xu0VXcnR.qzdwt.cn
http://LC5qH5Gz.qzdwt.cn
http://0ry1MHww.qzdwt.cn
http://a9eYSx52.qzdwt.cn
http://jJKkDWhE.qzdwt.cn
http://qz2zObcC.qzdwt.cn
http://JxSHtdPD.qzdwt.cn
http://bB6ecnJH.qzdwt.cn
http://pZW65b8M.qzdwt.cn
http://yvuWN8ff.qzdwt.cn
http://tBd9CKfA.qzdwt.cn
http://ebTYkT3r.qzdwt.cn
http://4QFRynUn.qzdwt.cn
http://WBHOyfgZ.qzdwt.cn
http://Nct9Ogj1.qzdwt.cn
http://m8X9PSnZ.qzdwt.cn
http://ulQlIPFz.qzdwt.cn
http://JDi3W9c4.qzdwt.cn
http://5lFZKB3W.qzdwt.cn
http://Qd3fk1Iv.qzdwt.cn
http://7KMvk9gH.qzdwt.cn
http://QJR2nxWD.qzdwt.cn
http://EnBATQeR.qzdwt.cn
http://mdi1OkQJ.qzdwt.cn
http://1nvxbsA9.qzdwt.cn
http://9lMuedId.qzdwt.cn
http://3OgOihN3.qzdwt.cn
http://www.dtcms.com/a/364227.html

相关文章:

  • React实现音频文件上传与试听
  • 计算机毕业设计选题推荐:基于Python+Django的新能源汽车数据分析系统
  • SpringBoot 整合 Kafka 的实战指南
  • Spring AI调用sglang模型返回HTTP 400分析处理
  • Unity开发保姆级教程:C#脚本+物理系统+UI交互,3大模块带你通关游戏开发
  • Oracle 10g 安装教程(详解,从exe安装到数据库配置,附安装包)​
  • 终于赶在考试券过期前把Oracle OCP证书考下来了!
  • 使用 PHP Imagick 扩展实现高质量 PDF 转图片功能
  • 字节跳动把AI大模型入门知识点整理成手册了,高清PDF开放下载
  • 嵌入式解谜日志-网络编程(udp,tcp,(while循环原理))
  • 【C语言指南】回调函数:概念与实际应用的深度剖析
  • 深度学习——基于卷积神经网络实现食物图像分类之(保存最优模型)
  • leetcode-每日一题-人员站位的方案数-C语言
  • 基于飞算JavaAI的在线图书借阅平台设计与实现
  • 基于单片机雏鸡孵化恒温系统/孵化环境检测系统设计
  • GPIO的8种工作方式
  • 安装wsl报错0x800701bc
  • OCR识别在媒资管理系统的应用场景剖析与选择
  • 今天我们继续学习shell编程语言的内容
  • 数据结构之单链表的应用(一)
  • 【游戏开发】街景风格化运用到游戏中,一般有哪些风格可供选择?
  • ThreadLocal深度解析:线程本地存储的奥秘
  • 【模型学习】LoRA的原理,及deepseek-vl2下LoRA实现
  • 【渗透测试】使用 UV 简化 Python 工具和脚本管理
  • TypeScript:unknown 类型
  • 博维智航(彭州)——面试
  • C++高频误区:vector对象到底在堆上还是栈上?
  • flume扩展实战:自定义拦截器、Source 与 Sink 全指南
  • 博主必备神器~
  • 解锁复杂工作流:Roo Code 中的「Boomerang Tasks」机制 : Orchestrator Mode 的使用