欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Android中TextView文本高亮和點(diǎn)擊行為的封裝方法

 更新時(shí)間:2017年03月27日 11:34:54   作者:小松的技術(shù)博客  
這篇文章主要介紹了Android中TextView文本高亮和點(diǎn)擊行為的封裝方法,文中介紹的非常詳細(xì),相信對大家具有一定的參考價(jià)值,需要的朋友們下面來一起看看吧。

前言

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

  1. ClickableSpan 默認(rèn)沒有高亮行為,也不能添加背景顏色;
  2. ClickableSpan 必須配合 MovementMethod 使用
  3. 一旦使用 MovementMethod,TextView 必定消耗事件
  4. 當(dāng)點(diǎn)擊ClickableSpan時(shí),TextView的點(diǎn)擊也會隨后觸發(fā)
  5. 當(dāng)press ClickableSpan 時(shí), TextView的press態(tài)也會被觸發(fā)

這些默認(rèn)的表現(xiàn)會使得添加 ClickableSpan 后會出現(xiàn)各種不符合預(yù)期的問題,因此我們需要對其進(jìn)行封裝。

據(jù)個人使用經(jīng)驗(yàn),封裝后應(yīng)該能夠方便開發(fā)實(shí)現(xiàn)以下行為:

  1. 讓Span支持字體顏色和背景顏色變化,并且有press態(tài)行為
  2. Span的click或者press不影響TextView的click和press
  3. 可選擇的決定TextView是否應(yīng)該消耗事件

對于第三點(diǎn),需要解釋下TextView是否消耗事件的影響

用一張圖來闡述下我們的目的。我們開發(fā)過程中,可能將點(diǎn)擊事件加在TextView上,也可能將點(diǎn)擊行為添加在TextView的父元素上,例如評論一般是點(diǎn)擊整個評論item就可以觸發(fā)回復(fù)。 如果我們把點(diǎn)擊事件加在TextView的父元素上,那么我們期待的是點(diǎn)擊TextView的綠色區(qū)域應(yīng)該也要響應(yīng)點(diǎn)擊事件,但現(xiàn)實(shí)總是殘酷的,如果TextView調(diào)用了setMovementMethod, 點(diǎn)擊綠色區(qū)域?qū)⒉粫腥魏畏磻?yīng),因?yàn)闀r(shí)間被TextView消耗了,并不會傳遞到TextView的父元素上。

那我們來一步一步看如何實(shí)現(xiàn)這幾個問題。

首先我們定義一個接口 ITouchableSpan, 用于抽象press和點(diǎn)擊:

public interface ITouchableSpan {
 void setPressed(boolean pressed);
 void onClick(View widget);
}

然后建立一個 ClickableSpan的子類 QMUITouchableSpan 來擴(kuò)充它的表現(xiàn):

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狀態(tài)和點(diǎn)擊行為傳遞給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 中會調(diào)用到 LinkMovementMethod的onTouchEvent,并且會傳入Spannable,這是一個去處理Spannable數(shù)據(jù)的好hook點(diǎn)。 我們抽取一個 QMUILinkTouchDecorHelper 用于處理公共邏輯,因?yàn)長inkMovementMethod存在多個行為各異的子類。

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,然后做了相應(yīng)的修改。完成這些,我們才僅僅能做到我們想要的第一步而已。

接下來我們看如何處理TextView的click與press與 QMUITouchableSpan 沖突的問題。 這一步我們需要建立一個TextView的子類QMUISpanTouchFixTextView去處理相關(guān)細(xì)節(jié)。

第一步我們需要判斷是否是點(diǎn)擊到了QMUITouchableSpan, 這個判斷可以放在 QMUILinkTouchDecorHelper#onTouchEvent中完成, 在onTouchEvent中補(bǔ)充以下代碼:

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;
 }
}

這個時(shí)候我們在 QMUISpanTouchFixTextView就可以通過是否點(diǎn)擊到QMUITouchableSpan來決定不同行為了,對于點(diǎn)擊是非常好處理的,代碼如下:

@Override
public boolean performClick() {
 if (!mTouchSpanHint) {
  return super.performClick();
 }
 return false;
}

對于press行為,就會有點(diǎn)棘手,因?yàn)閟etPress在 onTouchEvent多次調(diào)用,而且在QMUILinkTouchDecorHelper#onTouchEvent前就會被調(diào)用到,所以不能簡單的用mTouchSpanHint這個變量來管理。來看看我給出的方案:

// 記錄每次真正傳入的press,每次更改mTouchSpanHint,需要再調(diào)用一次setPressed,確保press狀態(tài)正確
// 第一步: 用一個變量記錄setPress傳入的值,這個是TextView真正的press值
private boolean mIsPressedRecord = false;

// 第二步,onTouchEvent在調(diào)用super前將mTouchSpanHint設(shè)為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才調(diào)用super.setPressed,開一個onSetPressed給子類覆寫
@Override
public final void setPressed(boolean pressed) {
 mIsPressedRecord = pressed;
 if (!mTouchSpanHint) {
  onSetPressed(pressed);
 }
}

protected void onSetPressed(boolean pressed) {
 super.setPressed(pressed);
}

// 第四步: 每次調(diào)用setTouchSpanHint是調(diào)用一次setPressed,并傳入mIsPressedRecord,確保press狀態(tài)的統(tǒng)一
public void setTouchSpanHint(boolean touchSpanHint) {
 if (mTouchSpanHint != touchSpanHint) {
  mTouchSpanHint = touchSpanHint;
  setPressed(mIsPressedRecord);
 }
}

這幾個步驟相互耦合,靜下心好好理解下。這樣就順利的解決了第二個問題。那么我們來看看如何消除 MovementMethod造成TextView對事件的消耗行為。

調(diào)用 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);
 }
}

原來設(shè)置MovementMethod后會把clickable,longClickable和focusable都設(shè)置為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;
 // 調(diào)用super.onTouchEvent,會走到QMUILinkTouchMovementMethod
 // 會走到QMUILinkTouchMovementMethod#onTouchEvent會修改mTouchSpanHint
 boolean ret = super.onTouchEvent(event);
 if(!mShouldConsumeEvent){
  return mTouchSpanHint;
 }
 return ret;
}

經(jīng)過層層fix,我們終于可以給出一份不錯的封裝代碼提供給業(yè)務(wù)方使用了:

public class QMUISpanTouchFixTextView extends TextView {
 private boolean mTouchSpanHint;

 // 記錄每次真正傳入的press,每次更改mTouchSpanHint,需要再調(diào)用一次setPressed,確保press狀態(tài)正確
 private boolean mIsPressedRecord = false;
 private boolean mShouldConsumeEvent = true; // TextView是否應(yīng)該消耗事件

 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;
  // 調(diào)用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);
 }
}

總結(jié)

以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對給位Android開發(fā)者們能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。

相關(guān)文章

最新評論