×

不到100行代码实现左右对齐TextView

96
mandypig
2018.04.18 00:28* 字数 1497
关键词:左右对齐textview 左右对齐textview 左右对齐textview

俗话说的好,人在江湖飘哪有不挨刀,作为一个it工程狮,开发中总是难免遇到各种各样的问题,每个人遇到的问题都不一样,这里就把自己最近遇到的一个问题和大家分享下,给大家节约点时间,让你们有更多的时间去填其他的坑。

事情是这样的:UI效果图需要展示一个大文本的控件,当时也没注意到会有什么问题,直接用textview全部展示不就完了?那么恭喜你肯定也会遇到和我一样的问题,因为默认的TextView只支持左对齐,右对齐,或者居中对齐3种对齐方式,平时开发中一般不会遇到这种展示大文本的UI需求,所以textview的弊端不会被暴露出来,但是一旦需要多行展示那么textview的问题就来了,一图胜千言

test.png

可以发现会非常的难看,右边的文字无法做到对齐控件边缘,既然默认TextView不能解决该问题,那么先在网上搜一发,这么普遍的问题前人肯定没少踩坑,果然比较轻松就搜到了一个开源左右对齐的TextView,github地址如下
https://github.com/androiddevelop/AlignTextView
这里贴出github上的效果图
屏幕快照png

看起来效果非常的不错,满足我们的需求,既然有轮子了那就拿来用,将控件集成进项目,设置一大段中文看了下确实没问题。
但是如果真的一点问题都没有的话,也就不会有这篇文章了,作为一个程序员光会用轮子不行,强烈的好奇心也会驱使我看下源码实现,AlignTextview代码并不多,这里简单说下作者的实现思路:核心代码如下

private void calc(Paint paint, String text) {
        if (text.length() == 0) {
            lines.add("\n");
            return;
        }
        int startPosition = 0; // 起始位置
        float oneChineseWidth = paint.measureText("中");
        int ignoreCalcLength = (int) (width / oneChineseWidth); // 忽略计算的长度
        StringBuilder sb = new StringBuilder(text.substring(0, Math.min(ignoreCalcLength + 1,
                text.length())));

        for (int i = ignoreCalcLength + 1; i < text.length(); i++) {
            if (paint.measureText(text.substring(startPosition, i + 1)) > width) {
                startPosition = i;
                //将之前的字符串加入列表中
                lines.add(sb.toString());

                sb = new StringBuilder();

                //添加开始忽略的字符串,长度不足的话直接结束,否则继续
                if ((text.length() - startPosition) > ignoreCalcLength) {
                    sb.append(text.substring(startPosition, startPosition + ignoreCalcLength));
                } else {
                    lines.add(text.substring(startPosition));
                    break;
                }

                i = i + ignoreCalcLength - 1;
            } else {
                sb.append(text.charAt(i));
            }
        }
        if (sb.length() > 0) {
            lines.add(sb.toString());
        }

        tailLines.add(lines.size() - 1);
    }

(1)首先会计算出一个中文字体所占的宽度
(2)计算出AlignTextView控件的宽度,然后用控件宽度除以中文字体宽度得到一行最多显示多少字
(3)对字体排布进行重新计算,这个有个问题就是作者只计算中文字体所占的宽度,但是标点符号还有英文字体的宽度和中文宽度是不一样的,但可以肯定的一点就是中文字体的宽度是比英文或者标点要宽的,所以作者这里会设置一个阀值,一行字符数至少阀值个数,通过for循环每次多添加一个字符进sb,然后计算该sb中的字符总宽度是否超过控件宽度,最后得到的lines就是一行能包含最多的字符个数
(4)得到各行的lines之后ondraw方法就比较简单了,计算一行剩余的空隙除以每行字符个数就是字符之间的间隙,最后通过drawtext方法给绘制出来。

整个实现的大体思路就是上面所诉,但如果认真思考过你会发现一个问题,如果文本是中英文混排或者全部英文会怎么样,这里直接上图就能很快发现问题所在了
屏幕快照png

很显然虽然实现了左右对齐但是一个单词很大概率上被分成了两行,其实如果理解了上述作者实现的原理就不难发现,作者只是计算一行最多能塞下多少个字符但是并没有计算换行的时候是否会分割单词,所以该开源控件存在一定的问题不能直接使用。

这里比较难解决的一个问题就是如何判断一个单词是否会被换行,思考了下好像没什么思路,但是很明显可以知道默认的Textview是不会出现这种将一个单词分行的处理,既然这样就查看下源码看看如何实现这么个功能,一顿搜索还真发现了解决方法!TextView内部有一个很重要的成员变量StaticLayout,textview之所以能够实现一行显示多少文字不会导致单词分割换行就是该类进行处理的,既然google爸爸已经帮我们实现了这么好用的类那我们就直接拿来用就可以了,然后结合AlignTextView的左右对齐思路,便有了如下代码,代码很简洁100行不到

public class AlignTextView extends AppCompatTextView {

    private boolean alignOnlyOneLine;

    public AlignTextView(Context context) {
        this(context, null);
    }

