Android——TextView實現虛線下劃線並支持點擊

在這裏插入圖片描述
效果圖如上圖,大家可以看到今天要實現的功能主要有虛線下劃線點擊文本
下面我們來分別分析下實現原理和知識點,最後給大家放上關鍵代碼。

虛線下劃線

給文本添加下劃線相信大家都會,這不就是富文本的內容嗎?提到富文本大家可能會想到SpannableStringBuilderClickableSpan的默認效果就是帶下劃線。但今天我們的目標是虛線下劃線,所以我們可能要自己手動改造下了。這裏我採用的是自定義view手動畫線的方式實現。

大致思路就是我們需要算出每一段下劃線的位置,然後在onDraw方法中根據座標在劃線。實現步驟大致爲:

1、計算下劃線座標:

分兩種情況討論:

1)下劃線都在一行上面;
2)下劃線不在一行上:

	1)保存第一行的座標;
	2)保存最後一行的座標;
	3)計算折行整行的座標;

注意:計算座標時我們仍然會用到Layout提供的一些方法。

  • getLineForOffset(int offset)獲取指定字符的行號;
  • getLineBounds(int line, Rect bounds)獲取指定行的所在的區域;
  • getPrimaryHorizontal(int offset) 獲取指定字符的左座標;
  • getSecondaryHorizontal(int offset)獲取指定字符的輔助水平偏移量;
  • getLineMax(int line)獲取指定行的寬度,包含縮進但是不包含後面的空白,可以認爲是獲取文本區域顯示出來的一行的寬度;
  • getLineStart(int line)獲取指定行的第一個字符的下標;

2、設置畫筆樣式

paint = new Paint();
paint.setStyle(Paint.Style.STROKE);//描邊
paint.setStrokeWidth(6);//描邊寬度
setHighlightColor(Color.TRANSPARENT);//設置選中文字背景色高亮顯示

Path path = new Path();
path.addCircle(0, 0, 2, Path.Direction.CCW);
PathEffect effects = new PathDashPathEffect(path, 6, 1, PathDashPathEffect.Style.ROTATE);//設置路徑樣式
paint.setPathEffect(effects);

注意:
1、設置高亮顏色,如果不設置會取ClickableSpan的默認高亮顏色;
2、設置路徑樣式PathEffect,如果不設置的話就是直線效果了;

點擊TextView的某一段文字

ClickableSpan可以讓我們在點擊TextView相應文字時響應點擊事件,比如常用的URLSpan,會在點擊時打開相應的鏈接。
這裏我們需要點擊文字並彈出PopupWindow,所以需要重寫ClickableSpan,根據自己的需求來開發onClick接口;

注意:重寫時,要記得去掉ClickableSpan的默認下劃線,修改選中文字的顏色;
關鍵代碼如下:
UnderlineTextView

public class UnderlineTextView extends AppCompatTextView {
    private List<UnderLineOptions> underLineOptionsList = new ArrayList<>();
    private Paint paint;
    private Path path = new Path();

    public UnderlineTextView(Context context) {
        this(context, null);
    }

