Android——TextView实现虚线下划线并支持点击

Android——TextView实现虚线下划线并支持点击_第1张图片
效果图如上图,大家可以看到今天要实现的功能主要有虚线下划线点击文本
下面我们来分别分析下实现原理和知识点,最后给大家放上关键代码。

虚线下划线

给文本添加下划线相信大家都会,这不就是富文本的内容吗?提到富文本大家可能会想到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 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 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 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 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 lineXYs) {
        this.lineXYs = lineXYs;
    }

    public List 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 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);
    }

你可能感兴趣的:(Android,自定义View)