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

前言

前面一篇介绍了自定义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;

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

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); // 设置文本大小
    }

    // 测量控件的宽高
    @Override
    protected 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());
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

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

        // 绘制文本
        // x 开始位置 0
        // y 基线 baseLine

        Paint.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.MyTextView
        android: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绘制文本内容,调整基线位置以实现文本居中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/913406.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

多用户商城系统的功能及设计和开发

多用户商城系统的功能及设计与开发&#xff08;基于 PHP MySQL&#xff09; 在现代电子商务平台的开发中&#xff0c;PHP MySQL 是一对非常流行且高效的技术栈。PHP作为服务器端脚本语言&#xff0c;结合MySQL数据库&#xff0c;可以高效地处理多用户商城系统的各种需求。本…

丹摩征文活动|快速上手 CogVideoX-2b:智谱清影 6 秒视频生成部署教程

文章目录 一、生成视频效果 二、CogVideoX 技术新起点三、CogVideoX 上手部署3.1 创建丹摩实例3.2 配置环境和依赖3.3 模型与配置文件3.4 运行3.5 问题与处理方法 四、CogVideoX-2b 用创新点燃未来 一、生成视频效果 A street artist, clad in a worn-out denim jacket and a c…

实现 think/queue 日志分离

当我们使用think/queue包含了比较多的不同队列,日志会写到runtime/log目录下,合并写入的,不好排查问题,我们遇到一个比较严重的就是用了不同用户来执行,权限冲突了,导致部分队列执行不了. 为了解决以上问题,本来希望通过Log::init设置不同日志路径的,但是本地测试没生效,于是用…

Ubuntu24.04安装Perforce服务

安装 参考链接:https://www.perforce.com/manuals/p4sag/Content/P4SAG/install.linux.packages.install.html Perforce是一款收费的版本控制管理工具,当然其中也有一些免费的教学版本,应需要下载。 下载网址: https://www.perforce.com/downloads/helix-core-p4d安装前…

使用 GitHub Actions 部署到开发服务器的详细指南

使用 GitHub Actions 部署到开发服务器的详细指南 在本篇博客中&#xff0c;我们将介绍如何使用 GitHub Actions 实现自动化部署&#xff0c;将代码从 GitHub 仓库的 dev 分支自动部署到开发服务器。通过这种方式&#xff0c;可以确保每次在 dev 分支推送代码时&#xff0c;服…

Logrus入门

Logrus入门 1. 下载 go get github.com/sirupsen/logrus2. logrus常用方法 logrus.Debugln("Debugln") logrus.Infoln("Infoln") logrus.Warnln("Warnln") logrus.Errorln("Errorln") logrus.Println("Println")// 输出如…

告别重启大法,CPU飙高问题如何排查详细教程以及解决方案

文章目录 0 前言1.确定问题进程2.获取线程信息3.转换线程ID为十六进制4.获取线程堆栈5.分析代码6.性能分析工具7. 查看GC日志8.检查系统资源总结 0 前言 本篇是本人认为最实用的一篇&#xff0c;在日常开发运维工作中&#xff0c;经常遇到CPU较高的情况&#xff0c;一开始时还不…

在 Jupyter Notebook 中使用 Matplotlib 进行交互式可视化的教程

在 Jupyter Notebook 中使用 Matplotlib 进行交互式可视化的教程 引言 数据可视化是数据分析的重要组成部分&#xff0c;能够帮助我们更直观地理解数据。Matplotlib 是 Python 中最流行的绘图库之一&#xff0c;而 Jupyter Notebook 则是进行数据分析和可视化的理想环境。本文…

数据库SQL——什么是实体-联系模型(E-R模型)?

目录 什么是实体-联系模型&#xff1f; 1.实体集 2.联系集 3.映射基数 一对一&#xff08;1:1&#xff09; 一对多&#xff08;1:n&#xff09; 多对一&#xff08;n:1&#xff09; 多对多&#xff08;m:n&#xff09; 全部参与&#xff1a; 4.主码 弱实体集&#xf…

机器学习4_支持向量机_核函数——MOOC

目录 核函数的定义 核函数以及低维到高维的映射 的相互关系 例1&#xff1a;已知 求 K 例2&#xff1a;已知核函数 K 求 映射 的例子 核函数 K 求 映射 是一一对应的关系 支持向量机优化问题 K 满足交换性和半正定性 内积的形式 例如&#xff1a;可以证明 核函数…

