preface

Recently, the company has an intentional demand, which is similar to an e-book, to select a paragraph of text and take notes. It needs to underline the text that has been done, and then add a icon button at the end of the underline, and click the popup box to display the notes.

You immediately think of using the fromHtml method of TextView to manually tag the text that you’re adding notes to, or using the related methods of the SpanString class to tag it.

But!

After repeated testing, no matter which underline label or SpanString is used to set the underline, the underline color is always the same as the text, and the color cannot be defined arbitrarily. What’s more: we need an icon at the end of the underscore, and it should be clickable. It seems that this method is not feasible…

Then, began my custom road ~~~~

First look at the renderings:

This is a plain text TextView

This is a Rich text TextView

Analysis of the

To meet the above requirements, we should start from the following aspects:

For text display, you can call TextView’s setText method for normal text. For rich text, you can use TextView’s fromHtml method. As for how to display pictures, I used TextView to achieve rich text display in the last article. The core is to intercept the image URL and then load the image yourself.

To set the starting and ending position of the line to be crossed, you need to figure out which lines to draw on, where each line starts and ends, and pay attention to the first and last lines.

The next step is to draw the calculated lines line by line in the onDraw method, drawing the note icon (small circle) at the end of the last line.

The onTouchEvent of the TextView determines whether the pressed position is near the note icon (small circle). If it is, the PopupWindow will be displayed.

Text display

Without any repetition, the text presentation is simple:

Either setText or fromHtml methods are called.

Color and other properties

private Rect mRect;
private Paint mPaint;
private int mColor = 0xFFFFA200;
private float density;
private floatmStrokeWidth; Private Paint mPointPaint; EndIndex private int startIndex = 0; private int endIndex = 0; // Underline position (each update) privatefloatx_start, x_stop, x_diff; private int baseline; // The position of small circle is privatefloat notePointX, notePointY;

private int scrollY = 0;
Copy the code

We need to define the brush, the brush color, the line thickness; Index of the end position of the start position.

And then there’s the position of the underscore, because we’re drawing in rows, and we’re recalculating every row, especially the end of the horizontal line, so I’ll define the end of the x, and I’ll update it every time.

Finally, keep the calculated x and y values of the little ICONS, which will be used in the onTouchEvent.

And initialize:

// Get the screen density = getResources().getDisplayMetrics().density; mStrokeWidth = density; mRect = new Rect(); mPaint = new Paint(); mPaint.setStyle(Paint.Style.FILL_AND_STROKE); mPaint.setColor(mColor); mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(mStrokeWidth);

mPointPaint = new Paint();
mPointPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mPointPaint.setColor(Color.WHITE);
mPointPaint.setAntiAlias(true); MPointPaint. SetStrokeWidth (2.2 f);Copy the code

Calculate scribing position

class TextIndex { int line; int start; int end; public TextIndex(int line, int start, int end) { this.line = line; this.start = start; this.end = end; }}Copy the code

Let’s define an entity class that holds an index for each row, and a start and end index for each row.

Private List<TextIndex> indexs = new ArrayList<>(); private List<TextIndex> indexs = new ArrayList<>(); Private List<TextIndex> drawIndexs = new ArrayList<>();Copy the code

Define two collections that hold information about all rows and information about rows that need to be drawn.

Let’s start the calculation:

for(int i = 0; i < indexs.size(); I++) {// first determine the start positionif(startIndex >= indexs.get(I).start && startIndex <= indexs.get(I).end) {// At the end positionif (endIndex >= indexs.get(i).start && endIndex <= indexs.get(i).end) {
			drawIndexs.add(new TextIndex(i, startIndex, endIndex));
			break;
		} elseDrawindexs. add(new TextIndex(I, startIndex, indexs.get(I).end)); // Add (new TextIndex(I, startIndex, indexs.get(I).end)); hasStart =true;
			continue; }}else {
		if (endIndex >= indexs.get(i).start && endIndex <= indexs.get(i).end) {

			drawIndexs.add(new TextIndex(i, indexs.get(i).start, endIndex));
			hasStart = false;
			break; // If not, draw the whole line.else {
			if(hasStart) { drawIndexs.add(new TextIndex(i, indexs.get(i).start, indexs.get(i).end)); }}}}Copy the code

The thinking goes like this:

  1. Loop through all rows;
  2. If the start position and end position of the draw are in the same row, add an object directly to the set to be drawn, ending the loop;
  3. If the start position is in this row, but the end position is not in this row, add an object whose end position is the end position of the line to the set to be drawn, and continue the next loop;
  4. If the end position is in this row, add an object whose start position is the start position of the row and end position is its end position to the collection.
  5. Otherwise, the entire row is filled into the collection.

