An implementation of an underlined footer control

Reference image

Code Implementation

import android. content. Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android. graphics. Paint;
import android.graphics.Rect;
import android.text.Layout;
import android.text.TextPaint;
import android.util.AttributeSet;

import androidx.appcompat.widget.AppCompatTextView;

import com.hl.sun.R;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class UnderLineNoteTextView extends AppCompatTextView {<!-- -->
    //underline starting point and footnote information
    private ArrayList<TextLineInfo> mInfos = new ArrayList<>();
    private Rect mRect;
    private Paint mLinePaint;
    private TextPaint mTxtPaint;
    private int mUnderLineColor;
    private int mNoteTxtColor;
    private float mUnderLineWidth;
    private float mNoteTopMargin = 0;
    private float mUnderLineTopMargin = 0;
    private float mNoteTextSize = 20;
    private float maxSpace;

    public UnderLineNoteTextView(Context context) {<!-- -->
        this(context, null, 0);
    }

    public UnderLineNoteTextView(Context context, AttributeSet attrs) {<!-- -->
        this(context, attrs, 0);
    }

    public UnderLineNoteTextView(Context context, AttributeSet attrs, int defStyleAttr) {<!-- -->
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
    }

    private void init(Context context, AttributeSet attrs, int defStyleAttr) {<!-- -->
        //Get custom attributes
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.UnderlinedTextView, defStyleAttr, 0);

        mUnderLineColor = array.getColor(R.styleable.UnderlinedTextView_underlineColor, 0xFFFF0000);
        mNoteTxtColor = array.getColor(R.styleable.UnderlinedTextView_noteTextColor, 0xFFFF0000);

        mUnderLineWidth = array.getDimension(R.styleable.UnderlinedTextView_underlineWidth, 20);
        mUnderLineTopMargin = array.getDimension(R.styleable.UnderlinedTextView_underLineTopMargin, 0);

        mNoteTextSize = array.getDimension(R.styleable.UnderlinedTextView_noteTextSize, 20);
        mNoteTopMargin = array.getDimension(R.styleable.UnderlinedTextView_noteTopMargin, 0);

        //Set the line spacing: (footnote text size + footnote top margin) compared with (underline width + underline top margin), take the maximum value
        maxSpace = Math.max(mNoteTextSize + mNoteTopMargin, mUnderLineWidth + mUnderLineTopMargin);
        setLineSpacing(maxSpace, 1F);
        //setPadding has been overridden
        setPadding(getLeft(), getTop(), getRight(), getBottom());

        array. recycle();

        ///underline brush
        mRect = new Rect();
        mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        //Set the underline with rounded corners
        mLinePaint.setStrokeCap(Paint.Cap.ROUND);
        mLinePaint.setStyle(Paint.Style.STROKE);
        mLinePaint.setColor(mUnderLineColor);
        mLinePaint.setStrokeWidth(mUnderLineWidth);

        ///Footnote brush
        mTxtPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mTxtPaint.setColor(mNoteTxtColor);
        mTxtPaint.setTextSize(mNoteTextSize);
    }

    @Override
    protected void onDraw(Canvas canvas) {<!-- -->
        //Get how many lines the TextView displays
        int count = getLineCount();
        //Get the layout of TextView
        final Layout layout = getLayout();
        final float getRight = getRight() - getLeft();

        float x_start, x_stop;
        int firstCharInLine, lastCharInLine;

        // loop through each row
        for (int i = 0; i < count; i ++ ) {<!-- -->
            //getLineBounds gets the outsourcing rectangle of this line, the top Y coordinate of this character is the top of the rect, and the bottom Y coordinate is the bottom of the rect
            int baseline = getLineBounds(i, mRect);
            //Returns the text offset at the beginning of the specified line (that is, the number of the first word of each line in the entire text) (0 & amp;hellip; getLineCount()). If the specified number of lines is equal to the number of lines, returns the length of the text.
            firstCharInLine = layout. getLineStart(i);
            // Returns the text offset after the last character on the specified line.
            lastCharInLine = layout. getLineEnd(i);

            Iterator<TextLineInfo> iterator = mInfos. iterator();
            while (iterator.hasNext()) {<!-- -->
                x_start = 0;
                x_stop = 0;
                TextLineInfo info = iterator. next();
                //The underline starts from the first word
                int lineStart = info. getLineStart();
                //The underscore ends at the first word
                int lineEnd = info. getLineEnd();

                //Determine whether the underline is displayed before the current line
                if (lineEnd <= firstCharInLine) {<!-- -->
                    //Delete the data that has been displayed in the upper row, so you can traverse less
                    //But when the Activity executes onResume, it will go to the onDraw method again, resulting in lack of data
                    //iterator. remove();
                } else {<!-- -->
                    / / Determine whether the current line has an underline to display
                    if (lineStart < lastCharInLine & amp; & amp; lineEnd >= firstCharInLine) {<!-- -->
                        //If the underline starts from the previous line, then firstCharInLine<lineStart
                        //getPrimaryHorizontal: Get the X coordinate of the left side of the character, that is, the X coordinate of the starting point of the underline
                        x_start = layout.getPrimaryHorizontal(Math.max(lineStart, firstCharInLine));
                        //Prevent underline rounded corners from exceeding the text width
                        x_start += mUnderLineWidth / 2;
                        //If the underscore ends on the next line, then lineEnd<lastCharInLine
                        int endNum = Math.min(lineEnd, lastCharInLine);
                        //If the underline reaches the end of the line, the value of x_stop is the X coordinate of the end of the line getLineRight
                        x_stop = (endNum == lastCharInLine) ? layout.getLineRight(i) : layout.getPrimaryHorizontal(endNum);
                        //Prevent underline rounded corners from exceeding the text width
                        x_stop -= mUnderLineWidth / 2;
                    }

                    //By changing x_stop to judge whether the current line has an underline
                    if (x_stop > 0) {<!-- -->
                        //Y coordinate of the vertical midpoint of the underline = half the height of the underline + margin at the top of the underline + remaining value
                        float bottomY = baseline + mUnderLineWidth / 2 + mUnderLineTopMargin + 2;
                        canvas.drawLine(x_start, bottomY, x_stop, bottomY, mLinePaint);
                        //Judge whether the underline wraps in the current line: if the underline wraps, the current line does not draw a footnote, and draws it in the last line
                        if (lineEnd <= lastCharInLine) {<!-- -->
                            ///Draw the footnote
                            String txt = info. getNote();
                            //Calculate whether the current row can fit
                            float txtWidth = mTxtPaint.measureText(txt);
                            //The difference between the end point of the line and the end point of the underline is not enough to put down the text, so the end point of the text should be moved to the left
                            float txtEndX = ((getRight - x_stop) < txtWidth) ? getRight : (x_stop + txtWidth);
                            //X coordinate of text start point = X coordinate of text end point - text width
                            float txtStartX = txtEndX - txtWidth;
                            canvas.drawText(txt, txtStartX, baseline + mNoteTextSize + mNoteTopMargin, mTxtPaint);
                        }
                    }
                }
            }
        }
        super.onDraw(canvas);
    }

    public int getUnderLineColor() {<!-- -->
        return mUnderLineColor;
    }

    public void setUnderLineColor(int mColor) {<!-- -->
        this.mUnderLineColor = mColor;
        invalidate();
    }

    @Override
    public void setPadding(int left, int top, int right, int bottom) {<!-- -->
        //Leave enough underscores, footnotes, and margins at the bottom to show the full distance
        super.setPadding(left, top, right, Math.round(bottom + maxSpace));
    }

    public float getUnderlineWidth() {<!-- -->
        return mUnderLineWidth;
    }

    public void setUnderlineWidth(float mStrokeWidth) {<!-- -->
        this.mUnderLineWidth = mStrokeWidth;
        invalidate();
    }

    public void setUnderInfo(Collection<TextLineInfo> infos) {<!-- -->
        if (infos != null) {<!-- -->
            mInfos. clear();
            mInfos.addAll(infos);
            invalidate();
        }
    }
}

