重学 Android 自定义 View 系列:动手实现专属 TextView

news/2024/11/14 4:44:26/

前言

前面一篇介绍了自定义View的基础概念(皮毛),接下来全部是自定义View实战,让我们一起开启自定义View之旅吧!

1. 实现目标

本篇将实现一个自定义的TextView,通过自定义属性让我们可以配置文本内容、颜色、字体大小。主要是掌握自定义View中画文字的基础概念。

  • attrs.xml 中定义自定义属性
  • 在自定义View中解析自定义属性
  • 绘制文本
  • 测量控件宽高

以下是完成效果图:

效果1:不带Padding
在这里插入图片描述
效果2:带Padding

在这里插入图片描述
看似很简单,其实有一点绕,先来看一张图。

在这里插入图片描述

首先我们需要知道一点,文字的绘制是从左下角开始的,并不是 文字左上角的坐标,这一点与其他自定义view的绘制会有一些不同,因为文字的绘制是以基线为基础的,就是上图的Baseline,当自定义Textview时,我们要么拿到确定的宽高,要么是根据文本的长度和大小去自适应宽高,不管怎样我们得到的都是一个矩形。

而我们要绘制文本,最重要的是确认基线坐标,那么基线坐标怎么获取呢?这时候就要用到 Paint.FontMetricsInt了。

Paint.FontMetricsInt 的基本概念

Paint.FontMetricsIntPaint 类中的一个内部类,用于提供字体的度量信息。它的字段主要表示从基线(baseline)到不同字符边界的距离,通常用来计算文本在垂直方向上的绘制位置。

主要字段如下:

  • top:从基线到字符最高点的距离,通常是负值。
  • ascent:从基线到字符实际顶部的距离,也是负值,但通常比 top 小。
  • descent:从基线到字符实际底部的距离,通常是正值。
  • bottom:从基线到字符最低点的距离,一般用于字符尾部的间距,是正值。
  • leading:字符之间的间距,通常用于行间距的控制。它的值可能是正的、负的或者零。

这些字段的单位通常是像素,且它们的值相对于基线来计算,如所示:

    top    --  文本框架的最高点ascent --  字符的最高点(字体上方留白)baseline -- 基线,字符绘制的参考线descent --  字符的最低点(字体下方留白)bottom  --  文本框架的最低点

这里主要用到的是 top 和 bottom。计算基线的公式如下:

     float dy = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;float baseLine = getHeight() / 2 + dy;

同:

float baseLine = getHeight() / 2f - (textPaint.descent() + textPaint.ascent()) / 2f;

getHeight() / 2是控件高度的一半,(bottom - top) / 2 是字体中心与顶部的距离,用来计算从基线到中心点的位移dy。减去 bottom 是为了调整基线,使得绘制效果居中。

因为 descent 绝对值小于 ascent 所以 (textPaint.descent() + textPaint.ascent()) / 2f整体是个负值,并且为 descentascent 中点坐标,又是getHeight() / 2baseline 的偏移量,所以上面方法等同,是不是解释的不太清楚,拿笔画个图就理解了,下面那个公式确实不太好理解。下图就是草图:

在这里插入图片描述
其中 -3 位置为ascent ,2 位置为descent

(-3 + 2) / 2 = -0.5 这下理解了吧。

OK!了解了这些基础概念后,你就能画一个很正的文本了。

2. 定义自定义属性


res/values/attrs.xml 中,我们定义了三个自定义属性:xText(文本内容)、xTextColor(文本颜色)、xTextSize(文本大小)。

<declare-styleable name="MyTextView"><attr name="xText" format="string"/><attr name="xTextColor" format="color"/><attr name="xTextSize" format="dimension"/>
</declare-styleable>

3. 自定义TextView的实现


  • 解析自定义属性
  • 初始化画笔
  • 测量View的宽高(onMeasure)
  • 绘制文本(onDraw)

onLayout 呢?,不是说好三步走的吗?这是因为 onLayout 方法的主要作用是对子View(即子元素)进行布局,而 MyTextView 继承自 View,它本身就是一个单一的视图控件,没有任何子元素,因此无需重写 onLayout。

大部分自定义View都是单View的,一般不需要自定义onLayout

4. 完整代码实现

