前言 相信大家应该都有所体会,对于一个社交性质的App,业务上少不了给一段文本加上@功能、话题功能,或者是评论上要高亮人名的需求。当然,Android为我们提供了Clickab
前言
相信大家应该都有所体会,对于一个社交性质的App,业务上少不了给一段文本加上@功能、话题功能,或者是评论上要高亮人名的需求。当然,Android为我们提供了ClickableSpan,用于解决TextView部分内容可点击的问题,但却附加了一堆的坑点:
ClickableSpan 默认没有高亮行为,也不能添加背景颜色; ClickableSpan 必须配合 MovementMethod 使用 一旦使用 MovementMethod,TextView 必定消耗事件 当点击ClickableSpan时,TextView的点击也会随后触发 当press ClickableSpan 时, TextView的press态也会被触发这些默认的表现会使得添加 ClickableSpan 后会出现各种不符合预期的问题,因此我们需要对其进行封装。
据个人使用经验,封装后应该能够方便开发实现以下行为:
让Span支持字体颜色和背景颜色变化,并且有press态行为 Span的click或者press不影响TextView的click和press 可选择的决定TextView是否应该消耗事件对于第三点,需要解释下TextView是否消耗事件的影响
用一张图来阐述下我们的目的。我们开发过程中,可能将点击事件加在TextView上,也可能将点击行为添加在TextView的父元素上,例如评论一般是点击整个评论item就可以触发回复。 如果我们把点击事件加在TextView的父元素上,那么我们期待的是点击TextView的绿色区域应该也要响应点击事件,但现实总是残酷的,如果TextView调用了setMovementMethod, 点击绿色区域将不会有任何反应,因为时间被TextView消耗了,并不会传递到TextView的父元素上。
那我们来一步一步看如何实现这几个问题。
首先我们定义一个接口 ITouchableSpan, 用于抽象press和点击:
public interface ITouchableSpan {
void setPressed(boolean pressed);
void onClick(View widget);
}
然后建立一个 ClickableSpan的子类 QMUITouchableSpan 来扩充它的表现:
public abstract class QMUITouchableSpan extends ClickableSpan implements ITouchableSpan {
private boolean mIsPressed;
@ColorInt private int mNORMalBackgroundColor;
@ColorInt private int mPressedBackgroundColor;
@ColorInt private int mNormalTextColor;
@ColorInt private int mPressedTextColor;
private boolean mIsNeedUnderline = false;
public abstract void onSpanClick(View widget);
@Override
public final void onClick(View widget) {
if (ViewCompat.isAttachedToWindow(widget)) {
onSpanClick(widget);
}
}
public QMUITouchableSpan(@ColorInt int normalTextColor,
@ColorInt int pressedTextColor,
@ColorInt int normalBackgroundColor,
@ColorInt int pressedBackgroundColor) {
mNormalTextColor = normalTextColor;
mPressedTextColor = pressedTextColor;
mNormalBackgroundColor = normalBackgroundColor;
mPressedBackgroundColor = pressedBackgroundColor;
}
// .... get/set ...
public void setPressed(boolean isSelected) {
mIsPressed = isSelected;
}
public boolean isPressed() {
return mIsPressed;
}
@Override
public void updateDrawState(TextPaint ds) {
// 通过updateDrawState来更新字体颜色和背景色
ds.setColor(mIsPressed ? mPressedTextColor : mNormalTextColor);
ds.bGColor = mIsPressed ? mPressedBackgroundColor
: mNormalBackgroundColor;
ds.setUnderlineText(mIsNeedUnderline);
}
}
然后我们要把press状态和点击行为传递给QMUITouchableSpan,这一层我们可以通过重载 LinkMovementMethod去解决:
public class QMUILinkTouchMovementMethod extends LinkMovementMethod {
@Override
public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
return sHelper.onTouchEvent(widget, buffer, event)
|| Touch.onTouchEvent(widget, buffer, event);
}
public static MovementMethod getInstance() {
if (sInstance == null)
sInstance = new QMUILinkTouchMovementMethod();
return sInstance;
}
private static QMUILinkTouchMovementMethod sInstance;
private static QMUILinkTouchDecorHelper sHelper = new QMUILinkTouchDecorHelper();
}
对TextView使用 setMovementMethod 后,TextView的 onTouchEvent 中会调用到 LinkMovementMethod的onTouchEvent,并且会传入Spannable,这是一个去处理Spannable数据的好hook点。 我们抽取一个 QMUILinkTouchDecorHelper 用于处理公共逻辑,因为LinkMovementMethod存在多个行为各异的子类。
public class QMUILinkTouchDecorHelper {
private ITouchableSpan mPressedSpan;
public boolean onTouchEvent(TextView textView, Spannable spannable, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
mPressedSpan = getPressedSpan(textView, spannable, event);
if (mPressedSpan != null) {
mPressedSpan.setPressed(true);
Selection.setSelection(spannable, spannable.getSpanStart(mPressedSpan),
spannable.getSpanEnd(mPressedSpan));
}
if (textView instanceof QMUISpanTouchFixTextView) {
QMUISpanTouchFixTextView tv = (QMUISpanTouchFixTextView) textView;
tv.setTouchSpanHint(mPressedSpan != null);
}
return mPressedSpan != null;
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
ITouchableSpan touchedSpan = getPressedSpan(textView, spannable, event);
if (mPressedSpan != null && touchedSpan != mPressedSpan) {
mPressedSpan.setPressed(false);
mPressedSpan = null;
Selection.removeSelection(spannable);
}
return mPressedSpan != null;
} else if (event.getAction() == MotionEvent.ACTION_UP) {
boolean touchSpanHint = false;
if (mPressedSpan != null) {
touchSpanHint = true;
mPressedSpan.setPressed(false);
mPressedSpan.onClick(textView);
}
mPressedSpan = null;
Selection.removeSelection(spannable);
return touchSpanHint;
} else {
if (mPressedSpan != null) {
mPressedSpan.setPressed(false);
}
Selection.removeSelection(spannable);
return false;
}
}
public ITouchableSpan getPressedSpan(TextView textView, Spannable spannable, MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= textView.getTotalPaddingLeft();
y -= textView.getTotalPaddingTop();
x += textView.getScrollX();
y += textView.getScrollY();
Layout layout = textView.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
ITouchableSpan[] link = spannable.getSpans(off, off, ITouchableSpan.class);
ITouchableSpan touchedSpan = null;
if (link.length > 0) {
touchedSpan = link[0];
}
return touchedSpan;
}
}
上述的很多行为直接取自官方的LinkTouchMovementMethod,然后做了相应的修改。完成这些,我们才仅仅能做到我们想要的第一步而已。
接下来我们看如何处理TextView的click与press与 QMUITouchableSpan 冲突的问题。 这一步我们需要建立一个TextView的子类QMUISpanTouchFixTextView去处理相关细节。
第一步我们需要判断是否是点击到了QMUITouchableSpan, 这个判断可以放在 QMUILinkTouchDecorHelper#onTouchEvent中完成, 在onTouchEvent中补充以下代码:
public boolean onTouchEvent(TextView textView, Spannable spannable, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
// ...
if (textView instanceof QMUISpanTouchFixTextView) {
QMUISpanTouchFixTextView tv = (QMUISpanTouchFixTextView) textView;
tv.setTouchSpanHint(mPressedSpan != null);
}
return mPressedSpan != null;
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
// ...
if (textView instanceof QMUISpanTouchFixTextView) {
QMUISpanTouchFixTextView tv = (QMUISpanTouchFixTextView) textView;
tv.setTouchSpanHint(mPressedSpan != null);
}
return mPressedSpan != null;
} else if (event.getAction() == MotionEvent.ACTION_UP) {
// ...
Selection.removeSelection(spannable);
if (textView instanceof QMUISpanTouchFixTextView) {
QMUISpanTouchFixTextView tv = (QMUISpanTouchFixTextView) textView;
tv.setTouchSpanHint(touchSpanHint);
}
return touchSpanHint;
} else {
// ...
if (textView instanceof QMUISpanTouchFixTextView) {
QMUISpanTouchFixTextView tv = (QMUISpanTouchFixTextView) textView;
tv.setTouchSpanHint(false);
}
// ...
return false;
}
}
这个时候我们在 QMUISpanTouchFixTextView就可以通过是否点击到QMUITouchableSpan来决定不同行为了,对于点击是非常好处理的,代码如下:
@Override
public boolean performClick() {
if (!mTouchSpanHint) {
return super.performClick();
}
return false;
}
对于press行为,就会有点棘手,因为setPress在 onTouchEvent多次调用,而且在QMUILinkTouchDecorHelper#onTouchEvent前就会被调用到,所以不能简单的用mTouchSpanHint这个变量来管理。来看看我给出的方案:
// 记录每次真正传入的press,每次更改mTouchSpanHint,需要再调用一次setPressed,确保press状态正确
// 第一步: 用一个变量记录setPress传入的值,这个是TextView真正的press值
private boolean mIsPressedRecord = false;
// 第二步,onTouchEvent在调用super前将mTouchSpanHint设为true,这会使得QMUILinkTouchDecorHelper#onTouchEvent的press行为失效,参考第三步
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!(getText() instanceof Spannable)) {
return super.onTouchEvent(event);
}
mTouchSpanHint = true;
return super.onTouchEvent(event);
}
// 第三步: final掉setPressed,如果!mTouchSpanHint才调用super.setPressed,开一个onSetPressed给子类覆写
@Override
public final void setPressed(boolean pressed) {
mIsPressedRecord = pressed;
if (!mTouchSpanHint) {
onSetPressed(pressed);
}
}
protected void onSetPressed(boolean pressed) {
super.setPressed(pressed);
}
// 第四步: 每次调用setTouchSpanHint是调用一次setPressed,并传入mIsPressedRecord,确保press状态的统一
public void setTouchSpanHint(boolean touchSpanHint) {
if (mTouchSpanHint != touchSpanHint) {
mTouchSpanHint = touchSpanHint;
setPressed(mIsPressedRecord);
}
}
这几个步骤相互耦合,静下心好好理解下。这样就顺利的解决了第二个问题。那么我们来看看如何消除 MovementMethod造成TextView对事件的消耗行为。
调用 setMovementMethod为何会使得TextView必然消耗事件呢?我们可以看看源码:
public final void setMovementMethod(MovementMethod movement) {
if (mMovement != movement) {
mMovement = movement;
if (movement != null && !(mText instanceof Spannable)) {
setText(mText);
}
fixFocusableAndClickableSettings();
// SelectionModifierCursorController depends on textCanBeSelected, which depends on
// mMovement
if (mEditor != null) mEditor.prepareCursorControllers();
}
}
private void fixFocusableAndClickableSettings() {
if (mMovement != null || (mEditor != null && mEditor.mKeyListener != null)) {
setFocusable(true);
setClickable(true);
setLongClickable(true);
} else {
setFocusable(false);
setClickable(false);
setLongClickable(false);
}
}
原来设置MovementMethod后会把clickable,longClickable和focusable都设置为true,这样必然TextView会消耗事件了。因此我们想到的解决方案就是:如果我们想不让TextView消耗事件,那么我们就在 setMovementMethod之后再改一次clickable,longClickable和focusable。
public void setShouldConsumeEvent(boolean shouldConsumeEvent) {
mShouldConsumeEvent = shouldConsumeEvent;
setFocusable(shouldConsumeEvent);
setClickable(shouldConsumeEvent);
setLongClickable(shouldConsumeEvent);
}
public void setMovementMethodCompat(MovementMethod movement){
setMovementMethod(movement);
if(!mShouldConsumeEvent){
setShouldConsumeEvent(false);
}
}
仅仅这样还不够,我们还必须在 onTouchEvent里面返回false:
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!(getText() instanceof Spannable)) {
return super.onTouchEvent(event);
}
mTouchSpanHint = true;
// 调用super.onTouchEvent,会走到QMUILinkTouchMovementMethod
// 会走到QMUILinkTouchMovementMethod#onTouchEvent会修改mTouchSpanHint
boolean ret = super.onTouchEvent(event);
if(!mShouldConsumeEvent){
return mTouchSpanHint;
}
return ret;
}
经过层层fix,我们终于可以给出一份不错的封装代码提供给业务方使用了:
public class QMUISpanTouchFixTextView extends TextView {
private boolean mTouchSpanHint;
// 记录每次真正传入的press,每次更改mTouchSpanHint,需要再调用一次setPressed,确保press状态正确
private boolean mIsPressedRecord = false;
private boolean mShouldConsumeEvent = true; // TextView是否应该消耗事件
public QMUISpanTouchFixTextView(Context context) {
this(context, null);
}
public QMUISpanTouchFixTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public QMUISpanTouchFixTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setHighlightColor(Color.TRANSPARENT);
setMovementMethod(QMUILinkTouchMovementMethod.getInstance());
}
public void setShouldConsumeEvent(boolean shouldConsumeEvent) {
mShouldConsumeEvent = shouldConsumeEvent;
setFocusable(shouldConsumeEvent);
setClickable(shouldConsumeEvent);
setLongClickable(shouldConsumeEvent);
}
public void setMovementMethodCompat(MovementMethod movement){
setMovementMethod(movement);
if(!mShouldConsumeEvent){
setShouldConsumeEvent(false);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!(getText() instanceof Spannable)) {
return super.onTouchEvent(event);
}
mTouchSpanHint = true;
// 调用super.onTouchEvent,会走到QMUILinkTouchMovementMethod
// 会走到QMUILinkTouchMovementMethod#onTouchEvent会修改mTouchSpanHint
boolean ret = super.onTouchEvent(event);
if(!mShouldConsumeEvent){
return mTouchSpanHint;
}
return ret;
}
public void setTouchSpanHint(boolean touchSpanHint) {
if (mTouchSpanHint != touchSpanHint) {
mTouchSpanHint = touchSpanHint;
setPressed(mIsPressedRecord);
}
}
@Override
public boolean performClick() {
if (!mTouchSpanHint && mShouldConsumeEvent) {
return super.performClick();
}
return false;
}
@Override
public boolean performLongClick() {
if (!mTouchSpanHint && mShouldConsumeEvent) {
return super.performLongClick();
}
return false;
}
@Override
public final void setPressed(boolean pressed) {
mIsPressedRecord = pressed;
if (!mTouchSpanHint) {
onSetPressed(pressed);
}
}
protected void onSetPressed(boolean pressed) {
super.setPressed(pressed);
}
}
总结
以上就是这篇文章的全部内容了,希望本文的内容对给位Android开发者们能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对编程网的支持。
您可能感兴趣的文章:Android TextView实现词组高亮的示例代码Android中实现词组高亮TextView方法示例Android TextView中部分文字高亮显示Android 自定义TextView实现滑动解锁高亮文字
--结束END--
本文标题: Android中TextView文本高亮和点击行为的封装方法
本文链接: https://lsjlt.com/news/21971.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-01-21
2023-10-28
2023-10-28
2023-10-27
2023-10-27
2023-10-27
2023-10-27
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0