目录

Android中TextView的内容展示不全的问题

简单的数学计算

TextView中字体的绘制

TextView的行高

TextView的Layout

TextView可以多紧凑

结语

参考资料


 

Android中TextView的内容展示不全的问题

 

简单的数学计算

在工作中第一次遇到新问题,我们可以尝试用讨巧的方式解决,而在第二次遇见的时候,注定了你要去探索它。——我说的。

构造这样的问题很简单,如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/main_text"
        android:layout_width="match_parent"
        android:layout_height="82sp"
        android:background="@android:color/holo_green_dark"
        android:paddingTop="2dp"
        android:paddingBottom="2dp"
        android:lineSpacingExtra="2dp"
        android:ellipsize="end"
        android:maxLines="3"
        android:text="妾发初覆额,折花门前剧。郎骑竹马来,绕床弄青梅。同居长干里,两小无嫌猜,十四为君妇,羞颜未尝开。低头向暗壁,千唤不一回。十五始展眉,愿同尘与灰。常存抱柱信,岂上望夫台。十六君远行,瞿塘滟滪堆。五月不可触,猿声天上哀。门前迟行迹,一一生绿苔。苔深不能扫,落叶秋风早。八月胡蝶来,双飞西园草。感此伤妾心,坐愁红颜老。早晚下三巴,预将书报家。相迎不道远,直至长风沙。"
        android:textSize="23sp" />

</RelativeLayout>

按我的理解是TextView的高度 82>23*3(3行字体)+2*2(两行行间距)+2+2(top/bottompadding)应该是可以完全放的下的。

android 超出父View android view超出屏幕范围_textview

但实际显示出的效果却是只能放下两行多一些。

 

TextView中字体的绘制

那么问题出在哪呢?百度了一通,尝试过includeFontPadding,发现没有什么用处。

于是我开始思考,是不是因为行间距的问题,注意看的话,行间距并非我们设置的lineSpacingExtra = 2dp那么小,而有半个字体那么多,而且从属性名可以看到,我们设置的是额外的lineSpacing。那么我们求得其本身的行间距,是否就可以算出TextView真正需要的高度了呢?

那么我们知道Android中的字体,是基于五根线。这里其他博主就解释的很清楚了。

为了很好的理解这5个变量的意义,我们用下面的图示来进行说明。

android 超出父View android view超出屏幕范围_行间距_02

Baseline是基线,在Android中,文字的绘制都是从Baseline处开始的,Baseline往上至字符“最高处”的距离我们称之为ascent(上坡度),Baseline往下至字符“最低处”的距离我们称之为descent(下坡度);

  leading(行间距)则表示上一行字符的descent到该行字符的ascent之间的距离;

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

android 超出父View android view超出屏幕范围_android_03

top的意思其实就是除了Baseline到字符顶端的距离外还应该包含这些符号的高度,bottom的意思也是一样。一般情况下我们极少使用到类似的符号所以往往会忽略掉这些符号的存在,但是Android依然会在绘制文本的时候在文本外层留出一定的边距,这就是为什么top和bottom总会比ascent和descent大一点的原因。而在TextView中我们可以通过xml设置其属性android:includeFontPadding="false"去掉一定的边距值但是不能完全去掉。

 到这我们知道字体大小是由Bottom-Top,而行间距则是由下一行的Ascent- 这一行的Descent (Leading的值)。而FontMetra正好记录了这些内容。

val fontMetrics = textview.paint.getFontMetrics()
        Log.d("MainActivity","ascent:${fontMetrics.ascent}---top:${fontMetrics.top}----leading:${fontMetrics.leading}----descent:${fontMetrics.descent}----bottom:${fontMetrics.bottom}")

打印出来:
D/MainActivity: ascent:-55.664062---top:-63.36914----leading:0.0----descent:14.6484375----bottom:16.259766

这里奇怪的是leading居然为0,而我们看到行间距是实实在在存在的,不管我在layout中设置lineSpacingExtra 为多大,leading都为0。但我们可以拿到此时字体大小:bottom -  top ,此时算出的字体大小约为79.63px

var dm = DisplayMetrics()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            windowManager.defaultDisplay.getRealMetrics(dm)
        }
        Log.d("MainActivity","dm.density:"+dm.density)

打印出:
D/MainActivity: dm.density:2.625

然后通过DisplayMetrics拿到density = 2.625  则可以算的 字体大约为 30.3sp。这比我们设置的23sp足足大了7.3sp,那么是不是说明其本身内部的lineSpacing 就约等于 7.3sp呢

我们先让TextView的layout_height="wrap_content",然后再测量TextView的高度,此时的高度应该就是TextView装下这些内容的最小高度了

override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
        Log.d("MainActivity","textview.measuredHeight${textview.measuredHeight}")

打印出:
D/MainActivity: textview.measuredHeight275

可算出 TextView的高度为 104.8dp

但是我们算出的字体占据的空间+padding值即使往大了算 :(30.3+2)*3+2+2 = 100.6dp

相差还有4dp多。

 

TextView的行高

随后我想从TextView的方法中是否还能找到什么线索。

android 超出父View android view超出屏幕范围_textview_04

既然还有行高那么我们也来打印看看

Log.d("MainActivity","textview.lineHeight:"+textview.lineHeight)

打印出:
D/MainActivity: textview.lineHeight:76

相较于用FontMetrics算的行高,是更小的,但是修改lineSpaceExtra的值后,lineHeight也会跟着修改。可见这个值绝对和内容高度有关,但暂时也不知道有什么用。

 

TextView的Layout

最后我发现了TextView持有一个变量layout

