理解TextView三部曲之番外篇:或许这会是最终的进化
额,为什么会有番外篇呢。。因为新版本上线后,别的同学用我的这个控件,描边显示出问题了-_-!
什么问题呢?
我把问题抽出来,同时把问题放大点,给大家看看(抹眼泪.png)
好嘛,问题不大。。就是描边歪了一点点,对吧。
可是怎么会这样!?,我自己测根本就没有问题,压根就没出现过这样的问题啊。。(抹眼泪.png)
我又去检查了一遍计算描边位置那块的代码,最初是以为其他同学一不小心该了那块的代码,导致描边位置计算出错了,结果发现,代码丝毫没有动过的痕迹。
那怎么会描边出错呢?而且他描边出问题的地方,在我这里这里显示也没什么问题,在他那里会什么会有这么大的偏差呢?
我不信邪,看看那位同学都对StrokeTextView做了哪些设置?结果发现,他多了下面这行代码:
mStrokeTextView.setTypeface(typeface);
我捉摸了一下,发现这行代码很有问题,因为我的StrokeTextView是继承自TextView的,调用setTypeface(),看看它的默认实现:
public void setTypeface(@Nullable Typeface tf) {
if (mTextPaint.getTypeface() != tf) {
mTextPaint.setTypeface(tf);
if (mLayout != null) {
nullLayouts();
requestLayout();
invalidate();
}
}
}
看了一眼我就明白了,它只是给TextPaint设置了不同的typeFace,而我们的描边是使用不同的TextPaint,也就是说setTypeface()只是给我们的文本设置了字体,却没有给我们的StrokeTextPaint设置相同的字体,导致了两种不同字体之间,没有办法对齐位置,导致了描边差异。
怎么解决?简单,照葫芦画瓢就行,我们在StrokeTextView重写setTypeface()方法。
setTypeface()的默认实现有两种,我们都要重写:
@Override
public void setTypeface(@androidx.annotation.Nullable Typeface tf) {
// 模仿TextView的设置
// 需在super.setTypeface()调用之前,不然没有效果
if (mStrokePaint != null && mStrokePaint.getTypeface() != tf) {
mStrokePaint.setTypeface(tf);
}
super.setTypeface(tf);
}
另一种比较复杂,不过我们会模仿就行了:
public void setTypeface(@Nullable Typeface tf, int style) {
if (style > 0) {
if (tf == null) {
tf = Typeface.defaultFromStyle(style);
} else {
tf = Typeface.create(tf, style);
}
setTypeface(tf);
// now compute what (if any) algorithmic styling is needed
int typefaceStyle = tf != null ? tf.getStyle() : 0;
int need = style & ~typefaceStyle;
getPaint().setFakeBoldText((need & Typeface.BOLD) != 0);
getPaint().setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
// 同步设置mStrokeTextPaint
if (mStrokePaint != null) {
mStrokePaint.setFakeBoldText((need & Typeface.BOLD) != 0);
mStrokePaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
}
} else {
getPaint().setFakeBoldText(false);
getPaint().setTextSkewX(0);
// 同步设置mStrokeTextPaint
if (mStrokePaint != null) {
mStrokePaint.setFakeBoldText(false);
mStrokePaint.setTextSkewX(0);
}
setTypeface(tf);
}
}
两步解决,但为什么我这显示没问题,别的同学那里显示就出问题了呢?
我突然想起来,相同字体在不同手机上显示是有差异的,而且有些手机不一定都支持那种字体。
我和那位同学用着不同厂商的真机进行测试,而我的真机是不支持他设置的字体的,所以看着没问题,但他的小米是支持的。难怪我这看着没问题,他那看着就很离谱。
修改完后,我们在运行一遍。
怎一个完美形容!ok,bug解决了,准备提交代码
就这样结束了吗?
时隔多日,我又重新审核了一遍代码,我留意到这样一行代码
float heightWeNeed
= getCompoundPaddingTop() + getCompoundPaddingBottom() + mStrokeWidth + mTextRect.height() + DensityUtil.dp2px(getContext(), 4);
我们需要的高度 = 内边距 + 描边高度 + 文本高度 + 一个额外设定的值 ?
怎么会需要一个额外的值呢?要实现wrap_content的效果,我们的宽度不是只需要加上边距、文本高度和一个描边的高度吗?
好奇怪的逻辑,这不是多余嘛,我当时怎么想的来着哈哈?不符合我wrap_content的预期,把它删了试试,再测一遍
把我之前的测试用例都测了一遍,都运行正常
除了。。除了下面这种情况。
果然,去掉额外的高度,就会有这种高度不够显示的情况。看来当时的我,就是遇到了这种情况,然后一个手快,就给heightWeNeed做了这种适配。
不过这种手快的适配方法貌似不太优雅,为了适配单一的这种情况,要牺牲剩下的所有情况都增加一个额外的高度。
而且因为我们适配的额外高度是一个固定值,如果我们给文本字体大小设置大一点,还是会有高度不够显示的可能,毕竟文本变大了,所需要的高度也就更多了。
好吧,这种适配方法看来是用不得了,要换一个吗?但是计算高度的公式 = 内边距 + 文本高度 + 描边高度,这个公式肯定是没错的。
回到我们最初的问题,我们为什么会需要增加一个额外的固定高度呢?明明公式都是对的,为什么还是会有偏差,难道是公式里的对应的值计算错误了?
我们看看再来看看这个式子:
heightWeNeed = getCompoundPaddingTop() + getCompoundPaddingBottom() +
mStrokeWidth + mTextRect.height();
其中,getCompoundPaddingTop() 和 getCompoundPaddingBottom() 是Android提供的计算内边距的api,这个肯定不至于错吧。
mStrokeWidth是我们的描边宽度,是由用户使用时自定义的,这个没什么需要计算的,就是一个值而已
那么mTextRect.height() 这个呢,我们需要这里返回一个正确的文本高度。
看看这个mTextRect是在哪里赋值的
getPaint().getTextBounds(text, 0, text.length(), mTextRect);
从getTextBounds()里跟下去,发现最后调用测量的是native方法,看不到内部实现,不过我们可以看看getTextBounds()的注释
/**
* Retrieve the text boundary box and store to bounds.
*
* Return in bounds (allocated by the caller) the smallest rectangle that
* encloses all of the characters, with an implied origin at (0,0).
*
* @param text string to measure and return its bounds
* @param start index of the first char in the string to measure
* @param end 1 past the last char in the string to measure
* @param bounds returns the unioned bounds of all the text. Must be allocated by the caller
*/
public void getTextBounds(String text, int start, int end, Rect bounds) {
...
// native 方法
nGetStringBounds(mNativePaint, text, start, end, mBidiFlags, bounds);
}
Return in bounds the smallest rectangle that encloses all of the characters
在bounds中返回包含所有字符的最小矩形
也就是说bounds返回的高度,只是能够包含文本的最小高度。
我们在三部曲概览里就讨论过,安卓里文本的描绘,是由几根线来确定的
文本的高度应该为(fontMetrics.bottom -fontMetrics. top),但是,bounds中返回的height也够文本显示啊?怎么会显示成下面这个样子?
比如这样
但实际情况好像是这样的
我想到,安卓绘制文本是有起点坐标的,这个起点由gravity,textAlign,和baseline确定,和内容展示高度好像没有关系。
虽然我们展示高度设小了,但它的起点坐标还在原来的位置(比如y坐标baseline),这才导致了18数字显示不完整,底部好像缺了一块。
问题的根本找到了,看来好像有两种解决方法
- 调整baseline的位置:把我们的baseline位置上移一些,让它和展示区域底部位置重合,这样就能以最小区域显示完整的文本内容。
- 拓宽bounds.height的高度,以(fontMetrics.bottom - fontMetrics.top)作为文本的高度显示,这样就无需改变baseline的位置,但比第一种方案要多需要一些空间。
这里我选了第二种,顺着系统的绘制规则来,图个方便,而且我们的描边也可以利用文本顶部多出来的这些空间。
我们新设个变量 textHeight = fontMetrics.descent - fontMetrics.top
heightWeNeed = getCompoundPaddingTop() + getCompoundPaddingBottom() +
textHeight + mStrokeWidth / 2;
为了最大化利用空间,文字顶部到top线的距离已经足够我们的描边显示了,而bottom线到descent线之间的距离很窄,就可能不够我们的描边显示。
所以只需要在文字底部加一半的描边宽度,同时去掉buttom线和descent线之间的距离,这样就能确保文字和描边都有足够的位置显示了。
好了,番外篇终于结束了,看了眼字数,居然比之前的三部曲系列都要多一些。实在没想到需要这么长的篇幅来讲这两个小优化,谢谢小伙伴们能够看到这里啦。
源码我都已经上传到github了,欢迎小伙伴自取,如果觉得写得不错的,还请给这份工程给个star ~_ <
兄dei,如果觉得我写的还不错,麻烦帮个忙呗 :-)
- 给俺点个赞被,激励激励我,同时也能让这篇文章让更多人看见,(#^.^#)
- 不用点收藏,诶别点啊,你怎么点了?这多不好意思!
- 噢!还有,我维护了一个路由库。。没别的意思,就是提一下,我维护了一个路由库 =.= !!
拜托拜托,谢谢各位同学!
链接:https://juejin.cn/post/7111669608842543135
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。