    public UnderlineTextView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public UnderlineTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        paint = new Paint();
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(6);
        setHighlightColor(Color.TRANSPARENT);//設置選中文字背景色高亮顯示
    }

    //計算某一個字符的座標的方法,它返回一個數組裏面存儲了座標信息,依次是左,上,右,下
    private float[] measureXY(int offset) {
        float[] floats = new float[4];
        Layout layout = getLayout();
        int line = layout.getLineForOffset(offset);
        Rect rect = new Rect();
        layout.getLineBounds(line, rect);
        //左
        floats[0] = layout.getPrimaryHorizontal(offset) + getPaddingLeft();
        //上
        floats[1] = rect.top + getPaddingTop();
        //右
        floats[2] = layout.getSecondaryHorizontal(offset) + getPaddingRight();
        //下
        floats[3] = rect.bottom + getPaddingBottom();

        return floats;
    }

    public void setLine(@NonNull UnderLineOptions options) {
        post(() -> {
            if (!addOptions(options)) {
                return;
            }
            invalidate();
        });
    }

    public void setLines(@NonNull List<UnderLineOptions> optionsList) {
        underLineOptionsList.clear();
        post(() -> {
            for (UnderLineOptions options : optionsList) {
                if (!addOptions(options)) {
                    break;
                }
            }
            invalidate();
        });
    }

    public boolean addOptions(UnderLineOptions underLineOptions) {
        int start = underLineOptions.getLineStart();
        int end = underLineOptions.getLineEnd();

        if (start > getText().toString().length() || end < 0) {
            return false;
        }

        start = start < 0 ? 0 : start;
        end = end > getText().toString().length() ? getText().toString().length() : end;
        underLineOptions.setContent(getText().toString().substring(start, end));

        if (underLineOptions.getClickableSpan() != null) {
            underLineOptions.getClickableSpan().setStart(start);
            underLineOptions.getClickableSpan().setEnd(end);
            underLineOptions.getClickableSpan().setContent(getText().toString().substring(start, end));
        }

        // 可以通過這種方法獲取被這一部分是否可以被點擊
//        ClickableSpan[] links = ((Spannable) getText()).getSpans(start,end, ClickableSpan.class);
//        System.out.println(getSelectionStart());
//        System.out.println(getSelectionEnd());
//        System.out.println(links.length > 0 ? links[0] : links);

        float[] startXY = measureXY(start);
        float[] endXY = measureXY(end);
        List<float[]> listXY = new ArrayList<>();
        if (startXY[1] == endXY[1]) {//如果只有一行
            listXY.add(startXY);
            listXY.add(endXY);
            //找到彈出框的中間點
            if (underLineOptions.getClickableSpan() != null) {
                int x = (int) (startXY[0] + (endXY[0] - startXY[0]) / 2);
                underLineOptions.getClickableSpan().setX(x);
                underLineOptions.getClickableSpan().setY((int) startXY[3]);
            }
            underLineOptions.setLineXYs(listXY);
        } else {//處理折行情況
            // 對於折行的彈窗,只能根據需求來做了。
            int lineStart = getLayout().getLineForOffset(start);
            int lineEnd = getLayout().getLineForOffset(end);
            int lineNum = lineStart;

            while (lineNum <= lineEnd) {
                Rect rect = new Rect();
                getLayout().getLineBounds(lineNum, rect);
                if (lineNum == lineStart) {//第一行
                    float[] endXYN = new float[]{getLayout().getLineMax(lineNum) + measureXY(getLayout().getLineStart
                            (lineNum))[0], startXY[1], getLayout().getLineMax(lineNum) + measureXY(getLayout()
                            .getLineStart(lineNum))[2], startXY[3]};
                    listXY.add(startXY);
                    listXY.add(endXYN);
                    //找到彈出框的中間點
                    if (underLineOptions.getClickableSpan() != null) {
                        int x = (int) (startXY[0] + (endXYN[0] - startXY[0]) / 2);
                        underLineOptions.getClickableSpan().setX(x);
                        underLineOptions.getClickableSpan().setY((int) startXY[3]);
                    }
                } else if (lineNum == lineEnd) {//最後一行
                    float[] startXYN = new float[]{measureXY(getLayout().getLineStart(lineNum))[0], endXY[1],
                            measureXY(getLayout().getLineStart(lineNum))[2], endXY[3]};
                    listXY.add(startXYN);
                    listXY.add(endXY);
                } else {
                    Rect rect1 = new Rect();
                    getLayout().getLineBounds(lineNum, rect1);
                    float[] startXYN = new float[]{measureXY(getLayout().getLineStart(lineNum))[0], rect.top +
                            getPaddingTop(), measureXY(getLayout().getLineStart(lineNum))[2], rect.bottom +
                            getPaddingTop()};
                    float[] endXYN = new float[]{getLayout().getLineMax(lineNum) + measureXY(getLayout().getLineStart
                            (lineNum))[0], rect.top + getPaddingTop(), getLayout().getLineMax(lineNum) + measureXY
                            (getLayout().getLineStart(lineNum))[2], rect.bottom + getPaddingTop()};
                    listXY.add(startXYN);
                    listXY.add(endXYN);
                }
                lineNum++;
            }
            underLineOptions.setLineXYs(listXY);
        }
        underLineOptionsList.add(underLineOptions);
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (UnderLineOptions options : underLineOptionsList) {
            if (options.getLineXYs() != null) {
               if (options.getLineStyle() == UnderLineOptions.Style.LINE_STYLE_DOTTED) {
                   Path path = new Path();
                    path.addCircle(0, 0, 2, Path.Direction.CCW);
                    PathEffect effects = new PathDashPathEffect(path, 6, 1, PathDashPathEffect.Style.ROTATE);//設置路徑樣式
                   paint.setPathEffect(effects);
               } else {
                    paint.setPathEffect(null);
                }
                paint.setColor(options.getLineColor());
                for (int i = 0; i < options.getLineXYs().size(); i++) {
                    Log.d("lixx", i + " xy->  " + options.getLineXYs().get(i)[0] + "," + options.getLineXYs().get(i)
                            [1] + "," + options.getLineXYs().get(i)[2] + "," + options.getLineXYs().get(i)[3]);
                    if (i % 2 == 0) {//用下標的奇偶來表示開始還是結束, 偶數開始,奇數結束
                        path.moveTo(options.getLineXYs().get(i)[0], options.getLineXYs().get(i)[3]);
                    } else {
                        path.lineTo(options.getLineXYs().get(i)[0], options.getLineXYs().get(i)[3]);
                        canvas.drawPath(path, paint);//每一行畫一條線
                        path.reset();
                    }
                }
            }
        }
    }

    @Override
    public boolean performClick() {//攔截處理,TextView 的點擊和它的局部點擊事件衝突
        ClickableSpan[] links = ((Spannable) getText()).getSpans(getSelectionStart(), getSelectionEnd(),
                ClickableSpan.class);
        if (links.length > 0) {
            return false;
        }

        return super.performClick();
    }

    @Override
    protected void onDetachedFromWindow() {
        underLineOptionsList.clear();
        super.onDetachedFromWindow();
    }
}

