注册

理解TextView三部曲之番外篇:或许这会是最终的进化

额,为什么会有番外篇呢。。因为新版本上线后,别的同学用我的这个控件,描边显示出问题了-_-!


什么问题呢?


我把问题抽出来,同时把问题放大点,给大家看看(抹眼泪.png)


1_error_show



好嘛,问题不大。。就是描边歪了一点点,对吧。


可是怎么会这样!?,我自己测根本就没有问题,压根就没出现过这样的问题啊。。(抹眼泪.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);
}
}

两步解决,但为什么我这显示没问题,别的同学那里显示就出问题了呢?


我突然想起来,相同字体在不同手机上显示是有差异的,而且有些手机不一定都支持那种字体


我和那位同学用着不同厂商的真机进行测试,而我的真机是不支持他设置的字体的,所以看着没问题,但他的小米是支持的。难怪我这看着没问题,他那看着就很离谱。


修改完后,我们在运行一遍。


2_fixed_show


怎一个完美形容!ok,bug解决了,准备提交代码



就这样结束了吗?


时隔多日,我又重新审核了一遍代码,我留意到这样一行代码


float heightWeNeed
= getCompoundPaddingTop() + getCompoundPaddingBottom() + mStrokeWidth + mTextRect.height() + DensityUtil.dp2px(getContext(), 4);

我们需要的高度 = 内边距 + 描边高度 + 文本高度 + 一个额外设定的值 ?


怎么会需要一个额外的值呢?要实现wrap_content的效果,我们的宽度不是只需要加上边距、文本高度和一个描边的高度吗?


好奇怪的逻辑,这不是多余嘛,我当时怎么想的来着哈哈?不符合我wrap_content的预期,把它删了试试,再测一遍


把我之前的测试用例都测了一遍,都运行正常


除了。。除了下面这种情况。


3_error_show



果然,去掉额外的高度,就会有这种高度不够显示的情况。看来当时的我,就是遇到了这种情况,然后一个手快,就给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中返回包含所有字符的最小矩形


5_bounds_height


也就是说bounds返回的高度,只是能够包含文本的最小高度。


我们在三部曲概览里就讨论过,安卓里文本的描绘,是由几根线来确定的


4_text_lines


文本的高度应该为(fontMetrics.bottom -fontMetrics. top),但是,bounds中返回的height也够文本显示啊?怎么会显示成下面这个样子?


6_error_show


比如这样


7_thought_show


但实际情况好像是这样的


8_thought_show_2


我想到,安卓绘制文本是有起点坐标的,这个起点由gravity,textAlign,和baseline确定,和内容展示高度好像没有关系。


虽然我们展示高度设小了,但它的起点坐标还在原来的位置(比如y坐标baseline),这才导致了18数字显示不完整,底部好像缺了一块。



问题的根本找到了,看来好像有两种解决方法



  1. 调整baseline的位置:把我们的baseline位置上移一些,让它和展示区域底部位置重合,这样就能以最小区域显示完整的文本内容。
  2. 拓宽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,如果觉得我写的还不错,麻烦帮个忙呗 :-)



  1. 给俺点个赞被,激励激励我,同时也能让这篇文章让更多人看见,(#^.^#)
  2. 不用点收藏,诶别点啊,你怎么点了?这多不好意思!
  3. 噢!还有,我维护了一个路由库。。没别的意思,就是提一下,我维护了一个路由库 =.= !!

拜托拜托,谢谢各位同学!


作者:古嘉明同学
链接:https://juejin.cn/post/7111669608842543135
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册