我在Android開發(fā)中遇到的坑之微博正文點擊處理
- 開發(fā)是一個漫長的過程,我們會遇到很多很多的坑,有些卻是系統(tǒng)級的坑,有時候遇到真是抓狂俯在,不過這也是我們不斷進步的過程,今天就給大家講一個我遇到的一個很坑的問題娃惯。
- 還好我遇到了一個萬能的 Android 大神 stainberg 跷乐,他幫助我仔細排查并且解決了問題,有他我真的提高了好多趾浅。
需求描述
- 上圖是我們常見的微博界面愕提,其中微博正文中出現(xiàn)了不同標記的字段,有At用戶皿哨,有##話題浅侨,有Url標簽。
- 重點就是如何處理類似于微博正文中证膨,不同標記的點擊事件如输。
- 很顯然,使用過微博SDK的同學們都知道央勒,其中微博正文這一段字是在一個 Text 中返回的不见,所以我們也理應(yīng)在一個 TextView 中對不同的標記做處理。
- 處理的方式很簡單崔步,就是使用 Android 中的 SpannableString 和 ClickableSpan 稳吮,先配合正則表達式匹配出想要的字符,再通過 SpannableString 的 setSpan() 方法來對標記出得字符串做處理井濒,我們可以對該字符串自定義顏色灶似,點擊事件等(后面會有源碼)列林。
- 注意所在的 TextView 要實現(xiàn) textview.setMovementMethod(LinkMovementMethod.getInstance()) 才可以使自定義的點擊事件生效。
一個巨大的坑
- 當我做完上面這些后酪惭,哇...好棒希痴,每一個標記的字段都可以執(zhí)行自己規(guī)定的點擊事件了。
- 但是撞蚕!我發(fā)現(xiàn)了一個很嚴重的問題润梯,標記的字段是可以點擊过牙,但由于設(shè)置了 textview.setMovementMethod(LinkMovementMethod.getInstance()) 導致 TextView 對點擊事件做了攔截甥厦,而原本在 RecyclerView 中 item 自己的點擊事件卻失效了。
- 就是說寇钉,textView 攔截了全部的點擊事件刀疙,如果我這一段文字沒有任何匹配到的At,##話題標簽和Url這類的字符串扫倡,它任會攔截谦秧。
- 我原本想要設(shè)計的效果是,當點擊特殊字符串的時候撵溃,執(zhí)行自定義的點擊事件疚鲤,而沒有特殊字符出現(xiàn)的時候,執(zhí)行 item 原本的點擊事件缘挑,例如點擊正常文字集歇,進入微博詳情頁。
排查問題
- 我想問題的原因语淘,應(yīng)該就是出在了 textview.setMovementMethod(LinkMovementMethod.getInstance()) 上面诲宇,所以我查看了 LinkMovementMethod 的源碼。
- 通過打 debug 發(fā)現(xiàn)執(zhí)行攔截操作的核心代碼是下面這一段惶翻。
@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[] link = buffer.getSpans(off, off, ClickableSpan.class);
if (link.length != 0) {
if (action == MotionEvent.ACTION_UP) {
link[0].onClick(widget);
} else if (action == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer,
buffer.getSpanStart(link[0]),
buffer.getSpanEnd(link[0]));
}
return true;
} else {
Selection.removeSelection(buffer);
}
}
return super.onTouchEvent(widget, buffer, event);
}
- 其中有特殊字符串時姑蓝,走
if (link.length != 0) {}
這里面,執(zhí)行你的自定義點擊事件吕粗,沒有特使字符串的時候走 return super.onTouchEvent(widget, buffer, event);
- 然后我繼續(xù)對沒有特使字符串的地方打斷點排查纺荧,這時候我發(fā)現(xiàn)了一個很坑的問題,無論什么樣颅筋,
return super.onTouchEvent(widget, buffer, event);
都返回 true 宙暇,這就意味著 TextView 會一直攔截事件,而外層的 item 永遠不會執(zhí)行點擊事件垃沦,這里我終于找到了問題的所在客给。
- 我靠,這是一個系統(tǒng)級的 bug 啊肢簿,很早之前我就發(fā)現(xiàn)了這個問題靶剑,但我一直不知道問什么蜻拨,今天終于明白了,這么久 Google 竟然還不修復桩引。
解決方案
- 既然我們知道了問題出現(xiàn)的原因缎讼,那么就很好解決了,在沒有匹配到特殊字符串的時候坑匠,返回 False 就好啦血崭。
- 一開始我想著重寫 LinkMovementMethod ,然后在最后返回 False 厘灼,然而并沒有什么卵用夹纫,依舊被攔截。
- 最后在萬能的 StackOverFlow 上發(fā)現(xiàn)了解決的方法设凹,就是重寫一個 TextView 的 setontouchlistener 方法舰讹,把上面的代碼寫到里面就好了,沒錯就是這么簡單闪朱,膜拜一下 StackOverFlow 上的大神(代碼如下)月匣。
public class MyLinkMovementMethod implements View.OnTouchListener {
public static MyLinkMovementMethod getInstance() {
if (sInstance == null)
sInstance = new MyLinkMovementMethod();
return sInstance;
}
private static MyLinkMovementMethod sInstance;
@Override
public boolean onTouch(View v, MotionEvent event) {
boolean ret = false;
CharSequence text = ((TextView) v).getText();
Spannable stext = Spannable.Factory.getInstance().newSpannable(text);
TextView widget = (TextView) v;
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[] link = stext.getSpans(off, off, ClickableSpan.class);
if (link.length != 0) {
if (action == MotionEvent.ACTION_UP) {
link[0].onClick(widget);
}
ret = true;
}
}
return ret;
}
}
- 然后在 textView 上調(diào)用
textView.setOnTouchListener(MyLinkMovementMethod.getInstance());
- 就這樣!有特殊字符串的地方奋姿,會執(zhí)行自定義點擊事件锄开,沒有特殊字符串的地方執(zhí)行 item 原有的點擊事件。
一些代碼
/**
* 將微博正文中的 @ 和 # ,url標識出
*
* @param text
* @return
*/
public static SpannableString getWeiBoText(Context context, String text) {
Resources res = context.getResources();
//四種正則表達式
Pattern AT_PATTERN = Pattern.compile("@[\\u4e00-\\u9fa5\\w\\-]+");
Pattern TAG_PATTERN = Pattern.compile("#([^\\#|.]+)#");
Pattern Url_PATTERN = Pattern.compile("((http|https|ftp|ftps):\\/\\/)?([a-zA-Z0-9-]+\\.){1,5}(com|cn|net|org|hk|tw)((\\/(\\w|-)+(\\.([a-zA-Z]+))?)+)?(\\/)?(\\??([\\.%:a-zA-Z0-9_-]+=[#\\.%:a-zA-Z0-9_-]+(&)?)+)?");
Pattern EMOJI_PATTER = Pattern.compile("\\[([\u4e00-\u9fa5\\w])+\\]");
SpannableString spannable = new SpannableString(text);
Matcher tag = TAG_PATTERN.matcher(spannable);
while (tag.find()) {
String tagNameMatch = tag.group();
int start = tag.start();
spannable.setSpan(new MyTagSpan(context, tagNameMatch), start, start + tagNameMatch.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
Matcher at = AT_PATTERN.matcher(spannable);
while (at.find()) {
String atUserName = at.group();
int start = at.start();
spannable.setSpan(new MyAtSpan(context, atUserName), start, start + atUserName.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
Matcher url = Url_PATTERN.matcher(spannable);
while (url.find()) {
String urlString = url.group();
int start = url.start();
spannable.setSpan(new MyURLSpan(context, urlString), start, start + urlString.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
Matcher emoji = EMOJI_PATTER.matcher(spannable);
while (emoji.find()) {
String key = emoji.group(); // 獲取匹配到的具體字符
int start = emoji.start(); // 匹配字符串的開始位置
Integer imgRes = Emotion.getImgByName(key);
System.out.println("@@@"+imgRes);
if (imgRes != null) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, imgRes, options);
int scale = (int) (options.outWidth / 32);
options.inJustDecodeBounds = false;
options.inSampleSize = scale;
Bitmap bitmap = BitmapFactory.decodeResource(res, imgRes, options);
ImageSpan span = new ImageSpan(context, bitmap);
spannable.setSpan(span, start, start + key.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
return spannable;
}
/**
* 用于weibo text中的連接跳轉(zhuǎn)
*/
private static class MyURLSpan extends ClickableSpan {
private String mUrl;
private Context context;
MyURLSpan(Context ctx, String url) {
context = ctx;
mUrl = url;
}
@Override
public void updateDrawState(TextPaint ds) {
ds.setColor(Color.parseColor("#f44336"));
}
@Override
public void onClick(View widget) {
Intent intent = UrlActivity.newIntent(context, mUrl);
context.startActivity(intent);
}
}