热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

AndroidTextView富文本之ClickableSpan

前言ClickableSpan可以让我们在点击TextView相应文字时响应点击事件,比方常用的URLSpan,会在点击时打开相应的链接。而为了让TextView能够响应Click

前言

ClickableSpan可以让我们在点击TextView相应文字时响应点击事件,比方常用的URLSpan,会在点击时打开相应的链接。而为了让TextView能够响应ClickableSpan的点击,我们需要为它设置LinkMovementMethod,但是这个LinkMovementMethod又有着很大的坑,接下来就总结下这些坑和我的处理办法。

LinkMovementMethod的坑

1、点不准

这里将每个字符都设置上ClickableSpan,并在点击时Toast当前被点的字符(文字颜色和背景色应该是ClickableSpanLinkMovementMethod自动帮我们设置的)。设置完LinkMovementMethod后,你会发现自己明明没有点到相应的ClickableSpan,却还是响应了点击事件,或者者明明点到了却不响应,还有的都点到文字外面了,还是会有响应,如下图。

image

2、ellipsize不起作用且TextView会滚

maxLines设置为2ellipsizeend,却发现不起作用,而且整个TextView变成可以滚动的了。

image

简单分析下

我们大致看下LinkMovementMethod的实现。LinkMovementMethod继承自ScrollingMovementMethod,从名字可以看出来它是可以滚动的。他有一个onTouchEvent方法,看来是解决点击事件的,它会在action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN的时候去解决事件,取得点击位置的ClickableSpan,在ACTION_UP的时候响应点击事件。而在action == MotionEvent.ACTION_MOVE的时候交给父类ScrollingMovementMethod解决,这也就使TextView可以滚动,整个TextView可以滚动显示所有的文本,也就不会有ellipsize的省略号了。
Android 这样解决LinkMovementMethod可能是为了在大量文字时更方便地阅读,可以上下滚动,点击的时候点击的位置可以不遮挡要点击文字。但是在有些情况下就不太适用了,比方只是想缩略的显示两行文本,而点击时要点那儿是那儿,这就需要我们来自己解决TextView的点击事件。

处理LinkMovementMethod滚动的问题

我当时在stackoverflow找到了
处理方法,需要设置TextViewOnTouchListener,而后自己解决点击事件,大致贴一下源码。

public static class ClickableSpanTouchListener implements View.OnTouchListener { @Override public boolean onTouch(View v, MotionEvent event) { if (!(v instanceof TextView)) { return false; } TextView widget = (TextView) v; CharSequence text = widget.getText(); if (!(text instanceof Spanned)) { return false; } Spanned buffer = (Spanned) text; 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[] links = buffer.getSpans(off, off, ClickableSpan.class); if (links.length != 0) { ClickableSpan link = links[0]; if (action == MotionEvent.ACTION_UP) { link.onClick(widget); } return true; } } return false; }}

这段代码基本上就是从LinkMovementMethodOnTouchListener拷贝过来的,我们来看下效果。

image
TextView不再滚动,省略号也有了,很好的处理了LinkMovementMethod的问题,但是毕竟基本是拷贝过来的,原来点击Span不准的问题还是存在。

处理点击Span不准的问题

LinkMovementMethod在解决点击事件时没有做边缘判断,得到的点击位置结果可能不准,因而要自己手动解决这些边界的问题,经过反复试验,总算处理了这个问题,先来看下效果。

image
源码如下:

