Android TextView超过指定限制在文末显示 "...全文" 并且可以点击

最近有个需求就是文字显示指定行数比如3行,超过3行时就在第三行的末尾显示省略号加“全文”。
效果类似今日头条和微博:

image.png

实现如下:
1、ShowAllTextView 继承TextView类

public class ShowAllTextView extends TextView {

/**全文按钮点击事件*/
private ShowAllSpan.OnAllSpanClickListener onAllSpanClickListener;
private int maxShowLines = 0;  //最大显示行数

public ShowAllTextView(Context context) {
    super(context);
}

public ShowAllTextView(Context context, AttributeSet attrs) {
    super(context, attrs);
}

/**调用此方法才有效果*/
public void setMyText(CharSequence text) {
    super.setText(text);
    post(new Runnable() {
            @Override
            public void run() {
                addEllipsisAndAllAtEnd();
            }
     });
}

/**调用此方法才有效果*/
public void setMyText(int resId){
    setMyText(getContext().getResources().getText(resId));
}

/**超过规定行数时, 在文末添加 "...全文"*/
private void addEllipsisAndAllAtEnd(){
    if (maxShowLines > 0 && maxShowLines < getLineCount()) {
        try {
            int moreWidth = PaintUtils.getTheTextNeedWidth(getPaint(), "...全文");
            /**加上...全文 长度超过了textView的宽度, 则多减去5个字符*/
            if (getLayout().getLineRight(maxShowLines - 1) + moreWidth >= getLayout().getWidth()){
                this.setText(getText().subSequence(0, getLayout().getLineEnd(maxShowLines - 1) - 5));
                /**避免减5个字符后还是长度还是超出了,这里再减4个字符*/
                if (getLayout().getLineRight(maxShowLines - 1) + moreWidth >= getLayout().getWidth()){
                    this.setText(getText().subSequence(0, getLayout().getLineEnd(maxShowLines - 1) - 4));
                }
            }else {
                this.setText(getText().subSequence(0, getLayout().getLineEnd(maxShowLines - 1)));
            }
            if (getText().toString().endsWith("\n") && getText().length() >= 1){
                this.setText(getText().subSequence(0, getText().length() - 1));
            }
            this.append("...");
            SpannableString sb = new SpannableString("全文");
            sb.setSpan(new ShowAllSpan(getContext(), onAllSpanClickListener), 0, sb.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
            this.append(sb);
        }catch (Exception e){}
    }
}

public void setOnAllSpanClickListener(ShowAllSpan.OnAllSpanClickListener onAllSpanClickListener) {
    this.onAllSpanClickListener = onAllSpanClickListener;
}

public int getMaxShowLines() {
    return maxShowLines;
}

public void setMaxShowLines(int maxShowLines) {
    this.maxShowLines = maxShowLines;
}

//实现span的点击
private ClickableSpan mPressedSpan = null;
private boolean result = false;
@Override
public boolean onTouchEvent(MotionEvent event) {
    CharSequence text = getText();
    Spannable spannable = Spannable.Factory.getInstance().newSpannable(text);
    switch (event.getAction()){
        case MotionEvent.ACTION_DOWN:
            mPressedSpan = getPressedSpan(this, spannable, event);
            if (mPressedSpan != null){
                if (mPressedSpan instanceof ShowAllSpan){
                    ((ShowAllSpan) mPressedSpan).setPressed(true);
                }
                Selection.setSelection(spannable, spannable.getSpanStart(mPressedSpan), spannable.getSpanEnd(mPressedSpan));
                result = true;
            }else {
                result = false;
            }
            break;
        case MotionEvent.ACTION_MOVE:
            ClickableSpan mClickSpan = getPressedSpan(this, spannable, event);
            if (mPressedSpan != null && mPressedSpan != mClickSpan){
                if (mPressedSpan instanceof ShowAllSpan){
                    ((ShowAllSpan) mPressedSpan).setPressed(false);
                }
                mPressedSpan = null;
                Selection.removeSelection(spannable);
            }
            break;
        case MotionEvent.ACTION_UP:
            if (mPressedSpan != null){
                if (mPressedSpan instanceof ShowAllSpan){
                    ((ShowAllSpan) mPressedSpan).setPressed(false);
                }
                mPressedSpan.onClick(this);
            }
            mPressedSpan = null;
            Selection.removeSelection(spannable);
            break;
    }
    return result;

}

private ClickableSpan getPressedSpan(TextView textView, Spannable spannable, MotionEvent event) {

    ClickableSpan mTouchSpan = null;

    int x = (int) event.getX();
    int y = (int) event.getY();
    x -= textView.getTotalPaddingLeft();
    x += textView.getScrollX();
    y -= textView.getTotalPaddingTop();
    y += textView.getScrollY();
    Layout layout = getLayout();
    int line = layout.getLineForVertical(y);
    int off = layout.getOffsetForHorizontal(line, x);

    ShowAllSpan[] spans = spannable.getSpans(off, off, ShowAllSpan.class);
    if (spans != null && spans.length > 0){
        mTouchSpan = spans[0];
    }
    return mTouchSpan;
}
}

2、ShowAllSpan 可点击的“全文”的span类

public class ShowAllSpan extends ClickableSpan {

private OnAllSpanClickListener clickListener;
private boolean isPressed = false;
private Context context;

public ShowAllSpan(Context context, OnAllSpanClickListener clickListener){
    this.context = context;
    this.clickListener = clickListener;
}

@Override
public void onClick(View widget) {
    if (clickListener != null){
        clickListener.onClick(widget);
    }
}

public void setPressed(boolean pressed) {
    isPressed = pressed;
}

public interface OnAllSpanClickListener{
    void onClick(View widget);
}

@Override
public void updateDrawState(TextPaint ds) {
    if (isPressed){
        ds.bgColor = context.getResources().getColor(android.R.color.darker_gray);
    }else {
        ds.bgColor = context.getResources().getColor(android.R.color.transparent);
    }
    ds.setColor(context.getResources().getColor(android.R.color.holo_blue_light));
    ds.setUnderlineText(false);
}
}

3、PaintUtils 画笔工具类

public class PaintUtils 

/** 计算指定画笔下指定字符串需要的宽度*/
public static int getTheTextNeedWidth(Paint thePaint, String text) {
    float[] widths = new float[text.length()];
    thePaint.getTextWidths(text, widths);
    int length = widths.length, nowLength = 0;
    for (int i = 0; i < length; i++) {
        nowLength += widths[i];
    }
    return nowLength;
}
}

