Support multi-line TextView with labels at the end

During the project development process, I encountered a requirement on the UI. In line with the principle of not reinventing the wheel and agile development, I searched on the Internet and found that I should do it myself. If I can’t find such a requirement, let’s take a look at us first. Effect.

In summary, there are three points to note:

  • The vip part at the end is a whole and cannot be wrapped;
  • When there is a single line or less than 2 lines, the following vip part is displayed directly behind the text;
  • After more than 2 lines, the text part is displayed... and the vip part is guaranteed to be displayed at the end.

So for this effect, we first rule out the way of combining multiple controls horizontally, because this will cause the vip part at the end to be displayed on the right side of the entire TextView instead of being placed behind the text.

The first thing that comes to mind is to use rich text. Everyone is familiar with SpannableString. That’s right. If the vip part is a whole, then we can’t treat it as text, because it will encounter line breaks, which is not satisfactory. It is required, so we have to process it into a picture, so that we don't have to care whether it wraps or not, TextView will automatically handle it for us. But our vip part is dynamically set, and the +15 in it is the value issued by the interface, so we must not use hard-coded pictures in this place, so how to deal with it?

Is there a solution to convert the layout into a picture? Have! Of course there is! as follows:

 private Bitmap createTaskTag(String num){
        View layout = LayoutInflater.from(getActivity()).inflate(R.layout.layout_task_tag_end, null);
        TextView tvNum = layout.findViewById(;
        tvNum.setText(String.format("member extra+%s Yundou",num));
        layout.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
        layout.layout(0, 0,  ScreenUtils.dp2px(102), layout.getMeasuredHeight());
        Bitmap bitmap = Bitmap.createBitmap(layout.getDrawingCache());
        return  bitmap;

Now that the picture is already there, it's easy, go directly to ImageSpan, is this over?
We found that when there are more than two lines, the end directly becomes ... not displayed, so we have to deal with the problem of text display. The last line must reserve the position of pictures and ..., and then use the remaining space for strings Intercept operation. as follows:

    private CharSequence getNewTextByConfig() {
        if (TextUtils.isEmpty(mOrigText)) {
            return mOrigText;

        mLayout = getLayout();
        if (mLayout != null) {
            mLayoutWidth = mLayout.getWidth();

        if (mLayoutWidth <= 0) {
            if (getWidth() == 0) {
                if (mFutureTextViewWidth == 0) {
                    return mOrigText;
                } else {
                    mLayoutWidth = mFutureTextViewWidth - getPaddingLeft() - getPaddingRight();
            } else {
                mLayoutWidth = getWidth() - getPaddingLeft() - getPaddingRight();

        mTextPaint = getPaint();

        mTextLineCount = -1;
        mLayout = null;
        mLayout = new DynamicLayout(mOrigText, mTextPaint, mLayoutWidth, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
        mTextLineCount = mLayout.getLineCount();

//        if (mTextLineCount <= mMaxLinesOnShrink) {
//            return mOrigText;
//        }
        int maxLine = 0;
        if (mTextLineCount > 1) {
            maxLine = mMaxLinesOnShrink - 1;
            SpannableStringBuilder ssbShrink = new SpannableStringBuilder(mOrigText);
            ssbShrink.setSpan(imgSpan1, ssbShrink.length() - 1, ssbShrink.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            return ssbShrink;
        int indexEnd = getValidLayout().getLineEnd(maxLine);
        int indexStart = getValidLayout().getLineStart(maxLine);
        int indexEndTrimmed = indexEnd
                - getLengthOfString(mEllipsisHint)
                - getLengthOfString(mGapToExpandHint);

        if (indexEndTrimmed <= indexStart) {
            indexEndTrimmed = indexEnd;

        int remainWidth = getValidLayout().getWidth() -
                (int) (mTextPaint.measureText(mOrigText.subSequence(indexStart, indexEndTrimmed).toString()) + 0.5) - bitmap1.getWidth();
        float widthTailReplaced = mTextPaint.measureText(getContentOfString(mEllipsisHint)
                + getContentOfString(mGapToExpandHint));

        int indexEndTrimmedRevised = indexEndTrimmed;
        if (remainWidth > widthTailReplaced) {
            int extraOffset = 0;
            int extraWidth = 0;
            while (remainWidth > widthTailReplaced + extraWidth) {
                if (indexEndTrimmed + extraOffset <= mOrigText.length()) {
                    extraWidth = (int) (mTextPaint.measureText(
                            mOrigText.subSequence(indexEndTrimmed, indexEndTrimmed + extraOffset).toString()) + 0.5);
                } else {
            indexEndTrimmedRevised += extraOffset - 1;
        } else {
            int extraOffset = 0;
            int extraWidth = 0;
            while (remainWidth + extraWidth < widthTailReplaced) {
                if (indexEndTrimmed + extraOffset > indexStart) {
                    extraWidth = (int) (mTextPaint.measureText(mOrigText.subSequence(indexEndTrimmed + extraOffset, indexEndTrimmed).toString()) + 0.5);
                } else {
            indexEndTrimmedRevised += extraOffset;

        String fixText = removeEndLineBreak(mOrigText.subSequence(0, indexEndTrimmedRevised));
        SpannableStringBuilder ssbShrink = new SpannableStringBuilder(fixText);
        if (remainWidth <= widthTailReplaced) {

        if (issetSpecialColor) {
            int lenth = ssbShrink.length();
            if (specialColorLenth <= lenth) {
                lenth = specialColorLenth;
            ssbShrink.setSpan(colorSpan, specialColorStart, lenth, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);


        ssbShrink.setSpan(imgSpan1, ssbShrink.length() - 1, ssbShrink.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

        return ssbShrink;

The code also handles the following bug s:

Summary: The important thing about this UI requirement is not the code part, but seeing this requirement, if you think about it, how to deal with it. Of course, this approach may not be optimal, but I think it is the most convenient to deal with. If there is more Good plan, welcome to leave a message in the comment area.

Because there is only one class, it is placed on csdn.
Complete resource download

Tags: Android TextView

Posted by mattgleeson on Sun, 20 Nov 2022 14:18:42 +0530