效果圖如上圖,大家可以看到今天要實現的功能主要有虛線下劃線
和點擊文本
。
下面我們來分別分析下實現原理和知識點,最後給大家放上關鍵代碼。
虛線下劃線
給文本添加下劃線相信大家都會,這不就是富文本的內容嗎?提到富文本大家可能會想到SpannableStringBuilder
。ClickableSpan
的默認效果就是帶下劃線。但今天我們的目標是虛線下劃線,所以我們可能要自己手動改造下了。這裏我採用的是自定義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);
}