Android 自定义View-怎么绘制居中文本?

接触过自定义控件的开发者一看,笑了,立马关了网页。但是…你真的知道怎么绘制居中文本吗?

我不会?开玩笑,不就是:X=控件宽度/2 - 文本宽度/2;Y=控件高度/2 + 文本宽度/2

好吧,那我试一下。

1.自定义控件基本步骤

  1. 自定义View的属性
  2. 在View的构造方法中获得我们自定义的属性
  3. #重写onMesure #
  4. 重写onDraw

OK,简单,直接干起来。

1. 自定义View的属性

按照最简单的来,属性有:文本,文本颜色,文本大小。
我们在 /value/attrs.xml 中这么写:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <attr name="text" format="string" />
    <attr name="textColor" format="color" />
    <attr name="textSize" format="dimension" />

    <!-- RTextView -->
    <declare-styleable name="RTextView">
        <attr name="text" />
        <attr name="textColor" />
        <attr name="textSize" />
    </declare-styleable>

</resources>

2. 在View的构造方法中获得我们自定义的属性

    /**
     * 基本属性
     */
    private String mText = "Loading";
    private int mTextColor;
    private int mTextSize;

    /**
     * 画笔,文本绘制范围
     */
    private Rect mBound;
    private Paint mPaint;

    public RTextView(Context context) {
        this(context, null);
    }

    public RTextView(Context context, AttributeSet attrs) {
        super(context, attrs);

        /*
         * 获取基本属性
         */
        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.RTextView);
        mText = a.getString(R.styleable.RTextView_text);
        mTextSize = a.getDimensionPixelSize(R.styleable.RTextView_textSize, 20);
        mTextColor = a.getColor(R.styleable.RTextView_textColor, Color.BLACK);
        a.recycle();

        /*
         * 初始化画笔
         */
        mBound = new Rect();
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Style.FILL);
        mPaint.setTextSize(mTextSize);
        mPaint.getTextBounds(mText, 0, mText.length(), mBound);

    }

代码超级简单,就是在构造方法中获取自定义的属性。

3. #重写onMesure

诶,这个有点不一样哦。简单说一下吧。我们在使用控件的时候一般会设置宽高。
设置类型有:wrap_contentmatch_parent100dp(明确值)

自定义控件时,
如果设置了 明确的宽高(100dp),系统帮我们测量的结果就是我们设置的实际值;
如果是 wrap_content 或者 match_parent 系统帮我们测量的结果就是 match_parent。
所以当设置为 wrap_content 的时候我们需要 重写onMesure 方法重新测量。

重写之前了解 MeasureSpec 的 specMode,一共分为三种类型:
EXACTLY:一般表示设置了 明确值,或者 match_parent
AT_MOST:表示子控件限制在一个最大值内,一般为 wrap_content
UNSPECIFIED:表示子控件像多大就多大,很少使用

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = onMeasureR(0, widthMeasureSpec);
        int height = onMeasureR(1, heightMeasureSpec);
        setMeasuredDimension(width, height);
    }
    /**
     * 计算控件宽高
     * 
     * @param attr属性
     *            [0宽,1高]
     * @param oldMeasure
     * @author Ruffian
     */
    public int onMeasureR(int attr, int oldMeasure) {

        int newSize = 0;
        int mode = MeasureSpec.getMode(oldMeasure);
        int oldSize = MeasureSpec.getSize(oldMeasure);

        switch (mode) {
        case MeasureSpec.EXACTLY:
            newSize = oldSize;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.UNSPECIFIED:

            float value;

            if (attr == 0) {

                value = mBound.width();
                // value = mPaint.measureText(mText);

                // 控件的宽度  + getPaddingLeft() +  getPaddingRight()
                newSize = (int) (getPaddingLeft() + value + getPaddingRight());

            } else if (attr == 1) {

                value = mBound.height();
                // FontMetrics fontMetrics = mPaint.getFontMetrics();
                // value = Math.abs((fontMetrics.descent - fontMetrics.ascent));

                // 控件的高度  + getPaddingTop() +  getPaddingBottom()
                newSize = (int) (getPaddingTop() + value + getPaddingBottom());

            }

            break;
        }

        return newSize;
    }