4、测试demo:

public class ShowAllTextActivity extends AppCompatActivity {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_showalltext);

    ShowAllTextView tv_1 = (ShowAllTextView) findViewById(R.id.tv_1);
    ShowAllTextView tv_2 = (ShowAllTextView) findViewById(R.id.tv_2);

    tv_1.setMaxShowLines(3);
    tv_1.setMyText("Dwayne Jones was a Jamaican 16-year-old who was killed by a violent mob " +
            "in Montego Bay on the night of 21 July 2013, after he attended a dance party " +
            "dressed in women's clothing. Perceived as effeminate, Jones had been bullied " +
            "in school and rejected by his father, and had moved into a derelict house in " +
            "Montego Bay with transgender friends. When some men at the dance party discovered " +
            "that the cross-dressing Jones was not a woman, they confronted and attacked him. " +
            "He was beaten, stabbed, shot, and run over by a car. Police investigated, " +
            "but the murder remains unsolved. The death made news internationally. " +
            "While voices on social media accused Jones of provoking his killers by ");
    tv_1.setOnAllSpanClickListener(new ShowAllSpan.OnAllSpanClickListener() {
        @Override
        public void onClick(View view) {
            Toast.makeText(ShowAllTextActivity.this, "点击了全文1", Toast.LENGTH_SHORT).show();
        }
    });

    tv_2.setMaxShowLines(5);
    tv_2.setMyText("水温刚刚合适,服侍我的是那个面目清秀的男孩,他把手慢慢的挪到我两腿之间,抚摸着我白皙的长腿,他小心翼翼的为我脱毛,神情紧张,生怕弄疼了我。\n" +
            "我就这样躺在白砂做的浴缸里,男孩轻轻的在我胸口,腹部均匀的涂抹浴盐,又在我的背部涂抹类似于橄榄油一样的护肤品。\n" +
            "男孩从一旁取出一些瓶瓶罐罐的香料,他把一些类似于花瓣一样的红色的颗粒撒在我的周围,并用纱布把那种名贵的香料挤出汁液淋在我身上。我的身体愈加的酥软,真的太舒服了,男孩的手法娴熟,让我如痴如醉,他不断的在我沐浴的水中添加美白和使我皮肤细腻的香料。\n" +
            "水温有些升高了。\n" +
            "男孩突然把我已经瘫软的身体翻了过来,他分开我的双腿……\n" +
            "他,他拿出了相机,居然在拍照,作为一只鸡,被偷拍是我们这一行的大忌,男孩把照片印在一本花名册上,我打赌那上面一定有很多鸡的照片,可能都是他找过的吧。\n" +
            "可是我却,我却居然有些喜欢上这个男孩了。\n" +
            "他还给我在水中沐浴的照片起了个奇怪的名字:“枸杞炖鸡汤。");
    tv_2.setOnAllSpanClickListener(new ShowAllSpan.OnAllSpanClickListener() {
        @Override
        public void onClick(View view) {
            Toast.makeText(ShowAllTextActivity.this, "点击了全文2", Toast.LENGTH_SHORT).show();
        }
    });
}
}

效果如下:

image.png

待优化:

int moreWidth = PaintUtils.getTheTextNeedWidth(getPaint(), "...全文");
            /**加上...全文 长度超过了textView的宽度, 则多减去5个字符*/
 if (getLayout().getLineRight(maxShowLines - 1) + moreWidth >= getLayout().getWidth()){
 this.setText(getText().subSequence(0, getLayout().getLineEnd(maxShowLines - 1) - 5));
 /**避免减5个字符后还是长度还是超出了,这里再减4个字符*/
 if (getLayout().getLineRight(maxShowLines - 1) + moreWidth >= getLayout().getWidth()){
this.setText(getText().subSequence(0, getLayout().getLineEnd(maxShowLines - 1) - 4));
                }
            }

判断全文是否超出了本行的逻辑处理比较简单,需求没那么严格,暂时就这样。如果要严格在行末的位置,可以一个一个的减字符,直到可以刚好把“...全文”放下。

备注:
代码地址:https://github.com/yang1006/ShowAllTextView

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

推荐阅读更多精彩内容