    public AlignTextView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public AlignTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.AlignTextView);
        alignOnlyOneLine = typedArray.getBoolean(R.styleable.AlignTextView_alignOnlyOneLine, false);
        typedArray.recycle();
        setTextColor(getCurrentTextColor());
    }

    @Override
    public void setTextColor(int color) {
        super.setTextColor(color);
        getPaint().setColor(color);
    }

    protected void onDraw(Canvas canvas) {
        CharSequence content = getText();
        if (!(content instanceof String)) {
            super.onDraw(canvas);
            return;
        }
        String text = (String) content;
        Layout layout = getLayout();

        for (int i = 0; i < layout.getLineCount(); ++i) {
            int lineBaseline = layout.getLineBaseline(i) + getPaddingTop();
            int lineStart = layout.getLineStart(i);
            int lineEnd = layout.getLineEnd(i);
            if (alignOnlyOneLine && layout.getLineCount() == 1) {//只有一行
                String line = text.substring(lineStart, lineEnd);
                float width = StaticLayout.getDesiredWidth(text, lineStart, lineEnd, getPaint());
                this.drawScaledText(canvas, line, lineBaseline, width);
            } else if (i == layout.getLineCount() - 1) {//最后一行
                canvas.drawText(text.substring(lineStart), getPaddingLeft(), lineBaseline, getPaint());
                break;
            } else {//中间行
                String line = text.substring(lineStart, lineEnd);
                float width = StaticLayout.getDesiredWidth(text, lineStart, lineEnd, getPaint());
                this.drawScaledText(canvas, line, lineBaseline, width);
            }
        }

    }

    private void drawScaledText(Canvas canvas, String line, float baseLineY, float lineWidth) {
        if (line.length() < 1) {
            return;
        }
        float x = getPaddingLeft();
        boolean forceNextLine = line.charAt(line.length() - 1) == 10;
        int length = line.length() - 1;
        if (forceNextLine || length == 0) {
            canvas.drawText(line, x, baseLineY, getPaint());
            return;
        }

        float d = (getMeasuredWidth() - lineWidth - getPaddingLeft() - getPaddingRight()) / length;

        for (int i = 0; i < line.length(); ++i) {
            String c = String.valueOf(line.charAt(i));
            float cw = StaticLayout.getDesiredWidth(c, this.getPaint());
            canvas.drawText(c, x, baseLineY, this.getPaint());
            x += cw + d;
        }
    }
}

StaticLayout有几个好用的方法在代码也体现出来了
(1)getLineBaseline可以直接获取到各行的baseline,baseline就是每行的基准线,该行文字就是依据该baseline进行绘制
(2)getLineStart,getLineEnd获取每行起始结束的角标
(3)getDesiredWidth获取每行的宽度

这里用到了一个自定义属性alignOnlyOneLine,

<declare-styleable name="AlignTextView">
        <attr name="alignOnlyOneLine" format="boolean"/>
    </declare-styleable>

该属性的意思就是说当textview只有一行的时候是否需要实现对齐,比方说在实际开发中这种UI展示效果
20180619213644.png

如果将alignOnlyOneLine设置为true,“存储金”,“备注”就能轻松做到平分一行的ui效果,如果换成传统实现方式,可能就需要分别为“存”“储”“金”“备”“注”分别分配textview,然后通过居左居右居中等不同对齐方式实现这种布局效果,使用aligntext加alignOnlyOneLine属性可以很方便实现。

题外话

现在在写布局的时候很少会去使用relativelayout,原因嘛无非就是relativelayout会进行多次的onmeasure测量,在嵌套层数一样的情况下会尽量使用linearlayout和framelayout去实现,为了减少布局嵌套的问题google也推出了自家的布局利器constraintlayout,关于constraintlayout的使用网上已经有不少好的文章可以借鉴花些时间就能掌握,建议大家在以后的开发中使用constraintlayout去实现各种ui效果

剩下的代码自己去看就行了,最后上一个使用我写的alignTextView的效果图
屏幕快照png

这里说句题外话,其实网上流行的那种flowlayout流式布局实现思路和AlignTextView非常相似,只不过flowLayout将计算字符的宽度变成了计算每一个子View的宽度,实现思路大同小异。当然自己实现的AlignTextView还比较简陋,但核心思路就这么多,可以根据自己的需求进行修改。

注意

虽然AlignTextView能够实现左右对齐,但是这能说明可以在项目中大量使用该控件去代替TextView吗,答案是不可以代替Textview,这个问题在https://github.com/androiddevelop/AlignTextView这个开源控件中尤其明显,因为里面的for循环计算每行字符个数的过程是一个比较耗性能的过程,如果你只会使用轮子而不会去看源码那么这个问题你很可能不会注意到!!甚至使用该控件来代替任意TextView使用,自己实现的AlignTextView由于使用google提供的staticlayout所以每行多少个字符就不用自己操心,这方面性能会比开源控件要好,唯一消耗点性能的可能就是计算每行的间隙了,但即使这样在没有左右对齐要求的情况下还是尽量使用TextView比较好

随笔
Web note ad 1