/**
     * Gets the {@link android.text.Layout} that is currently being used to display the text.
     * This value can be null if the text or width has recently changed.
     * @return The Layout that is currently being used to display the text.
     */
    public final Layout getLayout() {
        return mLayout;
    }

大概意思是,这个对象用来布局text在TextView中如何被展示,但是当TextView某些属性改变后可能会为null

那我们就来一窥究竟

android 超出父View android view超出屏幕范围_android_05

同样这个layout需要在onWindowFocusChanged之后才能拿到,从方法名可以看出,layout可以获取某一行字体的的五线位置

override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
        var layout = textview.layout

        Log.d("MainActivity","layout.getLineDescent(0):${layout.getLineDescent(0)}---- layout.getLineAscent(0):${ layout.getLineAscent(0)}")
        Log.d("MainActivity","layout.getLineDescent(1):${layout.getLineDescent(1)}---- layout.getLineAscent(1):${ layout.getLineAscent(1)}")
        Log.d("MainActivity","layout.getLineDescent(2):${layout.getLineDescent(2)}---- layout.getLineAscent(2):${ layout.getLineAscent(2)}")
        Log.d("MainActivity","layout.getLineDescent(3):${layout.getLineDescent(3)}---- layout.getLineAscent(3):${ layout.getLineAscent(3)}")


        Log.d("MainActivity","layout.getLineBaseline(0):${layout.getLineBaseline(0)}")
        Log.d("MainActivity","layout.getLineBaseline(1):${layout.getLineBaseline(1)}")
        Log.d("MainActivity","layout.getLineBaseline(2):${layout.getLineBaseline(2)}")
        Log.d("MainActivity","layout.getLineBaseline(3):${layout.getLineBaseline(3)}")


        Log.d("MainActivity","layout.getLineTop(0):${layout.getLineTop(0)}---- layout.getLineBottom(0):${ layout.getLineBottom(0)}")
        Log.d("MainActivity","layout.getLineTop(1):${layout.getLineTop(1)}---- layout.getLineBottom(1):${ layout.getLineBottom(1)}")
        Log.d("MainActivity","layout.getLineTop(2):${layout.getLineTop(2)}---- layout.getLineBottom(2):${ layout.getLineBottom(2)}")
        Log.d("MainActivity","layout.getLineTop(3):${layout.getLineTop(3)}---- layout.getLineBottom(3):${ layout.getLineBottom(3)}")

        Log.d("MainActivity","textview.layout${textview.layout}")

        Log.d("MainActivity","textview.measuredHeight${textview.measuredHeight}")

 这里为了方便起见,我将layout的lineSpaceExtra移除,则所有的Log打印:

android 超出父View android view超出屏幕范围_android_06

 从这里我们可以发现,layout.getLineBottom(2) = 255 = layout.getLineTop(3)  而paddingTop和PaddingBottom加起来正好是10.5px, 那么我们可以大胆猜测,行高就是getLineBottom(n) - getLineTop(n)。这里的行高意义就是代表了字体加上其上下行间距。但我们又发现行高是变化的!第一行行高为81px,而第二行行高变成了 87px,这很让人费解。

但不管怎么样我们都前进了一大步,然后行高其实也和getLineDescent和getLineAscent有关。我们的行高不就是等于 getLineDescent(n) - getLineAscent(n) 么。而BaseLine还记得么,是文本绘制的基线,DescentLine和AscentLine都是基于其绘制的,那么我们可以构建出一个绘制模型来了。

android 超出父View android view超出屏幕范围_Android TextView_07

我们看到DescentLine和AscentLine其实是同一条线,所以leading的确为0,也就是没有行间距,即使我们改变lineSpacintExtra,DescentLine和AscentLine依旧是同一条线,那么行间距是由谁来展示的呢?将layout 的lineSpacingExtra = 2dp再设置上我们可以看到

android 超出父View android view超出屏幕范围_Android TextView_08

Descent被改变了,因而导致LineTop和LineBottom也都改变了,正好变大了2dp的大小,也就是lineSpacingExtra会影响的是lineDescent的位置。其实lineSpacingExtra 也可以设置为负数,这就会导致lineDescent变小,视觉上就会导致行间距缩小。

在将lineSpacingExtra设置为 -6dp后,LineDescent已经变成了1px

android 超出父View android view超出屏幕范围_android_09

 

android 超出父View android view超出屏幕范围_行间距_10

 行间距的确减小了,那么现在的关键就是行间距可以缩小为多少呢?

 

TextView可以多紧凑

通过不断的尝试,输入不同字符包括数字,字母,还有特殊字符后发现,基本上最紧凑的时候就是lineDescent = 1或者0的时候,而lineDescent = 0时会在有些情况下两行内容有一丝触碰。

于是现在解决问题就明朗了,我只要保证getLineBottom(maxline-1) <=TextView内容区 则一定能放得下所有文字,而我们如果不缩小字符尺寸,那就得通过lineSpacingExtral来设置了,但是lineSpacingExtra也不能太小,不然会导致文字重叠,所以lineSpacingExtra是有个设置区间的,超过这个区间,就超过了缩小行间距来塞下内容的极限。那么只能修改maxLine了,让显示的最大行数减小同样可以达成目的。

 

结语

我自己写了这样一个自适应的TextView,可以实现判断是否超出内容区,然后缩小行间距,或者缩小maxline,也可以两者都缩小。有需要的自行取用。https://github.com/AndrewSuan/AdaptiveTextView/tree/master

 

参考资料

FontMetrics

Layout https://developer.android.com/reference/android/text/Layout