方法很简单,获取宽高的模式,如果是明确值,或者match_parent,直接获取原始值返回。
如果是 wrap_content,计算宽高:控件的宽高 + 左右(上下)内边距

4. 重写onDraw

好了关键的时候来了,绘制文字。
根据文章开头那些老鸟的方法:X=控件宽度/2 - 文本宽度/2;Y=控件高度/2 + 文本宽度/2

    @Override
    protected void onDraw(Canvas canvas) {

        mPaint.setColor(mTextColor);

        /*
         * 控件宽度/2 - 文字宽度/2
         */
        float startX = getWidth() / 2 - mBound.width() / 2;

        /*
         * 控件高度/2 + 文字高度/2,绘制文字从文字左下角开始,因此"+"
         */
        float startY = getHeight() / 2 + mBound.height() / 2;

        // 绘制文字
        canvas.drawText(mText, startX, startY, mPaint);

        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth(5);
        // 中线,做对比
        canvas.drawLine(0, getHeight() / 2, getWidth(), getHeight() / 2, mPaint);
    }

xml文件调用方式

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="2dp"
        android:layout_marginTop="2dp"
        android:background="#ADD597"
        android:text="@string/text"
        android:textSize="25sp" />

    <cn.r.android.view.RTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="2dp"
        android:layout_marginTop="2dp"
        android:background="#ADD597"
        custom:text="@string/text"
        custom:textColor="#000000"
        custom:textSize="25sp" />

</LinearLayout>

注意:这里宽高设置为wrap_content,并且没有padding

好了,根据那些老鸟的方法写出来了,那么运行一下看看结果。
为了更好的查看效果,加上原生TextView做对比

这里写图片描述

很明显可以看出自定义的宽度小了,高度也不够,宽高文字都不能完整的绘制。

获取很多人看到这个会觉得奇怪,以前没有发现这种效果,因为这里宽高设置为wrap_content,并且没有padding,如果设置了padding或许很难看出这些细微的效果,因此很多开发者以为这就是满意的效果了。

2.绘制水平,垂直居中文本

之前我也以为绘制文本嘛,再简单不过的啦,深入研究一下才发现,哎哟,有文章哦。

OK,说一下解决思路吧。上图所示,宽高都出现了问题,都偏小了。这里宽度问题比较容易解决,高度才比较麻烦。

2.1宽度偏小

宽度偏小是因为文字测量出现了误差,
原始方式,这是一种粗略的文字宽度计算

value = mBound.width();

改进,这是比较精确的测量文字宽度的方式

value = mPaint.measureText(mText);

开发者可以自行打印对比一下 mBound.width(); 和 mPaint.measureText(mText); 的值。

这里写图片描述

上图中,第1个是原生TextView,第2个是修改的过的,第三个是没有修改的,明显看到宽度已经和原生一样,而且最后一个文字也完整绘制出来了。第三个可以对比

2.2高度偏小

高度偏小就比较麻烦了。不是一行代码可以解决的了
先了解一下Android是怎么样绘制文字的,这里涉及到几个概念,分别是文本的top,bottom,ascent,descent,baseline。
看下面的图(摘自网络):

这里写图片描述

解释一下这张图片。(摘自网络)
Baseline是基线,在Android中,文字的绘制都是从Baseline处开始的,Baseline往上至字符“最高处”的距离我们称之为ascent(上坡度),Baseline往下至字符“最低处”的距离我们称之为descent(下坡度);

 leading(行间距)则表示上一行字符的descent到该行字符的ascent之间的距离;
 
 top和bottom文档描述地很模糊,其实这里我们可以借鉴一下TextView对文本的绘制,TextView在绘制文本的时候总会在文本的最外层留出一些内边距,为什么要这样做?因为TextView在绘制文本的时候考虑到了类似读音符号,下图中的A上面的符号就是一个拉丁文的类似读音符号的东西:

这里写图片描述

Baseline是基线,Baseline以上是负值,以下是正值,因此 ascent,top是负值, descent和bottom是正值。
OK,知道了这几个概念之后就开始想想要怎么修改了。

我们先修改高度偏小的问题
原始代码,

value = mBound.height();

修改后代码

FontMetrics fontMetrics = mPaint.getFontMetrics();
value = Math.abs((fontMetrics.bottom - fontMetrics.top));

结合图一,bottom和top相减的绝对值就是view的高度height。注意:Baseline以上是负值,以下是正值

这里写图片描述

OK,高度和宽度大小和原生的大小一样了,那么现在怎么使得文字垂直居中呢?

查阅了网上资料和测试了多次的结果得出一个计算 Y 值的计算公式:

FontMetricsInt fm = mPaint.getFontMetricsInt();

int startY = getHeight() / 2 - fm.descent + (fm.bottom - fm.top) / 2;

int startY = getHeight() / 2 - fm.descent + (fm.descent - fm.ascent)/ 2;

getHeight():控件的高度

getHeight()/2-fm.descent:意思是将整个文字区域抬高至控件的1/2

+ (fm.bottom - fm.top) / 2:(fm.bottom - fm.top)其实就是文本的高度,意思就是将文本下沉文本高度的一半

  • 执行:getHeight()/2-fm.descent , 将整个文字区域抬高至控件的1/2

这里写图片描述

  • 执行: + (fm.bottom - fm.top) / 2 , 将文本下沉文本高度的一半

这里写图片描述

为什么是:(fm.bottom - fm.top) ;而不是:(fm.descent - fm.ascent)

这里写图片描述

第一张是原生TextView,第二张是(fm.bottom - fm.top),第三张是(fm.descent - fm.ascent)。

从效果图看,第三种才是真正意义上的居中,不是吗?但是第二种是和原生TextView最接近的,为什么呢?经过测试你会知道,如果单纯是汉字或者数字第三种的效果或者会比较好,但是如果其他的语言,就比如上图的英文来看,第二种是比较好的。不能排除其他国家的语言,或者一些带音标的拼音之类的呢?

所以根据实际需求来确定使用哪一个,推荐第二种:(fm.bottom - fm.top)

源码下载

本页内容版权归属为原作者,如有侵犯您的权益,请通知我们删除。
首先,自定义控件分为三类: 自定义的组合控件 继承View的自定义控件 继承ViewGroup的自定义控件 在这里,我要写的是第二种,也就是继承自View的自定义控件,第一种自定义的组合控件,我已经写过了,可以在我的博客中可以找到 现在来看一下继承View的自定义控件 首先,需要写一个类继承自View,那么,它也有三个构造方法,有一个参数的构造方法实在代码中new这个自定义控件时被调用;有两个参数的构造方法是在布局中使用这个自定义控件的时候调用,有三个参数的构造方法,实在使用到这个自定义控件的样式时被调用

WindowManager的分析 - 2016-07-22 18:07:46

一、Window和WindowManager Window:表示一个窗口,从下面Window的源码中可以看出它有且只有一个实现类PhoneWindow。 The only existing implementation of this abstract class is * android.policy.PhoneWindow, which you should instantiate when needing a * Window. WindowManager:它是系统提供我们操作Window的一个接口

MTK6580-Psensor hal层驱动分析 - 2016-07-22 18:07:25

