专栏名称: 郭霖
Android技术分享平台,每天都有优质技术文章推送。你还可以向公众号投稿,将自己总结的技术心得分享给大家。
目录
相关文章推荐
stormzhang  ·  打工人可以薅点羊毛​了 ·  昨天  
鸿洋  ·  Android ... ·  2 天前  
鸿洋  ·  一文搞懂Window、PhoneWindow ... ·  6 天前  
郭霖  ·  Android 跨进程+解耦的数据持久化方案 ·  1 周前  
郭霖  ·  Android ... ·  1 周前  
51好读  ›  专栏  ›  郭霖

StyleTextView——细节至上的TextView

郭霖  · 公众号  · android  · 2016-09-14 07:31

正文

今日科技快讯


历时两年之久的快播案昨日终于落下帷幕,快播CEO王欣被判传播淫秽物品牟利罪,判处有期徒刑三年六个月,罚金100万元,快播公司被判处罚金1000万元。


中秋佳节将至,真是几家欢喜几家愁。阿里昨天在内部举办了一个月饼内销活动,4位安全部门的员工为了能够买到月饼,专门写脚本来抢,结果抢了100多盒月饼,引起高层注意,最终阿里以违反企业价值观的名义,将这4名员工开除。本是喜庆的节日,闹得这般下场,也真是让人唏嘘不已。




明天就是中秋节了,这里提前祝大家中秋快乐!接下来的三天就不推送文章了,希望大家都能过个愉快的假期。


本篇文章来自 siyehua 的投稿,为了满足美女设计的极致体验,他不得不深入字体测量学,反复实验,最终抱得美人归(这个还有待考证哈~~)。


由于探索得很细致,加上曲折的故事情节,读完大概需要 11 分钟。


简述


本文主要探讨 TextView 控件绘制文字,上下会留有一定空白原因。以及使用 canvas.drawText() 绘制文字时,baseLine 该如何确定,如何让文字上下没有留白。


最近做项目的时候遇到个问题:




当时的布局是这样的,一言不合就上图:


布局:




效果图:




什么!这百分号和数字怎么对齐?这数字下面的文字怎么居中?这层级为什么这么复杂?关键外面还有好多层父布局,这是要上天呀!




解决思路


薄荷就是醒脑,表情包还没做完思路就出来。


减少层级,采用一个TextView


LinearLayout>
    TextView/>
    TextView/>
    TextView/>
LinearLayout>


每个 TextView 的大小,用 SpannableString 控制其大小和颜色。理论上是很简单的,下面是具体的代码:


布局:




代码:




最终效果:





实践与理论一样简单,完全不是个事.然后我就兴冲冲的把代码提交了。一切都很顺利,同事还夸我脑子灵活。


当然故事肯定都没有这么简单,这么简单也没什么好写博客的,毕竟天天都有人夸我。


事故


说到事故,先说个故事。


APP都有个设计,视觉都会有个美女,自从调来了一个美女视觉之后我干活就有劲了,整天乐不思蜀....



美女的脾气都不好,她说这个%号要靠上,不能靠下。


我好说歹说,说实现起来不容易呀,如果靠上要用复杂的布局,会影响性能...


不行,必须靠上!!!


整个人都斯巴达了,关键是美女没法发脾气,这才是上层把她调过来的真实目的吗?


百分号%下对齐的原因:


百分号之所以下对齐,是因为普通图形的绘制,是从图纸的左上角开始绘制的,而文字的绘制则是从左下角开始的。


具体可以看一下 canvas.draw 图形方法,以及 canvas.drawText 方法参数。


问题是知道的,但是没有办法和设计解释,然而也想不出好的方法改变 baseLine 绘制。 Google一圈没有答案,群里问了一圈也没有,建议自己定义,看来只好自己动手了。


思路


SpannableString 基本上是可以满足需求的,关键是画%百分号的时候不满足。


TextView源码 是如何利用 SpannableString 画的,覆写其关键代码达到目的。


源码老复杂了看不懂,看资料 SpannableString 有一个上标的功能,试了一下上标是相对于本身字体大小来上标的,而60与%相差甚远,且无法微调。


画百分号%的时候自己画(看来只能这个思路了)。


自定义TextView


  • 自定义 StyleTextView 继承 TextView

  • 定义类 TextStyle




  • 定义方法 setText





  • 覆写 onDraw 方法


canvas.drawText(content, startX, baseLine, mPaint);


外传


故事到这里就结束肯定不叫故事。


上面所有的都并非关键,关键是 drawText()参数


之前说了,drawText 是从左下角开始画的,所以要计算出 startX baseLine 的值。


startX:计算出百分号之前的"60"的宽度即可。


baseLine:baseLine 默认是等于 -top(后文证明),但 TextView 上下有一定的空白,导致直接写 -top 不满足需求。




当文字字号过大时,空白非常的明显。设计要求"60%"百分号与60对齐,所以必须量出空白的高度。


测试上图中的各个属性


