自定义TextView实现多行情况下最后一行省略号

最近做需求,要实现标题多行情况下,最后一行中间省略号:


image.png

只有一行的情况下,TextView中的属性ellipsize可以实现得到,但是多行的情况下,从android4.0之后该属性失效,大概看了一下TextView源码,当多行的情况下,TextView中的那个makeSingleLayout方法会根据当前的文字长度给分行,但是如果文字中出现特殊字符时(比如有中文),生成的DynamicLayout会截取不争取,导致如下效果:


image.png

所以只能自定义TextView,自己将设置的文字给一行行画出来,如下步骤:

  • 判断是否超过最大行数
  • 将超过最大行数的文字等距离给删除掉并添加省略号(可以自定义)
  • 然后重写onDraw方法,将文字一行行画出来

要自己画文字,需要掌握android中TextView的绘制方式:
如何用TextPaint

Paint中有一个类用于文字的字体测量FrontMetrics

/**
     * Class that describes the various metrics for a font at a given text size.
     * Remember, Y values increase going down, so those values will be positive,
     * and values that measure distances going up will be negative. This class
     * is returned by getFontMetrics().
     */
    public static class FontMetrics {
        /**
         * The maximum distance above the baseline for the tallest glyph in
         * the font at a given text size.
         */
        public float   top;
        /**
         * The recommended distance above the baseline for singled spaced text.
         */
        public float   ascent;
        /**
         * The recommended distance below the baseline for singled spaced text.
         */
        public float   descent;
        /**
         * The maximum distance below the baseline for the lowest glyph in
         * the font at a given text size.
         */
        public float   bottom;
        /**
         * The recommended additional space to add between lines of text.
         */
        public float   leading;
    }

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

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

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

在TextPaint绘制文字时,是从BaseLine往上画的,BaseLine下面为正,上面为负,所以通过paint.getAscent拿到的都是负数,paint.getDescent为正数。
所以通过canvas.drawText(text, x, y, paint),其中(x,y)就是baseline的起始坐标。

了解了TextPiant的文字测量后,接下来就是:

  • 判断是否超过最大行数
  • 将超过最大行数的文字等距离给删除掉并添加省略号(可以自定义)
  • 然后重写onDraw方法,将文字一行行画出来

  • 判断是否超过最大行数
    每次当设置setText后,都会去调用requestLayout更新布局。

所以在onMeasure方法判断当前是否行数超过最大行,如果超过,截取文字并且重新设置。


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setText(mOriginText);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        try {
            mIsExactlyMode = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY;
            final Layout layout = getLayout();
            if (layout != null) {
                //超过设定的maxLine的时候,删除中间多余的字符,换成‘...’
                if (isExceedMaxLine(layout) || isOutOfBounds(layout)) {
                    adjustEllipsizeMiddleText(layout);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    @Override
    public void setText(CharSequence text, BufferType type) {
        if (mEnableUpdateOriginText) {
            mOriginText = text;
        }
        super.setText(text, type);
        if (mIsExactlyMode) {
            requestLayout();
        }
    }

如果超过最大行数时,从最后一行截取并且给中间添加省略号:

    private void adjustEllipsizeMiddleText(Layout layout) {
        CharSequence originText = mOriginText;
        int width = getContentWidth();
        int maxLineCount = Math.max(1, computeMaxLineCount(layout));
        int maxLineFirstCharIndex = computeWidthIndex(originText, 0, (maxLineCount - 1) * width);

        mEnableUpdateOriginText = false;
        int ellipizeTextWidth = (int)Layout.getDesiredWidth(mEllipsizeText, getPaint());
        int origninWidth = (int)Layout.getDesiredWidth(mOriginText, getPaint()) + ellipizeTextWidth;
        int maxWidth = width * maxLineCount;
        if (origninWidth > maxWidth) {
            int widthDiff = origninWidth - maxWidth;
            //找出最后一行中将字符的下标
            int maxLineMidCharIndex = computeWidthIndex(originText, maxLineFirstCharIndex, width >>> 1);
            //将超过的widthDiff的长度对应的字符给删掉
            int removedMiddleCount = computeRemovedCharCount(originText, maxLineMidCharIndex, widthDiff) + 1;

            CharSequence front = originText.subSequence(0, maxLineMidCharIndex);
            CharSequence behind = originText.subSequence(maxLineMidCharIndex + removedMiddleCount - 1, mOriginText.length());
            String result = new StringBuffer(front).append(mEllipsizeText).append(behind).toString();
            //重新设置文字
            setText(result);
        }
        mEnableUpdateOriginText = true;
    }

上面就是将多出的文字截取并且添加好省略号,接下来就是一行行第画出来:

@Override
    protected void onDraw(Canvas canvas) {
//        super.onDraw(canvas);
        final Layout layout = getLayout();
        if (layout != null) {
            int x = getPaddingLeft();
            //初始化y的值是第一行中的baseline位置.
            int y = getPaddingTop() + (int) Math.abs(getPaint().ascent());
            CharSequence text = getText();
            int start = 0;
            int end = 0;
            float charsWidth = 0;
            int size = text.length();
            int maxWidth = getContentWidth();
            for (int i = 0; i < layout.getLineCount(); i++) {
                while (charsWidth < maxWidth && end < size) {
                    charsWidth = getPaint().measureText(text.subSequence(start, (++end)).toString());
                }
                if (charsWidth > maxWidth) {
                    end--;
                }
                canvas.drawText(text, start, end, x, y, getPaint());
                start = end;
                charsWidth = 0;
                 //将y的位置移动到下面有行的baseline位置
                y = y + (int) Math.abs(getPaint().descent() - getPaint().ascent());
            }
        }
    }

上面就用到了文字测量的acent和descent。

完。

上面代码源码Demo版本EllipsizeMiddleTextView

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容