一、HAL 层Sensor 流程         Hal 就是对Linux内核驱动程序的封装,向上提供接口,屏蔽低层的实现细节。也就是说,把对硬件的支持分成了两层,一层放在用户空间(User Space),一层放在内核空间(Kernel Space),其中,硬件抽象层运行在用户空间,而Linux内核驱动程序运行在内核空间。 Sensor 打开设备时序图:  其中SensorDevice 属于 JNI 层,与 HAL 进行通信的接口 ; 在 JNI 层调用了 HAL 层的 open_sensors() 方法
ART世界探险(2) - 从java byte code说起 Dalvik时代,如果不做JIT的话,只需要了解java字节码和Dalivk的字节码就够了。但是,到了ART时代,我们可能还要至少学习两种新东西:一个是编译后端的IR中间代码。比如,我们假如使用LLVM做为编译后端的话,需要做从dex到LLVM IR的转换工作。这个IR可能还不只一层,比如分中层的MIR和底层的LIR。 最后,我们还得了解机器指令。仅就ARM来说,现在是64位时代了,我们需要了解的就是AArch64和AArch32两种状态下的A

使用AndFix实现Android热修复 - 2016-07-22 18:07:19

AndFix Github: https://github.com/alibaba/AndFix AndFix介绍 AndFix是一个Android App的在线热补丁框架。使用此框架,我们能够在不重复发版的情况下,在线修改App中的Bug。AndFix就是 “Android Hot-Fix”的缩写。  就目前来说,AndFix支持Android 2.3到6.0版本,并且支持arm 与 X86系统架构的设备。完美支持Dalvik与ART的Runtime。  AndFix 的补丁文件是以 .apatch 结

[置顶] VR学习 - 2016-07-22 18:07:15

VR学习 由于到去的公司从事VR这方面的开发,为了不打无准备之战,因此学习了一下Google的CardBoard VR实现。(仅仅是表皮,只是看Demo但是还是值得花点功夫看看) 效果图 这里的实现效果其实是,使用到了手机的传感器,陀螺仪(具体的往后面看) 学习Demo(再往深研究) 首先我们先看一下Demo中清单文件的权限和Activity的要求。 //请求网络权限 uses-permission android:name = "android.permission.INTERNET" / //手机NF

语音识别 - 2016-07-22 18:07:58

我使用 Speech Recognizer Intent 来获取 User Input ,然后再翻译成文本。但是我想让intent 连续的获取 user input 然后翻译成文本,来看用户是否说了某个词。现在的代码能实现,但是每次程序开始侦听输入时,电话就会发出短期的警笛声,准备输入。 有什么方法来删除已经播放过的又一次播放的声音? 代码: import java.util.ArrayList;import android.app.Activity;import android.content.Inte
前面已经分析了Android应用程序窗口View的的测量,布局过程,接下来分析View的draw过程. 在 frameworks/base/core/java/android/view/ ViewRootImpl.java中的 performTraversals()函数里调用 performLayout()函数进行布局之后,接着会调用 performDraw()函数进行绘制,现在就从这个函数开始分析 第一步: performDraw() 在 frameworks/base/core/java/androi
介绍 本篇主要是对个人对LinphoneManger类的理解及对上面的注释,这是对linphone研究的一个开始. 会慢慢对linphone逐步分析, 随着时间的推进, 我会对linphone有进一步的了解,希望希望了解的同学能跟上我的脚步. 简介 LinphoneManager类是Linphone的主要操作管理类. 主要功能: 官方 /** * * Manager of the low level LibLinphone stuff. br / * Including: ul * li Starting
大概在2015年10月底,QQ空间发了一篇叫《安卓App热补丁动态修复技术介绍》的文章,文章中提到为了能让Class进行热修复,其中一个条件就是防止类被打上CLASS_ISPREVERIFIED标记,具体的做法便是让一个Dex引用另一个Dex(hack.apk)中的空类(为了让业务无感知,需要在编译时动态注入字节码),并且在应用程序Application类起来的时候要加载这个hack.apk。也就是说最多需要进行两次反射,即加载hack.apk的时候需要进行一次反射操作,将hack.apk加入到DexEl