LRU-LFU缓存算法

文章目录 缓存算法LRU缓存算法LFU缓存算法定义实现方法一&#xff1a;哈希表平衡二叉树方法二&#xff1a;双哈希表哈希链表方法三&#xff1a;双哈希表 缓存算法 LRU缓存算法 https://labuladong.online/algo/data-structure/lru-cache/ LRU&#xff08;Least Recently Use…

斯坦福泡茶机器人DexCap源码解析:涵盖收集数据、处理数据、模型训练三大阶段

前言 因为我司「七月在线」关于dexcap的复现/优化接近尾声了&#xff0c;故准备把dexcap的源码也分析下。​下周则分析下iDP3的源码——为队伍「iDP3人形的复现/优化」助力 最开始&#xff0c;dexcap的源码分析属于此文《DexCap——斯坦福李飞飞团队泡茶机器人&#xff1a;带…

DICOM标准:DICOM医学影像中的覆盖层(Overlay)概念详解

引言 DICOM&#xff08;数字成像和通信医学&#xff09;标准在医学影像的存储、传输和交换中起着关键作用。覆盖层&#xff08;Overlay&#xff09;作为DICOM标准中的一个重要组成部分&#xff0c;用于在医学影像上叠加图形信息&#xff0c;如注释、标记、测量结果等。本文将深…

Windows搭建流媒体服务并使用ffmpeg推流播放rtsp和rtmp流

文章目录 搭建流媒体服务方式一安装mediamtx启动meidamtx关闭meidamtx 方式二安装ZLMediaKit启动ZLMediaKit关闭ZLMediaKit 安装FFmpeg进行推流使用FFmpeg进行rtmp推流使用VLC播放rtmp流停止FFmpeg的rtmp推流使用FFmpeg进行rtsp推流使用VLC播放rtmp流停止FFmpeg的rtsp推流 本文…

[ Linux 命令基础 5 ] Linux 命令详解-网络管理命令

&#x1f36c; 博主介绍 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 _PowerShell &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 &#x1f389;点赞➕评论➕收藏 养成习…

深入浅出WebSocket(实践聊天室demo)

文章目录 什么是WebSocket?WebSocket连接过程WebSocket与Http的区别重连机制完整代码使用方法心跳机制实现聊天室demo(基于Socket.io)参考文章、视频小广告~什么是WebSocket? WebSocket 是一种在单个TCP连接上进行全双工通信的协议(计算机网络应用层的协议) 在 WebSocket A…

时序预测 | 改进图卷积+informer时间序列预测,pytorch架构

时序预测 | 改进图卷积informer时间序列预测&#xff0c;pytorch架构 目录 时序预测 | 改进图卷积informer时间序列预测&#xff0c;pytorch架构预测效果基本介绍参考资料 预测效果 基本介绍 改进图卷积informer时间序列预测代码 CTR-GC卷积,informer&#xff0c;CTR-GC 图卷积…

vue+Leaflet.PM插件实现创建和编辑几何图形(点、线、面、圆等)

场景 VueLeaflet实现加载OSM显示地图&#xff1a;https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/122317394在上面加载显示OSM的基础上&#xff0c;使用Leaflet.pm插件实现在页面上绘制、编辑、剪切、移动几何元素。Leaflet.pm插件 用于创建和编辑几何图层的插件可…

网站架构知识之Ansible进阶2(day023)

1.include文件 应用场景: 1个ansible剧本内容过多,涉及到多个play(- host:web),可读性变弱&#xff0c;不方便调试。 于是人们想出把单个大的剧本拆分为多个小的剧本&#xff0c; 多个小的剧本可以通过include功能合并使用。 使用方法&#xff0c;书写好对应的剧本文件&#…

订单日记助力“实峰科技”提升业务效率

感谢北京实峰科技有限公司选择使用订单日记&#xff01; 北京实峰科技有限公司&#xff0c;成立于2022年&#xff0c;位于北京市石景区&#xff0c;是一家以从事生产、销售微特电机、输配电及控制设备等业务为主的企业。 在业务不断壮大的过程中&#xff0c;想使用一种既能提…