最近有需求需要在一个有限高度的页面上显示超过其高度的文字,当文字超过最大行之后显示省略号和查看更多,然后点击查看更多显示完整的信息,并且可以滚动。

先看效果图:

Android 超出区域外内容 android超出屏幕view_点击展开

功能很简单,网上搜的有人使用了scrollview来滚动,再添加一个按钮”查看更多”,然后点击后把消息全部展示,再把按钮隐藏。

但是这样做不是觉得很复杂,其实所有的工作在一个textView里就可以完成了,包括滚动效果,包括查看更多的按钮。这些都可以在同一个textView里处理完,先上代码,后面再解释其原理。

我封装成了一个工具类,使用该工具类来处理TextView的显示逻辑

使用方法:

导入下面的工具类MoreTextUtil,在你需要设置TextView拥有展开效果的时候添加下面代码:

textView.setText("你要显示的内容");
MoreTextUtil.setMore(textView);

你也可以这样,使用重载的方法:

MoreTextUtil.setMore(textView,"你要显示的内容");

默认最大可显示行数为5行,如果需要自定义就给自己的TextView的布局加上下面的属性:

android:maxLines="9"    //最多显示9行

用法很简单吧,下面上源码:

package com.shelwee.update.utils;

import android.annotation.SuppressLint;
import android.text.Layout;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.TextUtils.TruncateAt;
import android.text.method.LinkMovementMethod;
import android.text.method.ScrollingMovementMethod;
import android.text.style.ClickableSpan;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.TextView;

import com.shelwee.updater.R;

public class MoreTextUtil {

    public static boolean hasMesure = false;    //是否已经执行过一次

    public static void setMore(TextView textV,String content){
        textV.setText(content);
        setMore(textV,"...","查看更多");
    }

    public static void setMore(TextView textV){
        setMore(textV,"...","查看更多");
    }

    @SuppressLint("NewApi")
    public static void setMore(final TextView textV, final String ellipsis, final String strmore) {
        if (textV == null) {
            return;
        }
        if (2147483647 == textV.getMaxLines()) textV.setMaxLines(5);
        textV.setEllipsize(TruncateAt.END);
        textV.setVerticalScrollBarEnabled(true);

        hasMesure = false;

        //添加布局变化监听器,view 布局完成时调用,每次view改变时都会调用
        ViewTreeObserver vto = textV.getViewTreeObserver();
        vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if (!hasMesure) {
                    int maxLines = textV.getMaxLines();
                    int lines = textV.getLineCount();

                    //如果文字的行数超过最大行数,展示缩略的textview
                    if (lines >= maxLines) {
                        Layout layout=textV.getLayout();
                        String str=layout.getText().toString();
                        int end = layout.getLineEnd(maxLines-2);
                        str = str.substring(0, end);                    //缩略的文字
                        String strall = textV.getText().toString();     //完整的文字
                        hasMesure = true;

                        SpannableString spanstr;
                        //如果以换行符结尾,则不再换行
                        if (str.endsWith("\n")) {
                            spanstr = new SpannableString(str + ellipsis + strmore);
                        }else {
                            spanstr = new SpannableString(str + "\n" + ellipsis + strmore);
                        }

                        //设置“查看更多”的点击事件
                        spanstr.setSpan(new MyClickableSpan(strall,textV.getResources().getColor(android.R.color.holo_green_dark)), spanstr.length() - strmore.length(),
                                spanstr.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

                        textV.setText(spanstr);
                        //移除默认背景色
                        textV.setHighlightColor(textV.getResources().getColor(android.R.color.transparent));
                        textV.setMovementMethod(LinkMovementMethod.getInstance());
                    }
                }
            }
        });

    }

    static class MyClickableSpan extends ClickableSpan{
        private String str;
        private int color;
        public MyClickableSpan(String str,int color) {
            this.str = str;
            this.color = color;
        }

        @Override
        public void onClick(View view) {
            ((TextView)view).setMovementMethod(new ScrollingMovementMethod());
            ((TextView)view).setText(str);
        }

        @Override
        public void updateDrawState(TextPaint ds) {
            super.updateDrawState(ds);
            ds.setColor(color);         //设置“查看更多”字体颜色
            ds.setUnderlineText(false); //设置“查看更多”无下划线,默认有
            ds.clearShadowLayer();
        }
    }
}

代码中的注释很清楚,说下重点:
代码中为什么要使用布局变化监听器呢,如下:

ViewTreeObserver vto = textV.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener(){
    ....
});

原因是TextView会根据字符的排版规则自动换行,这种换行受字体大小的影响,而且英文字母符号中文等不同字符占的宽并不同,而且TextView还会自动匹配单词选择性换行,所以我们几乎无法通过一个字符串计算出TextView会在什么时候换行,我们也无法计算出在字符串的哪个位置会因为显示不下而出现截断。

那么我们就换一种方式,直接把字符串绘制到TextView中,在TextView生成布局渲染完成后把字符设置到页面前一刻再回调我们的方法,这样我们就可以得到TextView具体绘制了多少行,每行的字符是什么,这也就是为什么要使用ViewTreeObserver 添加 OnGlobalLayoutListener 的原因。在onGlobalLayout回调方法中完成我们的业务逻辑。

但是值得注意的是,OnGlobalLayoutListener 会在绘制过程中被调用多少,而我们的业务逻辑只需要处理一次就可以了,为了避免不必要的性能损失,我们得加以控制,通过hasMesure 这个布尔变量来控制代码只执行一次。

另外一个重要的地方就是,我们没有添加按钮,那就以为着我们的TextView必须要支持局部点击事件,就是说,点击“查看更多”的时候要有响应,而点其他地方没有。还好TextView的SpannableString (复合文本) 支持多事件和样式的处理,如下:

//设置“查看更多”的点击事件
spanstr.setSpan(new MyClickableSpan(strall,textV.getResources().getColor(R.color.cc_orage)),
                spanstr.length() - strmore.length(),
                spanstr.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

textV.setText(spanstr);

这里就使用了复合文本格式来设置局部的点击事件,方法原型如下:

spanstr.setSpan(Object what,int start,int end,int flags)

what 参数可以设置一个监听器
start 指定了设置的字符的开始位置
end 是指定字符的结束位置
flags 是间隔模式,SPAN_EXCLUSIVE_EXCLUSIVE表示不影响前后相邻的字符

接着看复合文本事件监听器:

static class MyClickableSpan extends ClickableSpan{
        private String str;
        private int color;
        public MyClickableSpan(String str,int color) {
            this.str = str;
            this.color = color;
        }

        @Override
        public void onClick(View view) {
            ((TextView)view).setMovementMethod(new ScrollingMovementMethod());
            ((TextView)view).setText(str);
        }

        @Override
        public void updateDrawState(TextPaint ds) {
            super.updateDrawState(ds);
            ds.setColor(color);         //设置“查看更多”字体颜色
            ds.setUnderlineText(false); //设置“查看更多”无下划线,默认有
            ds.clearShadowLayer();
        }
    }

构造方法我传入了完整的字符串和一个颜色值,当点击后把显示的字符串换成完整的字符,并设置可以滚动就ok,至于颜色值,我希望“查看更多”这4个字拥有不同的颜色,当然你不设置也没有什么关系,丑一点而已。
在点击事件执行之前,会先调用updateDrawState(TextPaint ds)方法设置绘制格式,在这里设置颜色和取消下划线,如果需要其他特殊效果的话也在这里处理。

觉得不错的下面有个顶,可以点一下 :)