TextView自定義輕鬆實現下劃線、點選彈框

Android機動車發表於2019-02-28

前言

最近公司有意需求,就是類似於電子書,選擇一段文字然後做筆記,需要給做過的文字加下劃線,下劃線最後加一圖示按鈕,點選彈框顯示筆記內容。

立馬會想到使用TextView的fromHtml方法,給新增筆記的文字手動加標籤,或者使用SpanString類的相關方法設定標籤。

但是!

經過反覆測試,無論使用何種下劃線標籤或者SpanString設定下劃線,畫出的下劃線顏色始終和文字內容顏色一樣,還不能隨便定義顏色。更何況:我們需要在下劃線最後加圖示,並且能夠點選。看來這種方法不可行...

於是,便開始了我的自定義之路~~~~

先看效果圖:

這是純文字的TextView

這裡寫圖片描述

這是富文字的TextView

這裡寫圖片描述

分析

這裡寫圖片描述

要實現以上需求,應該從這幾個方面入手:

文字展示,普通文字呼叫TextView的setText方法既可,如果是富文字,就使用TextView的fromHtml方法,至於圖片如何展示,我在上一篇文章用TextView實現富文字展示,點選斷句和語音播報介紹過了,有興趣的可以跳轉閱讀,核心是攔截到圖片url然後自己實現載入圖片。

給TextView設定要劃線的起始位置和結束位置,需要計算出在哪些行進行繪製,每行又是從哪裡開始,到哪裡結束,注意第一行和最後一行。

然後就是在onDraw方法中對計算出的行進行逐行繪製,在最後一行的結束位置繪製筆記圖示(小圓圈)。

在TextView的onTouchEvent判斷按下位置是否是筆記圖示(小圓圈)的附近,是的話則彈框(PopupWindow)顯示。

文字顯示

這裡就不再重複累贅了,文字展示很簡單:

呼叫setText或fromHtml方法既可。

顏色等屬性設定

private Rect mRect;
private Paint mPaint;
private int mColor = 0xFFFFA200;
private float density;
private float mStrokeWidth;

// 筆記白點
private Paint mPointPaint;

// 開始各結束位置索引,startIndex必須大於等於endIndex
private int startIndex = 0;
private int endIndex = 0;

// 下劃線的位置(每次更新)
private float x_start, x_stop, x_diff;
private int baseline;
// 小圓圈的位置
private float notePointX, notePointY;

private int scrollY = 0;
複製程式碼

我們需要定義畫筆、畫筆顏色、線條粗細;開始位置的結束位置的索引。

還有就是下劃線的位置,因為我們是按行來畫,每畫完一行就會重新計算,尤其是橫向的結束位置,所以我將x的結束位置定義出來,每次都更新。

最後要將計算出的小圖示的x和y值保留,在onTouchEvent中會用到。

並初始化:

//獲取螢幕密度
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.2f);
複製程式碼

計算劃線位置

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;
	}
}
複製程式碼

我們先定義一個實體類,這個類中存放每行的索引,和對應每行上的一個開始位置索引,結束位置索引。

// 存放所有行,和對應的每行開始位置和結束位置
private List<TextIndex> indexs = new ArrayList<>();
// 存放需要繪製的行,和每行對應的開始和結束的位置
private List<TextIndex> drawIndexs = new ArrayList<>();
複製程式碼

定義兩個集合,分別存放所有行的資訊和需要繪製的行的資訊。

接下來開始計算:

for (int i = 0; i < indexs.size(); i++) {
	// 先確定開始位置
	if (startIndex >= indexs.get(i).start && startIndex <= indexs.get(i).end) {
		// 在確定結束位置
        if (endIndex >= indexs.get(i).start && endIndex <= indexs.get(i).end) {
			drawIndexs.add(new TextIndex(i, startIndex, endIndex));
			break;
		} else {
			// 結束位置不再此行的話,先記下起始位置,結束位置為本行最後一位
			drawIndexs.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;
		// 否則此行全畫
		} else {
			if (hasStart) {
				drawIndexs.add(new TextIndex(i, indexs.get(i).start, indexs.get(i).end));
			}
		}
	}
}
複製程式碼

思路是這樣的:

  1. 迴圈所有行;
  2. 如果要繪製的開始位置在這行中,並且結束位置也在這行中,直接向要繪製的集合中新增一個物件,終止迴圈;
  3. 如果開始位置在這行中,但結束位置不在這行中,則新增一個結束位置是本行結束位置的物件到要繪製的集中中,繼續下次迴圈;
  4. 如果結束位置在此行,則新增開始位置為本行開始位置,結束位置為自己結束位置的物件到集合中;
  5. 否則,將整行填入集合。

繪製下劃線

for (int i = 0; i < drawIndexs.size(); i++) {

	// getLineBounds得到這一行的外包矩形,
	// 這個字元的頂部Y座標就是rect的top 底部Y座標就是rect的bottom
	baseline = getLineBounds(drawIndexs.get(i).line, mRect);

	//要得到這個字元的左邊X座標 用layout.getPrimaryHorizontal
	//得到字元的右邊X座標用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);
}
複製程式碼

核心使用的是canvas的drwaLine方法進行繪製。

迴圈所有要繪製的集合,得到這一行的外包矩形,根據當前行的開始和結束位置,算出橫向x的開始和結束位置;baseline是字元底部y的值,這樣就可以繪製劃線了!

繪製筆記圖示

/**
 * 在最後位置繪製橢圓和三個白點
 * 注意這裡的所有值都不能給死,否則無法適配
 */
if (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);
}
複製程式碼

如果是最後一行的,在本行的結束位置開始繪製筆記圖示。

使用canvas.drawCircle繪製圓圈,而圓的圓形座標可以下劃線最後的位置進行繪製。

再用另一條畫筆繪製三個白點,這個白點可以使用canvas.drawPoints繪製,傳入一個float型別陣列,下標是奇數,表示點的x值,下表為偶數,表示點的y值,也就是說float陣列的個數必須是偶數個,或者說是點數的兩倍。

圖示點選

    @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) // 設定佈局
                            .setContext(getContext()) // 設定上下文
                            .setOutSideCancle(true) // 點選外部消失
                            .setHeight(LinearLayout.LayoutParams.WRAP_CONTENT) // 設定高度
                            .setWidth(LinearLayout.LayoutParams.WRAP_CONTENT) // 設定寬度
                            .setAnimation(R.style.anim_pop) // 設定動畫
                            .build() // 構建
                            .showAtLocation(this, Gravity.TOP | Gravity.LEFT, (int) notePointX, (int) notePointY - scrollY);

                    TextView tv_pop = (TextView) popWindow.getItemView(R.id.tv_pop);
                    tv_pop.setText("我愛北京天安門,天安門上太陽升");
                }

                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }
複製程式碼

在上一步繪製小圖示時,就將圖示的x和y值儲存,在onTouchEvent中,判斷按下的位置是否在小圖示位置的“附近”,是的話就彈框顯示筆記內容。

這裡的彈框用的是我之前封裝的JsPopupWindow,有興趣的話可以點選閱讀github.com/shuaijia/Js…

這裡需要注意,如果TextView外層被ScrollView包裹,在彈框是就需要縱軸方向上減去ScrollView的偏移量。也就是TextView需要知道ScrollView的縱向偏移量,這裡我設定了方法,將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);
	}
});
複製程式碼

這樣就實現了我們如上圖展示的,給TextView繪製下劃線和圖示點選,彈框的效果。

想獲取更多精彩,請關注我的微信公眾號——Android機動車

TextView自定義輕鬆實現下劃線、點選彈框

相關文章