public class MyTextView extends View {// 创建画笔对象private Paint mPaint;// 文本内容、字体大小和颜色的默认值private String mText = "Hello!";private int mTextColor = Color.BLACK;private float mTextSize = 50f;public MyTextView(Context context) {this(context, null);}public MyTextView(Context context, AttributeSet attrs) {super(context, attrs);if (attrs != null) {// 获取所有自定义属性TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyTextView);mText = typedArray.getString(R.styleable.MyTextView_xText);mTextColor = typedArray.getColor(R.styleable.MyTextView_xTextColor, mTextColor);mTextSize = typedArray.getDimension(R.styleable.MyTextView_xTextSize, spToPx(mTextSize));//回收typedArray.recycle();}init();}// 初始化画笔private void init() {mPaint = new Paint();mPaint.setAntiAlias(true);  // 启用抗锯齿mPaint.setColor(mTextColor); // 设置文本颜色mPaint.setTextSize(mTextSize); // 设置文本大小}// 测量控件的宽高@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {// 获取父容器传递的宽度和高度的测量模式(MeasureSpec的模式和尺寸)int widthMode = MeasureSpec.getMode(widthMeasureSpec);int widthSize = MeasureSpec.getSize(widthMeasureSpec);int heightMode = MeasureSpec.getMode(heightMeasureSpec);int heightSize = MeasureSpec.getSize(heightMeasureSpec);// 1. 确定的值,这个时候不需要计算,给多少就是多少// 获取文本的矩形边界Rect bounds = new Rect();mPaint.getTextBounds(mText, 0, mText.length(), bounds);// 计算宽度int width = widthSize;// 2.给的是wrap_content 需要计算if (widthMode == MeasureSpec.AT_MOST) {width = bounds.width() + getPaddingLeft() + getPaddingRight();}// 计算高度int height = heightSize;if (heightMode == MeasureSpec.AT_MOST) {height = bounds.height() + getPaddingTop() + getPaddingBottom();}// 设置控件宽高setMeasuredDimension(width, height);}private float spToPx(float sp) {return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());}@Overrideprotected void onLayout(boolean changed, int left, int top, int right, int bottom) {super.onLayout(changed, left, top, right, bottom);}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);// 绘制文本// x 开始位置 0// y 基线 baseLinePaint.FontMetrics fontMetrics = mPaint.getFontMetrics();// top是一个负值 ,bottom是一个正值// dy 代表 盖度一半到基线的距离float dy = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;float baseLine = getHeight() / 2 + dy;int x = getPaddingLeft();float y = (getHeight() - fontMetrics.bottom + fontMetrics.top) / 2;canvas.drawText(mText, x, baseLine, mPaint);}
}

5. 使用:


    <com.example.quickdev.test.MyTextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@color/gray"app:xText="河南Xaye"android:padding="10dp"app:xTextColor="#ff0000"app:xTextSize="20dp"/>

显示结果就是开篇的图片。

6. 最后


知识点不多,主要是两点:1. 实现wrap_content的支持,2. 使用Paint绘制文本内容,调整基线位置以实现文本居中。


http://www.ppmy.cn/news/1546837.html

相关文章

命令行工具进阶指南

&#x1f680; 命令行工具进阶指南&#xff1a;Git、Shell与效率工具的进阶之路 掌握命令行工具&#xff0c;让你的开发效率突飞猛进。本文将深入探讨 Git 高级技巧、Shell 脚本自动化以及各种效率倍增的 CLI 工具。 &#x1f4d1; 目录 Git 高级技巧与工作流Shell 脚本自动化…

Python——飞机大战

以下是一个简单的用Python编写的飞机大战游戏的源代码&#xff1a; import pygame import random# 初始化游戏 pygame.init()# 设置游戏窗口的尺寸 screen_width 480 screen_height 640 screen pygame.display.set_mode((screen_width, screen_height))# 设置游戏标题 pyga…

Prompt Engineering介绍

什么是Prompt Engineering&#xff1f; 近年来&#xff0c;大语言模型&#xff08;LLM&#xff09;发展迅速&#xff0c;成为自然语言处理领域的重要技术。除了OpenAI的GPT系列、Google的PaLM&#xff08;Pathways Language Model&#xff09;和Bard&#xff0c;国内也涌现出多…

ODOO学习笔记(7):模块化架构(按需安装)

一、Odoo模块化架构概述 Odoo是一个功能强大的企业资源规划&#xff08;ERP&#xff09;系统&#xff0c;其模块化架构是它的核心优势之一。这种架构允许系统通过添加、移除或修改不同的模块来灵活地适应企业的各种业务需求。 核心模块与自定义模块&#xff1a; Odoo本身带有一…

The Input data type is inconsistent with defined schema

INFO:__main__:上传音频文件 梦阳... INFO:__main__:加载音频文件 梦阳&#xff0c;采样率: 48000, 信号形状: torch.Size([2, 719872]) INFO:speechbrain.utils.parameter_transfer:Loading pretrained files for: embedding_model, mean_var_norm_emb, classifier, label_enc…

Ceph 中PG与PGP的概述

在Ceph分布式存储系统中&#xff0c;PG&#xff08;Placement Group&#xff09;和PGP&#xff08;Placement Group for Placement purpose&#xff09;是两个至关重要的概念&#xff0c;它们共同决定了数据在集群中的分布和复制方式。以下是关于Ceph中PG和PGP关系的详细解释&a…

从零创建vue+elementui+sass+three.js项目

初始化&#xff1a; vue init webpack projectnamecd projectnamenpm install支持sass: npm install sass --save-dev npm install sass-loader7.1.0 --save-dev npm install node-sass4.12.0 --save-devbuild/webpack.base.conf.js添加 rules: [...,{test: /\.scss$/,loade…

【独立同分布】

独立同分布&#xff08;independent and identically distributed&#xff0c;i.i.d.&#xff09;在概率统计理论中&#xff0c;指随机过程中&#xff0c;任何时刻的取值都为随机变量&#xff0c;如果这些随机变量服从同一分布&#xff0c;并且互相独立&#xff0c;那么这些随机…