Draw underline

for(int i = 0; i < drawIndexs.size(); I++) {// getLineBounds gets the bounding rectangle of this line, // Bottom bounds = getLineBounds(drawindexs.get (I).line, mRect); // Bottom bounds = getLineBounds(drawindexs.get (I).line, mRect); / / to get the character to the left of the X coordinate with layout. GetPrimaryHorizontal / / get the characters on the right side of the X coordinate with layout. GetSecondaryHorizontal x_start = layout.getPrimaryHorizontal(drawIndexs.get(i).start); x_diff = layout.getPrimaryHorizontal(drawIndexs.get(i).start + 1) - x_start; x_stop = layout.getPrimaryHorizontal(drawIndexs.get(i).end - 1) + x_diff; canvas.drawLine(x_start, baseline + mStrokeWidth + 8, x_stop, baseline + mStrokeWidth + 8, mPaint); }Copy the code

The core uses the Canvas’s drwaLine method for drawing.

Loop through all the sets to be drawn to get the enclosing rectangle of the row, and calculate the start and end positions of the horizontal x based on the start and end positions of the current row; Baseline is the value of y at the bottom of the character so that the line can be drawn!

Draw note icon

/** * Draw the ellipse and three white dots at the last positionif (i == drawIndexs.size() - 1) {
	canvas.drawCircle(x_stop + mStrokeWidth * 4, baseline + mStrokeWidth + 8, mStrokeWidth * 4, mPaint);
	notePointX = x_stop + mStrokeWidth * 4;
	notePointY = baseline + mStrokeWidth + 8;
	Log.e(TAG, "onDraw: x=" + (x_stop + mStrokeWidth * 4) + "y=" + (baseline + mStrokeWidth + 8));
	float[] pts = {x_stop + mStrokeWidth * 2, baseline + mStrokeWidth + 8, x_stop + mStrokeWidth * 4, baseline + mStrokeWidth + 8, x_stop + mStrokeWidth * 6, baseline + mStrokeWidth + 8};
	canvas.drawPoints(pts, mPointPaint);
}
Copy the code

If it is the last line, start drawing the note icon at the end of the line.

Draw a circle using Canvas. DrawCircle, and the circle coordinates of the circle can be drawn by underlining the last position.

Draw three white dots using canvas. DrawPoints. Pass in an array of type float with an odd subscript representing the x value of the point and an even number representing the y value of the point.

Click on the icon

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onTouchEvent: " + event.getX() + "" + event.getY());
                Log.e(TAG, "onTouchEvent: " + event.getX() + "" + getScrollY());

                if(Math.abs(event.getX() - notePointX) <= 30 && Math.abs(event.getY() - notePointY) <= 30) { JsPopupWindow popWindow = new Jspopupwindow.builder ().setcontentviewid (r.layout.dialog_popupwindow) // set layout.setcontext (getContext()) // setContext .setOutSideCancle(true) / / click outside disappear. SetHeight (LinearLayout. LayoutParams. WRAP_CONTENT). / / set the height setWidth (LinearLayout. LayoutParams. WRAP_CONTENT) / / set width. SetAnimation (R.s tyle. Anim_pop) / / set the animation. The build () / / build showAtLocation (this, Gravity. TOP | Gravity. The LEFT, (int) notePointX, (int) notePointY - scrollY); TextView tv_pop = (TextView) popWindow.getItemView(R.id.tv_pop); tv_pop.setText("I love Tian 'anmen in Beijing, where the sun rises.");
                }

                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }
Copy the code

When the small icon is drawn in the previous step, the icon’s X and Y values are saved. In onTouchEvent, determine whether the pressed position is “near” the small icon’s position. If so, the note will be displayed in the box.

The pop-up here is using the JsPopupWindow that I encapsulated earlier. If you are interested, you can read github.com/shuaijia/Js… .

Now, the thing to notice here is that if the TextView is wrapped around a ScrollView, you’re going to subtract the ScrollView offset from the vertical axis. So the TextView needs to know the vertical offset of the ScrollView, and I’ve set up a method here to pass in the offset of the ScrollView.

scroll_rich.setOnScrollChangeListener(new View.OnScrollChangeListener() { @Override public void onScrollChange(View view, int i, int i1, int i2, int i3) { tv_rich_note.setMScrollY(i1); }});Copy the code

This gives the TextView the effect of drawing an underscore and clicking the icon, as shown above.

To get more exciting, please follow my wechat official account – Android Vehicles!