国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 系統 > Android > 正文

Android中TextView文本高亮和點擊行為的封裝方法

2019-12-12 03:16:26
字體:
來源:轉載
供稿:網友

前言

相信大家應該都有所體會,對于一個社交性質的App,業務上少不了給一段文本加上@功能、話題功能,或者是評論上要高亮人名的需求。當然,Android為我們提供了ClickableSpan,用于解決TextView部分內容可點擊的問題,但卻附加了一堆的坑點:

  1. ClickableSpan 默認沒有高亮行為,也不能添加背景顏色;
  2. ClickableSpan 必須配合 MovementMethod 使用
  3. 一旦使用 MovementMethod,TextView 必定消耗事件
  4. 當點擊ClickableSpan時,TextView的點擊也會隨后觸發
  5. 當press ClickableSpan 時, TextView的press態也會被觸發

這些默認的表現會使得添加 ClickableSpan 后會出現各種不符合預期的問題,因此我們需要對其進行封裝。

據個人使用經驗,封裝后應該能夠方便開發實現以下行為:

  1. 讓Span支持字體顏色和背景顏色變化,并且有press態行為
  2. Span的click或者press不影響TextView的click和press
  3. 可選擇的決定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來決定不同行為了,對于點擊是非常好處理的,代碼如下:

@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, 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開發者們能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對武林網的支持。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 石首市| 射阳县| 收藏| 鄱阳县| 肇源县| 武定县| 宣化县| 广元市| 景东| 阳城县| 克拉玛依市| 嵩明县| 隆化县| 汉寿县| 闽侯县| 乌什县| 长宁区| 缙云县| 马山县| 昌都县| 龙泉市| 宁晋县| 蒙城县| 肥城市| 乌拉特中旗| 永定县| 宁德市| 甘洛县| 永仁县| 磴口县| 松潘县| 和静县| 札达县| 元阳县| 长治县| 托里县| 崇文区| 巫山县| 长垣县| 密山市| 罗江县|