我在Android开发中遇到的坑之微博正文点击处理

我在Android开发中遇到的坑之微博正文点击处理

  • 开发是一个漫长的过程,我们会遇到很多很多的坑,有些却是系统级的坑,有时候遇到真是抓狂,不过这也是我们不断进步的过程,今天就给大家讲一个我遇到的一个很坑的问题。
  • 还好我遇到了一个万能的 Android 大神 stainberg ,他帮助我仔细排查并且解决了问题,有他我真的提高了好多。
QQ20161129-0@2x.png

需求描述

  • 上图是我们常见的微博界面,其中微博正文中出现了不同标记的字段,有At用户,有##话题,有Url标签。
  • 重点就是如何处理类似于微博正文中,不同标记的点击事件。
  • 很显然,使用过微博SDK的同学们都知道,其中微博正文这一段字是在一个 Text 中返回的,所以我们也理应在一个 TextView 中对不同的标记做处理。
  • 处理的方式很简单,就是使用 Android 中的 SpannableString 和 ClickableSpan ,先配合正则表达式匹配出想要的字符,再通过 SpannableString 的 setSpan() 方法来对标记出得字符串做处理,我们可以对该字符串自定义颜色,点击事件等(后面会有源码)。
  • 注意所在的 TextView 要实现 textview.setMovementMethod(LinkMovementMethod.getInstance()) 才可以使自定义的点击事件生效。

一个巨大的坑

  • 当我做完上面这些后,哇...好棒,每一个标记的字段都可以执行自己规定的点击事件了。
  • 但是!我发现了一个很严重的问题,标记的字段是可以点击,但由于设置了 textview.setMovementMethod(LinkMovementMethod.getInstance()) 导致 TextView 对点击事件做了拦截,而原本在 RecyclerView 中 item 自己的点击事件却失效了。
  • 就是说,textView 拦截了全部的点击事件,如果我这一段文字没有任何匹配到的At,##话题标签和Url这类的字符串,它任会拦截。
  • 我原本想要设计的效果是,当点击特殊字符串的时候,执行自定义的点击事件,而没有特殊字符出现的时候,执行 item 原本的点击事件,例如点击正常文字,进入微博详情页。

排查问题

  • 我想问题的原因,应该就是出在了 textview.setMovementMethod(LinkMovementMethod.getInstance()) 上面,所以我查看了 LinkMovementMethod 的源码。
  • 通过打 debug 发现执行拦截操作的核心代码是下面这一段。
  @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) {}这里面,执行你的自定义点击事件,没有特使字符串的时候走 return super.onTouchEvent(widget, buffer, event);
  • 然后我继续对没有特使字符串的地方打断点排查,这时候我发现了一个很坑的问题,无论什么样,return super.onTouchEvent(widget, buffer, event);都返回 true ,这就意味着 TextView 会一直拦截事件,而外层的 item 永远不会执行点击事件,这里我终于找到了问题的所在。
  • 我靠,这是一个系统级的 bug 啊,很早之前我就发现了这个问题,但我一直不知道问什么,今天终于明白了,这么久 Google 竟然还不修复。

解决方案

  • 既然我们知道了问题出现的原因,那么就很好解决了,在没有匹配到特殊字符串的时候,返回 False 就好啦。
  • 一开始我想着重写 LinkMovementMethod ,然后在最后返回 False ,然而并没有什么卵用,依旧被拦截。
  • 最后在万能的 StackOverFlow 上发现了解决的方法,就是重写一个 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 上调用 textView.setOnTouchListener(MyLinkMovementMethod.getInstance());
  • 就这样!有特殊字符串的地方,会执行自定义点击事件,没有特殊字符串的地方执行 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中的连接跳转
    */
   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);

       }
   }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容