按照上图的说法,我们打印一下这几个值:


TextView
    android:id="@+id/_test"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello !"
    android:textSize="60sp"/>




结果如下(单位px,后文无特殊说明,单位均默认为像素px):


top: -126.73828  ascent: -111.328125  descent: 29.296875  bottom: 32.51953  leading: 0.0
view-top: 0  view-bottom: 160 textSize: 120.0




其中 top ascent 负数,是因为 baseline 是 0


根据上图,默认状态下,top 与 ascent baseline 上面,而安卓手机的 Y轴 正方向是向下的,故 top 与 ascent 是负数,所以前面提到的 baseline 实际上就等于 -top


按照网上的说法,TextView 的高度实际上是 top bottom 的距离,与结果一致。


文字的高度是 ascent descent 之间的距离 111.328125 + 29.296875 ≈ 140.63px,而打印出来的文字的实际大小是 120px,这个结果与预期不符。


假设 ascent descent 是动态变化的,可能是 "Hello !" 这个字符串并没有达到 ascent 的最小值。


我们知道 a 与 b 两个字符绘制的结果,b 明显要冒出一个头。所以有可能是某些字符会突破天际,达到 ascen tdescent 值。


Google一圈没有发现到底用什么字符测试测试到上限值,但是不妨换一个思考方式,既然无法得到上限,可以突破下限。把 TextView 的字符串改为 ac,看 ascent descent 变化了没有。



可见 ascent descent 不是动态变化的。知乎里看过有人提到,TextView 上下有留白,是因为默认添加了一个上下的留白


TextView 本身包含一定的 FontPadding 。看 TextView setIncludeFontPadding() 方法。一言不合上代码:




源码不重要,关键是注释,注释里写默认是有一定的间距的,view 的高度是 top bottom 之间的间距,而设置为false之后,view 的高度是 ascent descent 之间的间距,重新设置并打印值:


TextView
    android:id="@+id/_test"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:includeFontPadding="false"
    android:text="Hello !"
    android:textSize="60sp"/>


结果 view 的高度等于 ascent descent 之间的高度。


top: -126.73828  ascent: -111.328125  descent: 29.296875  bottom: 32.51953  leading: 0.0
view-top: 0  view-bottom: 140 textSize: 120.0


结果符合源码的解释,同时完善一下上面的关于 baseline 的结论。


假设设置了android:includeFontPadding="false",则 baseline 实际的值等于 -ascent。


但是并没有得到:


ascent 与 descent 之间的距离等于 textSize 的大小。


这个结论。


那会不会是 textSize 的大小实际上指的并不是文字的高度,可能只是一个需要转换的值。




截屏后使用 Mark Man 一量,这文字的高度妥妥的就是120px。换个说法就是:


ascent 与 descent 之间的距离 不等于 textSize 的大小/高度.


分析




假设 top ascent 之间的空白是 firstSpaceascent 与文字顶端的空白是 secondSpace


则文字最顶端的 Y坐标 topTextY = firstSpace + secondSpace;(如 TextView 已经设置了 android:includeFontPadding="false" 则不需要加上 firstSpace)


firstSpace = ascent - top;


secondSpace = descent - ascent - textSize;


按照这个理论,画一条线:




非常标准,刚好对齐,深得我意。


所以画百分号的 baseline 等于:


baseLine = topTextY + -top(默认状态下,百分号的baseline) - 百分号的topTextY;


代码:




结果(60这个字符串本身距离直线有一点间距是因为字不够长,假设使用字母 'l' 刚刚好):




总结


终于要完了吗?实际上还没有。




假设 TextView 的宽度是 match_parent,让内容居中:


com.siyehua.styletextview.StyleTextView
    android:id="@+id/tv_content"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:text="Hello !"
    android:textSize="60sp"/>



会发现百分号并没有跟着跑,实际上是因为 canvas.drawText() 这个方法的参数,startX BaseLine 还受内容的对齐方式影响。


而我绘制百分号并没有考虑这方面的因素。


假设 TextView 的对齐方式是居中,则 startX TextView 文字的中点。




图中红框的右边便是文字的右边,而我们需要计算文字右边减去粉色百分号%,得到到即是上标百分号%的 startY


上标startY = 文字右边 - "%"宽度

文字右边距 = TextView宽度/2.0f + 需要绘制的文字的宽度/2.0f

需要绘制的文字的宽度 =  "60"串的宽度 + "%"百分号的宽度




结果:



可以看到百分号的已经设置到了正确的位置。


同理可得(其实我懒得算了)


当 TextView 的高度设置 match_parent,且内容设置居中时,需要用同样的方法计算 baseline。


这里就不在举例了,毕竟制作表情包太累了。


点击最后 阅读原文 查看源码。






如果你有好的技术文章想和大家分享,欢迎向我的公众号投稿,投稿具体细节请在公众号主页点击“投稿”菜单查看。


欢迎长按下图 -> 识别图中二维码或者扫一扫关注我的公众号: