TextView 設置ClickableSpan之後,需要設置setMovementMethod(LinkMovementMethod.getInstance()),而LinkMovementMethod 繼續ScrollingMovementMethod ,這樣對於長文本有如下兩個問題:
原文地址
GitHub測試Demo
1:ClickableSpan的點擊和TextView的長文本滑動衝突
2:點擊TextView空白區域,總選中最後一個文本
基於此實現了一個簡單的英文閱讀器,點擊鏈接查看
主要實現
1:實現頁面中單詞點擊選中
2:實現分頁功能
3:實現簡單的翻頁動畫
1分析原有的LinkMovementMethod 點擊和滑動邏輯
1)根據點擊位置查找當前行的ClickableSpan,若點擊空白區域,則爲當前行最後一個ClickableSpan;若ClickableSpan集合不爲空,則攔截down和up事件。
@Override
public boolean onTouchEvent(TextView widget, Spannable buffer,
MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
//根據點擊位置查找當前行的ClickableSpan,若點擊空白區域,則爲當前行最後一個ClickableSpan
ClickableSpan[] links = buffer.getSpans(off, off, ClickableSpan.class);
//ClickableSpan集合不爲空,則攔截down和up事件
if (links.length != 0) {
if (action == MotionEvent.ACTION_UP) {
links[0].onClick(widget);
} else if (action == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer,
buffer.getSpanStart(links[0]),
buffer.getSpanEnd(links[0]));
}
return true;
} else {
Selection.removeSelection(buffer);
}
}
return super.onTouchEvent(widget, buffer, event);
}
2)調用父類onTouchEvent透傳事件
@Override
public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
return Touch.onTouchEvent(widget, buffer, event);
}
3)處理拖拽事件。
ACTION_DOWN設置拖拽狀態,ACTION_UP清除拖拽狀態,ACTION_MOVE如果有拖拽狀態,滿足條件則移動內容。
/**
* Handles touch events for dragging. You may want to do other actions
* like moving the cursor on touch as well.
*/
public static boolean onTouchEvent(TextView widget, Spannable buffer,
MotionEvent event) {
DragState[] ds;
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
ds = buffer.getSpans(0, buffer.length(), DragState.class);
for (int i = 0; i < ds.length; i++) {
buffer.removeSpan(ds[i]);
}
//設置拖拽狀態
buffer.setSpan(new DragState(event.getX(), event.getY(),
widget.getScrollX(), widget.getScrollY()),
0, 0, Spannable.SPAN_MARK_MARK);
return true;
case MotionEvent.ACTION_UP:
ds = buffer.getSpans(0, buffer.length(), DragState.class);
//清除拖拽狀態
for (int i = 0; i < ds.length; i++) {
buffer.removeSpan(ds[i]);
}
if (ds.length > 0 && ds[0].mUsed) {
return true;
} else {
return false;
}
case MotionEvent.ACTION_MOVE:
ds = buffer.getSpans(0, buffer.length(), DragState.class);
//如果有拖拽狀態,滿足條件則移動內容
if (ds.length > 0) {
if (ds[0].mFarEnough == false) {
int slop = ViewConfiguration.get(widget.getContext()).getScaledTouchSlop();
if (Math.abs(event.getX() - ds[0].mX) >= slop ||
Math.abs(event.getY() - ds[0].mY) >= slop) {
ds[0].mFarEnough = true;
}
}
if (ds[0].mFarEnough) {
ds[0].mUsed = true;
boolean cap = (event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0
|| MetaKeyKeyListener.getMetaState(buffer,
MetaKeyKeyListener.META_SHIFT_ON) == 1
|| MetaKeyKeyListener.getMetaState(buffer,
MetaKeyKeyListener.META_SELECTING) != 0;
float dx;
float dy;
if (cap) {
// if we're selecting, we want the scroll to go in
// the direction of the drag
dx = event.getX() - ds[0].mX;
dy = event.getY() - ds[0].mY;
} else {
dx = ds[0].mX - event.getX();
dy = ds[0].mY - event.getY();
}
ds[0].mX = event.getX();
ds[0].mY = event.getY();
int nx = widget.getScrollX() + (int) dx;
int ny = widget.getScrollY() + (int) dy;
int padding = widget.getTotalPaddingTop() + widget.getTotalPaddingBottom();
Layout layout = widget.getLayout();
ny = Math.min(ny, layout.getHeight() - (widget.getHeight() - padding));
ny = Math.max(ny, 0);
int oldX = widget.getScrollX();
int oldY = widget.getScrollY();
scrollTo(widget, layout, nx, ny);
// If we actually scrolled, then cancel the up action.
if (oldX != widget.getScrollX() || oldY != widget.getScrollY()) {
widget.cancelLongPress();
}
return true;
}
}
}
return false;
}
2根據業務需要滑動和點擊事件,做如下處理
1)如果點擊當前行的區域爲空白區域,返回空集合。
2)ACTION_DOWN設置拖拽狀態,ACTION_UP清除拖拽狀態,設置之後再攔截
@Override
public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
if (mTouchSlop == 0) {
mTouchSlop = ViewConfiguration.get(widget.getContext()).getScaledTouchSlop();
}
ClickableSpan[] links;
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mIsMoved = false;
//設置拖拽狀態
super.onTouchEvent(widget, buffer, event);
mDownY = event.getY();
mDownX = event.getX();
links = findClickableSpans(widget, buffer, event);
if (links.length != 0) {
Selection.setSelection(buffer,
buffer.getSpanStart(links[0]),
buffer.getSpanEnd(links[0]));
return true;
} else {
Selection.removeSelection(buffer);
}
break;
case MotionEvent.ACTION_MOVE:
if (Math.abs(event.getX() - mDownX) > mTouchSlop ||
Math.abs(event.getY() - mDownY) > mTouchSlop) {
mIsMoved = true;
}
break;
case MotionEvent.ACTION_UP:
//清除拖拽狀態
super.onTouchEvent(widget, buffer, event);
links = findClickableSpans(widget, buffer, event);
//如果滑動,則不觸發點擊事件
if (!mIsMoved && links.length != 0) {
links[0].onClick(widget);
return true;
} else {
if (!mIsMoved && mClickWordListener != null) {
mClickWordListener.onClickEmpty();
}
Selection.removeSelection(buffer);
}
break;
default:
}
return super.onTouchEvent(widget, buffer, event);
}
/**
* 如果點擊區域爲空白區域,返回空集合
*/
private ClickableSpan[] findClickableSpans(TextView widget, Spannable buffer, MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
int lineMax = (int) layout.getLineMax(line);
//點擊空白區域返回空集合
if (x > lineMax) {
return new ClickableSpan[]{};
}
return buffer.getSpans(off, off, ClickableSpan.class);
}