UnderLineOptions

public class UnderLineOptions {
    public @interface Style {
        int LINE_STYLE_DOTTED = 1;
        int LINE_STYLE_STROKE = 2;
    }

    private int lineHeight = -1;
    private int lineStyle = Style.LINE_STYLE_DOTTED;
    private int lineColor = Color.WHITE;
    private int lineStart = 0;
    private int lineEnd = 0;
    private String content = "";
    private List<float[]> lineXYs;
    private CustomClickableSpan clickableSpan;
    private boolean clickable = false;


    public UnderLineOptions(int lineStyle, int lineColor, int lineStart, int lineEnd, CustomClickableSpan
            clickableSpan) {
        this.lineStyle = lineStyle;
        this.lineColor = lineColor;
        this.lineStart = lineStart;
        this.lineEnd = lineEnd;
        this.clickableSpan = clickableSpan;
    }

    public UnderLineOptions(int lineStart, int lineEnd) {
        this(Style.LINE_STYLE_DOTTED, Color.RED, lineStart, lineEnd, null);
    }

    public UnderLineOptions(int lineStart, int lineEnd, CustomClickableSpan clickableSpan) {
        this.lineStart = lineStart;
        this.lineEnd = lineEnd;
        this.clickableSpan = clickableSpan;
    }

    public UnderLineOptions(int lineColor, int lineStart, int lineEnd) {
        this(Style.LINE_STYLE_DOTTED, lineColor, lineStart, lineEnd, null);
    }

    public int getLineStart() {
        return lineStart;
    }

    public int getLineEnd() {
        return lineEnd;
    }

    public void setLineXYs(List<float[]> lineXYs) {
        this.lineXYs = lineXYs;
    }

    public List<float[]> getLineXYs() {
        return lineXYs;
    }

    public int getLineStyle() {
        return lineStyle;
    }

    public int getLineColor() {
        return lineColor;
    }

    public CustomClickableSpan getClickableSpan() {
        return clickableSpan;
    }

    public void setClickableSpan(CustomClickableSpan clickableSpan) {
        this.clickableSpan = clickableSpan;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }
}

CustomClickableSpan

public class CustomClickableSpan extends ClickableSpan {
    private int mStart;
    private int mEnd;
    private int x;
    private int y;
    private String content;
    private OnClickListener onClickListener;

    public CustomClickableSpan(){

    }

    public CustomClickableSpan(int start, int end) {
        this(start, end, "");
    }

    public CustomClickableSpan(int mStart, int mEnd, String content) {
        this.mStart = mStart;
        this.mEnd = mEnd;
        this.content = content;
    }

    public void setStart(int mStart) {
        this.mStart = mStart;
    }

    public int getStart() {
        return mStart;
    }