/**
 * Function: Underline starting point and footnote information
 */
class TextLineInfo(var lineStart: Int = 0, var lineEnd: Int = 0, var note: String? = null) {<!-- -->
}

Use

 <com.hl.sun.ui.widget.UnderLineNoteTextView
        android:id="@ + id/note_tv_underline"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="30dp"
        android:textSize="14dp"
        app:noteTextColor="#FF9A27"
        app:noteTextSize="10dp"
        app:noteTopMargin="4dp"
        app:underLineTopMargin="1dp"
        app:underlineColor="#33FF9F46"
        app:underlineWidth="4dp" />
val lineNoteTextView = findViewById<UnderLineNoteTextView>(R.id.note_tv_underline)
lineNoteTextView.text = XXX
lineNoteTextView. setUnderInfo(getSentenceInfo())

Test data

getSentenceInfo data

[{<!-- -->"lineEnd":5,"lineStart":0,"note":"0.1 points"},{<!-- -->"lineEnd":20,"lineStart ":18,"note":"0.2 points"},{<!-- -->"lineEnd":108,"lineStart":45,"note":"9.0 points"},{<!-- - ->"lineEnd":136,"lineStart":135,"note":"0.5 points"},{<!-- -->"lineEnd":214,"lineStart":174,"note":"2.0 points"},{<!-- -->"lineEnd":216,"lineStart":214,"note":"0.5 points"},{<!-- -->"lineEnd":223,"lineStart ":221,"note":"0.5 points"},{<!-- -->"lineEnd":241,"lineStart":239,"note":"0.5 points"},{<!-- - ->"lineEnd":279,"lineStart":265,"note":"0.5 points"}]

Copy
lineNoteTextView.text=
spring is coming
Looking forward, looking forward to, the east wind is coming, and the footsteps of spring are approaching. Everything looked like she had just woken up, and Xinxin opened her eyes. The mountains have become moist, the water has grown, and the sun has blushed. The grass secretly drilled out of the soil, tender and green. In the garden, in the fields, look around, there are a lot of them. Sitting, lying down, rolling twice, kicking a few balls, running a few times, catching and hiding a few times. Peach trees, apricot trees, and pear trees with soft grass, if you don’t let me or I don’t let you, the wind is soft and the grass is soft, they are all full of flowers. The red ones are like fire, the pink ones are like clouds, and the white ones are like snow with a sweet smell. When you close your eyes, the tree seems to be full of peaches, apricots, and pears! Hundreds of bees are buzzing under the flowers. Then, big and small butterflies fly around. The wild flower everywhere is: Various, has the name, does not have the name, disperses in the thick patch of grass, looks like the eye, looks like the star, but also winks winks.