扣细节之Android的TextView是如何绘制的

TextView作为Android系统最基本的view,担任着我们文字展示最重要最基础的任务,如何在Android中又快又完整的展示文字内容,都有赖于这个view来实现,细想一下:TextView是如何把文字绘制到屏幕手机屏幕上的?接下来跟着我一起来深究一下这里面的东西,抠一抠TextView的一些小细节,你会发现,原来一个小小的TextView,也包含着开发者的大智慧。

AndroidView的绘制三板斧

说到AndroidView的绘制,那必然离不开这三个方法:onMeasure,onLayout以及onDraw,那我们就从这三个方法入手,看一下源码是怎么写的,以下的代码基于Android-sdk version 31

onMeasure解析

ini 复制代码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    ....
    ```
if (widthMode == MeasureSpec.EXACTLY) {
    // Parent has told us how big to be. So be it.
    width = widthSize;
} else {
    if (mLayout != null && mEllipsize == null) {
        des = desired(mLayout);
    }

    if (des < 0) {
        boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
        if (boring != null) {
            mBoring = boring;
        }
    } else {
        fromexisting = true;
    }

    if (boring == null || boring == UNKNOWN_BORING) {
        if (des < 0) {
            des = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mTransformed, 0,
                    mTransformed.length(), mTextPaint, mTextDir, widthLimit));
        }
        width = des;
    } else {
        width = boring.width;
    }

先解析出设置的mode和size,可以看到是判断mode属于哪种类型,从而确定范围;但这里有个BoringLayout可以关注一下,我们看看BoringLayout的注释

scala 复制代码
/**
 * A BoringLayout is a very simple Layout implementation for text that
 * fits on a single line and is all left-to-right characters.
 * You will probably never want to make one of these yourself;
 * if you do, be sure to call {@link #isBoring} first to make sure
 * the text meets the criteria.
 * <p>This class is used by widgets to control text layout. You should not need
 * to use this class directly unless you are implementing your own widget
 * or custom display object, in which case
 * you are encouraged to use a Layout instead of calling
 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
 *  Canvas.drawText()} directly.</p>
 */
public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback {

直译过来就是它是用于包裹单行且只有左到右排列的文字view,从而提升简单textView性能。 BoringLayout继承至一个抽象类Layout,我们知道一个View它是有 文字属性,画笔,Padding,排版direction,ellipsEnd 或 ellipsStart 等属性,这些都是由Layout来控制,那么它是怎么控制的呢?这里先卖个关子,是由span来控制的,等下面再来看具体瞧瞧这里的逻辑。 那么,既然有BoringLayout,那么是不是有ExcitingLayout?哈哈哈哈,那倒没有,但是有为了动态调整文字View的DynamicLayout,现在来总结一下第一个知识点:TextView的文字属性等都是由Layout来控制的

接下来就基本上是测量整个TextView width 和 height,这里不再赘述 之后就开始传建Layout

scss 复制代码
if (mLayout == null) {
    makeNewLayout(want, hintWant, boring, hintBoring,
                  width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
} else {
    final boolean layoutChanged = (mLayout.getWidth() != want) || (hintWidth != hintWant)
            || (mLayout.getEllipsizedWidth()
                    != width - getCompoundPaddingLeft() - getCompoundPaddingRight());

    final boolean widthChanged = (mHint == null) && (mEllipsize == null)
            && (want > mLayout.getWidth())
            && (mLayout instanceof BoringLayout
                    || (fromexisting && des >= 0 && des <= want));

    final boolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum);

onLayout解析

接下来我们来看onLayout方法。从mDeferScroll可以看出,首先是判断当前区域的滑动,这块逻辑可以之后再去细看;然后是antoSizeText方法,我们知道在AppcompatTextView里面,有自动根据长度调整字体大小的逻辑,就是用于autoSizeText来判断的。

java 复制代码
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    if (mDeferScroll >= 0) {
        int curs = mDeferScroll;
        mDeferScroll = -1;
        bringPointIntoView(Math.min(curs, mText.length()));
    }
    // Call auto-size after the width and height have been calculated.
    autoSizeText();
}

接下来看看 autoSizeText的方法内部:

scss 复制代码
private void autoSizeText() {
        if (!isAutoSizeEnabled()) {
            return;
        }

        if (mNeedsAutoSizeText) {
            if (getMeasuredWidth() <= 0 || getMeasuredHeight() <= 0) {
                return;
            }

            final int availableWidth = mHorizontallyScrolling
                    ? VERY_WIDE
                    : getMeasuredWidth() - getTotalPaddingLeft() - getTotalPaddingRight();
            final int availableHeight = getMeasuredHeight() - getExtendedPaddingBottom()
                    - getExtendedPaddingTop();

            if (availableWidth <= 0 || availableHeight <= 0) {
                return;
            }

            synchronized (TEMP_RECTF) {
                TEMP_RECTF.setEmpty();
                TEMP_RECTF.right = availableWidth;
                TEMP_RECTF.bottom = availableHeight;
                final float optimalTextSize = findLargestTextSizeWhichFits(TEMP_RECTF);

                if (optimalTextSize != getTextSize()) {
                    setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, optimalTextSize,
                            false /* shouldRequestLayout */);

                    makeNewLayout(availableWidth, 0 /* hintWidth */, UNKNOWN_BORING, UNKNOWN_BORING,
                            mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
                            false /* bringIntoView */);
                }
            }
        }
        // Always try to auto-size if enabled. Functions that do not want to trigger auto-sizing
        // after the next layout pass should set this to false.
        mNeedsAutoSizeText = true;
    }

这段代码是用于自动调整视图中文本的大小。让我们逐步分析它的功能: 1.检测了是否开启的自动开启文本,检查当前区域的measureWidth和measureHeight是否测量过,还有当前文本的可用宽度和高度是否不为0,条件不符合的话都是直接不绘制; 2.接下来是一个通用做法:设置一个TEMP_RECTF,用于保存可用宽度和高度,然后通过findLargestTextSizeWhichFits来计算出当前文案这个可用区域内的实际绘制的文本大小; 3.如果最佳文本大小与当前文本大小不同,则更新文本大小,并使用新大小请求新的布局(makeNewLayout); 4.最后,将 mNeedsAutoSizeText 设置为 true,这个操作应该是为了确保一直是自动调整大小的逻辑,防止之后的绘制出现被置为false的异常情况出现(感觉是为了某种情况兜底,但不太清楚是啥情况...)

到了这里,想必你也看到我们标注的findLargestTextSizeWhichFits的方法,这里是为了最快找到在minTextSize和maxTextSize中的最适合的一个textsize,没错,这里我们算法的用武之处不就来了吗? 在一个有序区间内最快找到合适的textSize大小,一看就知道是用二分查找啦(原谅我是看了源码才知道的:))。

ini 复制代码
/**
 * Performs a binary search to find the largest text size that will still fit within the size
 * available to this view.
 */
private int findLargestTextSizeWhichFits(RectF availableSpace) {
    final int sizesCount = mAutoSizeTextSizesInPx.length;
    if (sizesCount == 0) {
        throw new IllegalStateException("No available text sizes to choose from.");
    }

    int bestSizeIndex = 0;
    int lowIndex = bestSizeIndex + 1;
    int highIndex = sizesCount - 1;
    int sizeToTryIndex;
    while (lowIndex <= highIndex) {
        sizeToTryIndex = (lowIndex + highIndex) / 2;
        if (suggestedSizeFitsInSpace(mAutoSizeTextSizesInPx[sizeToTryIndex], availableSpace)) {
            bestSizeIndex = lowIndex;
            lowIndex = sizeToTryIndex + 1;
        } else {
            highIndex = sizeToTryIndex - 1;
            bestSizeIndex = highIndex;
        }
    }

    return mAutoSizeTextSizesInPx[bestSizeIndex];
}

这里就是用二分逼近,每次都用区间中值去找到匹配看是否为最合适的textSize。

这里suggestedSizeFitsInSpace方法用到的检测逻辑,和我们前边提到的BoringLayout也是一样的做法,只是它用到的是一个叫StaticLayout 的类,我们查查chatGPT就知道它和BoringLayout的区别了(偷懒神器chatGPT): StaticLayoutBoringLayout 是 Android 中用于文本布局的两个类,它们有以下主要区别:

StaticLayout:是一个灵活的文本布局类,它可以处理多行文本,支持换行、对齐、字间距等特性。通常用于显示可变文本内容,比如 TextView 等控件中。 BoringLayout:专门用于处理单行文本布局,它通常在文本内容较为简单且不经常变化时使用,可以更高效地进行文本绘制。

那其实到这里,你已经把TextView如何绘制的基本情况到就了解了,但其实还有很多可以深挖的地方,比如:

  1. 是怎么基于基线(Baseline)去对齐文字中心的;
  2. span的设置是如何在这其中起到不同渲染设置的作用的;

这些等之后深入了解后在开一篇来讲解,以上就是我们今天要分享的内容啦,喜欢的话欢迎一键三连!雄起!!!

相关推荐
小飞猪Jay8 小时前
C++面试速通宝典——13
jvm·c++·面试
睡觉然后上课13 小时前
c基础面试题
c语言·开发语言·c++·面试
xgq14 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
邵泽明16 小时前
面试知识储备-多线程
java·面试·职场和发展
夜流冰17 小时前
工具方法 - 面试中回答问题的技巧
面试·职场和发展
杰哥在此1 天前
Python知识点:如何使用Multiprocessing进行并行任务管理
linux·开发语言·python·面试·编程
GISer_Jing1 天前
【React】增量传输与渲染
前端·javascript·面试
Neituijunsir1 天前
2024.09.22 校招 实习 内推 面经
大数据·人工智能·算法·面试·自动驾驶·汽车·求职招聘
小飞猪Jay2 天前
面试速通宝典——10
linux·服务器·c++·面试
猿java2 天前
Cookie和Session的区别
java·后端·面试