    public void setEnd(int mEnd) {
        this.mEnd = mEnd;
    }

    public int getEnd() {
        return mEnd;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public void setX(int x) {
        this.x = x;
    }

    public void setY(int y) {
        this.y = y;
    }

    public void setOnClickListener(OnClickListener onClickListener) {
        this.onClickListener = onClickListener;
    }

    @Override
    public void onClick(View widget) {
        if (onClickListener != null) {
            onClickListener.onClick(widget, content, x, y);
        }
    }

    @Override
    public void updateDrawState(TextPaint ds) {
        super.updateDrawState(ds);
        // android默認被點擊位置是有下劃線的 源碼如下:
//        ds.setColor(ds.linkColor);
        ds.setUnderlineText(false);//去掉選中下劃線
//        // 在這個方法中我們可以自己指定被點擊位置的樣式,這裏我偷了個懶直接設置了紅色
        ds.setColor(Color.RED);//選中文字顏色
    }

    public interface OnClickListener {
        void onClick(View v, String content, int x, int y);
    }
}

MainActivity

 private PopupWindow popupWindow;
    private TextView tv;
    private String popContent;
    private long showTime = 1500;//ms
    private long delayTime = showTime;
    private Disposable dismissDisposable;

    private void setTvUnderline() {
        SpannableString spanableInfo = new SpannableString("這是一個測試文本,點擊我看看!");
        CustomClickableSpan clickableSpan = new CustomClickableSpan();
        clickableSpan.setOnClickListener(this);
        CustomClickableSpan clickableSpan2 = new CustomClickableSpan();
        clickableSpan2.setOnClickListener(this);
        spanableInfo.setSpan(clickableSpan, 4, 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);//一段文字中可以實現多個文本點擊
        spanableInfo.setSpan(clickableSpan2, 9, 15, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        tvUnderline.setText(spanableInfo);
        tvUnderline.setMovementMethod(LinkMovementMethod.getInstance());

        //添加多處下劃線
        List<UnderLineOptions> lines = new ArrayList<>();
        UnderLineOptions lineOptions2 = new UnderLineOptions(4, 6, clickableSpan);
        UnderLineOptions lineOptions = new UnderLineOptions(9, 15, clickableSpan2);
        lines.add(lineOptions);
        lines.add(lineOptions2);
        tvUnderline.setLines(lines);
    }

    private void initPopUp(String content) {
        this.popContent = content;
        delayTime = showTime;
        LinearLayout layout = new LinearLayout(this);
        layout.setBackgroundColor(Color.GRAY);
        tv = new TextView(this);
        tv.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams
                .WRAP_CONTENT));
        tv.setText(content);
        tv.setTextColor(Color.WHITE);
        layout.addView(tv);

        popupWindow = new PopupWindow(layout, LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams
                .WRAP_CONTENT);

//        popupWindow.setFocusable(true);
//        popupWindow.setOutsideTouchable(false);
//        popupWindow.setBackgroundDrawable(new BitmapDrawable());
    }

    private void showPopUp(View v, String content, int x, int y) {
        if (popupWindow != null && popupWindow.isShowing()) {
            if (dismissDisposable != null && !dismissDisposable.isDisposed()) {
                dismissDisposable.dispose();
            }
            if (!TextUtils.equals(content, popContent)) {
                popupWindow.dismiss();
            } else {
                delayTime += showTime;
                dismissDelay(delayTime);
                return;
            }
        }

        initPopUp(content);

        TextPaint textPaint = tv.getPaint();
        int width = (int) (textPaint.measureText(content));
        Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
        int height = (int) (fontMetrics.bottom - fontMetrics.top);

        popupWindow.showAtLocation(v, Gravity.NO_GRAVITY, x - width / 2, y - height);
        Log.d("lixx", "showpopup delayTime-> " + delayTime);
        dismissDelay(delayTime);
    }

    private void dismissDelay(long delay) {
        dismissDisposable = Observable.timer(delayTime, TimeUnit.MILLISECONDS)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.io())
                .subscribe(aLong -> {
                    Log.d("lixx", "dismiss delayTime-> " + delayTime);
                    if (popupWindow != null && popupWindow.isShowing()) {
                        popupWindow.dismiss();
                    }
                });
    }

    @Override
    public void onClick(View v, String content, int x, int y) {
        showPopUp(v, content, x, y);
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章