Android中TextView文本高亮和点击行为的封装方法

Android中TextView文本高亮和点击行为的封装方法,第1张

概述前言相信大家应该都有所体会,对于一个社交性质的App,业务上少不了给一段文本加上@功能、话题功能,或者是评论上要高亮人名的需求。当然,Android为我们提供了ClickableSpan,用于解决TextView部分内容可点击的问题

前言

相信大家应该都有所体会,对于一个社交性质的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,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,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,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,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来决定不同行为了,对于点击是非常好处理的,代码如下:

@OverrIDepublic 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行为失效,参考第三步@OverrIDepublic boolean ontouchEvent(MotionEvent event) { if (!(getText() instanceof Spannable)) {  return super.ontouchEvent(event); } mtouchSpanHint = true; return super.ontouchEvent(event);}// 第三步: final掉setpressed,如果!mtouchSpanHint才调用super.setpressed,开一个onSetpressed给子类覆写@OverrIDepublic 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:

@OverrIDepublic 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,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文本高亮和点击行为的封装方法所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://www.outofmemory.cn/web/1146560.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-31
下一篇 2022-05-31

发表评论

登录后才能评论

评论列表(0条)

保存