public static class ClickableSpanTouchListener implements View.OnTouchListener { @Override public boolean onTouch(View v, MotionEvent event) { if (!(v instanceof TextView)) { return false; } TextView widget = (TextView) v; CharSequence text = widget.getText(); if (!(text instanceof Spanned)) { return false; } int action = event.getAction(); if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { int index = getTouchedIndex(widget, event); ClickableSpan link = getClickableSpanByIndex(widget, index); if (link != null) { if (action == MotionEvent.ACTION_UP) { link.onClick(widget); } return true; } } return false; } public static ClickableSpan getClickableSpanByIndex(TextView widget, int index) { if (widget == null || index <0) { return null; } CharSequence charSequence = widget.getText(); if (!(charSequence instanceof Spanned)) { return null; } Spanned buffer = (Spanned) charSequence; // end 应该是 index + 1,假如也是 index,得到的结果会往左偏 ClickableSpan[] links = buffer.getSpans(index, index + 1, ClickableSpan.class); if (links != null && links.length > 0) { return links[0]; } return null; } public static int getTouchedIndex(TextView widget, MotionEvent event) { if (widget == null || event == null) { return -1; } 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(); // 根据 y 得到对应的行 line int line = layout.getLineForVertical(y); // 判断得到的 line 能否正确 if (x layout.getLineRight(line) || y layout.getLineBottom(line)) { return -1; } // 根据 line 和 x 得到对应的下标 int index = layout.getOffsetForHorizontal(line, x); // 这里考虑省略号的问题,得到真实显示的字符串的长度,超过就返回 -1 int showedCount = widget.getText().length() - layout.getEllipsisCount(line); if (index > showedCount) { return -1; } // getOffsetForHorizontal 取得的下标会往右偏 // 取得下标处字符左边的左边,假如大于点击的 x,即可能点的是前一个字符 if (layout.getPrimaryHorizontal(index) > x) { index -= 1; } return index; }}

首先在getTouchedIndex中会首先得到点击的行line,这里不能完全相信layout.getLineForVertical返回的数据,要自己判断下点击的位置能否真的在该行。而后通过layout.getOffsetForHorizontal拿到对应的下标,这里要考虑两个问题,第一个是ellipsize省略号的问题,通过layout.getEllipsisCount拿到省略的字符数,在判断当前下标的字符是不是已经被省略了;第二个就是getOffsetForHorizontal得到的下标会往右偏(就是点“和”的右半边的时候会得到“谐”的下标),这个大家可以自己打log或者者debug试一下,判断下字符左边的横坐标大于 x,就说明点的是前一个字符,要index -= 1
而后就是根据index拿到对用的ClickableSpan,通过Spanned.getSpans就能拿得到,但是LinkMovementMethod中调用getSpans时的startend都是下标,这样会使得得到的ClickableSpan往左偏(注意,getOffsetForHorizontal是得到的下标往右偏),这也就是使用LinkMovementMethod点不准的起因,这里要使end = index + 1
最后假如点击到的字符是ClickableSpan,那就在ACTION_DOWN时直接返回true表示要解决该组触摸事件,在ACTION_UP时响应ClickableSpan的点击事件。

结束

至此,我遇到的ClickableSpan的坑和处理方法也都讲清楚了,很多涉及源码的地方也都没有深入研究,比方getOffsetForHorizontal得到的下标为什么会往右偏之类的问题,之后还需要多多研究源码,这样才能提高自己。照例附上源码 funnywolfdadada/RichTextDemo。
下一篇会总结下Html.formHtml超链接的解决,怎样自己解决a标签,拿到标签属性,同时响应点击事件,在本地打开对应页面。


推荐阅读
  • 本文介绍了如何在iOS平台上使用GLSL着色器将YV12格式的视频帧数据转换为RGB格式,并展示了转换后的图像效果。通过详细的技术实现步骤和代码示例,读者可以轻松掌握这一过程,适用于需要进行视频处理的应用开发。 ... [详细]
  • 深入剖析Java中SimpleDateFormat在多线程环境下的潜在风险与解决方案
    深入剖析Java中SimpleDateFormat在多线程环境下的潜在风险与解决方案 ... [详细]
  • 在使用 Qt 进行 YUV420 图像渲染时,由于 Qt 本身不支持直接绘制 YUV 数据,因此需要借助 QOpenGLWidget 和 OpenGL 技术来实现。通过继承 QOpenGLWidget 类并重写其绘图方法,可以利用 GPU 的高效渲染能力,实现高质量的 YUV420 图像显示。此外,这种方法还能显著提高图像处理的性能和流畅性。 ... [详细]
  • 在Android应用开发中,实现与MySQL数据库的连接是一项重要的技术任务。本文详细介绍了Android连接MySQL数据库的操作流程和技术要点。首先,Android平台提供了SQLiteOpenHelper类作为数据库辅助工具,用于创建或打开数据库。开发者可以通过继承并扩展该类,实现对数据库的初始化和版本管理。此外,文章还探讨了使用第三方库如Retrofit或Volley进行网络请求,以及如何通过JSON格式交换数据,确保与MySQL服务器的高效通信。 ... [详细]
  • SQLite数据库CRUD操作实例分析与应用
    本文通过分析和实例演示了SQLite数据库中的CRUD(创建、读取、更新和删除)操作,详细介绍了如何在Java环境中使用Person实体类进行数据库操作。文章首先阐述了SQLite数据库的基本概念及其在移动应用开发中的重要性,然后通过具体的代码示例,逐步展示了如何实现对Person实体类的增删改查功能。此外,还讨论了常见错误及其解决方法,为开发者提供了实用的参考和指导。 ... [详细]
  • 开发笔记:深入解析Android自定义控件——Button的72种变形技巧
    开发笔记:深入解析Android自定义控件——Button的72种变形技巧 ... [详细]
  • 【问题】在Android开发中,当为EditText添加TextWatcher并实现onTextChanged方法时,会遇到一个问题:即使只对EditText进行一次修改(例如使用删除键删除一个字符),该方法也会被频繁触发。这不仅影响性能,还可能导致逻辑错误。本文将探讨这一问题的原因,并提供有效的解决方案,包括使用Handler或计时器来限制方法的调用频率,以及通过自定义TextWatcher来优化事件处理,从而提高应用的稳定性和用户体验。 ... [详细]
  • 使用 ListView 浏览安卓系统中的回收站文件 ... [详细]
  • 在Android平台中,播放音频的采样率通常固定为44.1kHz,而录音的采样率则固定为8kHz。为了确保音频设备的正常工作,底层驱动必须预先设定这些固定的采样率。当上层应用提供的采样率与这些预设值不匹配时,需要通过重采样(resample)技术来调整采样率,以保证音频数据的正确处理和传输。本文将详细探讨FFMpeg在音频处理中的基础理论及重采样技术的应用。 ... [详细]
  • 使用Maven JAR插件将单个或多个文件及其依赖项合并为一个可引用的JAR包
    本文介绍了如何利用Maven中的maven-assembly-plugin插件将单个或多个Java文件及其依赖项打包成一个可引用的JAR文件。首先,需要创建一个新的Maven项目,并将待打包的Java文件复制到该项目中。通过配置maven-assembly-plugin,可以实现将所有文件及其依赖项合并为一个独立的JAR包,方便在其他项目中引用和使用。此外,该方法还支持自定义装配描述符,以满足不同场景下的需求。 ... [详细]
  • 分享一款基于Java开发的经典贪吃蛇游戏实现
    本文介绍了一款使用Java语言开发的经典贪吃蛇游戏的实现。游戏主要由两个核心类组成:`GameFrame` 和 `GamePanel`。`GameFrame` 类负责设置游戏窗口的标题、关闭按钮以及是否允许调整窗口大小,并初始化数据模型以支持绘制操作。`GamePanel` 类则负责管理游戏中的蛇和苹果的逻辑与渲染,确保游戏的流畅运行和良好的用户体验。 ... [详细]
  • 在处理 XML 数据时,如果需要解析 `` 标签的内容,可以采用 Pull 解析方法。Pull 解析是一种高效的 XML 解析方式,适用于流式数据处理。具体实现中,可以通过 Java 的 `XmlPullParser` 或其他类似的库来逐步读取和解析 XML 文档中的 `` 元素。这样不仅能够提高解析效率,还能减少内存占用。本文将详细介绍如何使用 Pull 解析方法来提取 `` 标签的内容,并提供一个示例代码,帮助开发者快速解决问题。 ... [详细]
  • QT框架中事件循环机制及事件分发类详解
    在QT框架中,QCoreApplication类作为事件循环的核心组件,为应用程序提供了基础的事件处理机制。该类继承自QObject,负责管理和调度各种事件,确保程序能够响应用户操作和其他系统事件。通过事件循环,QCoreApplication实现了高效的事件分发和处理,使得应用程序能够保持流畅的运行状态。此外,QCoreApplication还提供了多种方法和信号槽机制,方便开发者进行事件的定制和扩展。 ... [详细]
  • 本文深入探讨了Java多线程环境下的同步机制及其应用,重点介绍了`synchronized`关键字的使用方法和原理。`synchronized`关键字主要用于确保多个线程在访问共享资源时的互斥性和原子性。通过具体示例,如在一个类中使用`synchronized`修饰方法,展示了如何实现线程安全的代码块。此外,文章还讨论了`ReentrantLock`等其他同步工具的优缺点,并提供了实际应用场景中的最佳实践。 ... [详细]
  • 在MPAndroidChart中,当滑动至最后一个数据点时自动加载更多数据
    在MPAndroidChart中,当用户滑动图表至最后一个数据点时,系统将自动触发加载更多数据的功能,以提供连续的数据展示体验。这一机制特别适用于需要动态更新数据的场景,如实时监控和数据分析应用。 ... [详细]
author-avatar
朱玉龙1977
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有