• Android Span进阶之路——ClickableSpan


    一、前言

        在Android中,可以使用强大的标记(Span)对象来实现富文本展示,相比 HTML 而言更高效实用。关于 Android Span 的入门篇可以阅读 Android中强大的标记对象-Span。本文将对 ClickableSpan (可点击的Span)展开深入的学习。

    二、基本使用

        查看Android Doc 文档可以知道,ClickableSpan 是一个抽象类,它有两个子类,分别是 URLSpanTextLinks.TextLinkSpan(从 API Level 28 开始支持),对于这两个类的使用,这里不做详细讲解,我们主要讲解下如何通过继承 ClickableSpan 实现可点击的标记。

    2.1 ClicableSpan 源码剖析

        首先,我们先来看看 ClickableSpan 抽象类的源码:

    public abstract class ClickableSpan extends CharacterStyle implements UpdateAppearance {
        private static int sIdCounter = 0;
    
        private int mId = sIdCounter++;
    
        /**
         * Performs the click action associated with this span.
         */
        public abstract void onClick(@NonNull View widget);
    
        /**
         * Makes the text underlined and in the link color.
         */
        @Override
        public void updateDrawState(@NonNull TextPaint ds) {
            ds.setColor(ds.linkColor);
            ds.setUnderlineText(true);
        }
    
        /**
         * Get the unique ID for this span.
         *
         * @return The unique ID.
         * @hide
         */
        public int getId() {
            return mId;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

        从上面的源码来看,ClickableSpan 抽象类非常简单,继承该类需要重写的方法也是比较少,其中抽象方法 onClick() 是必须实现,下面讲解重写方法所能实现的效果:

    • public abstract void onClick(@NonNull View widget):抽象方法,必须实现。用以相应可点击标记被点击时的事件相应处理。
    • public void updateDrawState(@NonNull TextPaint ds):配置绘制参数,可以用来更改绘制样式,比如文字颜色、背景颜色、链接颜色、是否包含下划线等等。如果不重载此方法,将会使用默认的绘制样式。

    2.2 自定义 ClickableSpan

        从前面的源码我们了解到 ClickableSpan成员方法,实现自己的自定义 ClickableSpan 就非常容易了:

    /**
     * 自定义 ClickableSpan
     * @param textColor 可点击标记文字颜色
     * @param clickListener 点击时间监听
     */
    class CSClickableSpan (@param:ColorInt private val textColor: Int,
                           private val clickListener: View.OnClickListener?) : ClickableSpan() {
        override fun onClick(widget: View) {
            clickListener?.onClick(widget)
        }
    
        override fun updateDrawState(ds: TextPaint) {
            super.updateDrawState(ds)
    
            ds.color = textColor // 字体颜色(前景色)
            ds.bgColor = Color.TRANSPARENT  // 背景颜色
            ds.linkColor = textColor // 链接颜色
            ds.isUnderlineText = false // 是否显示下划线
            // 这里还可以配置其他绘制样式,比如下划线的粗细(如果启用下划线)、字体等等
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    2.3 使用自定义的 ClickableSpan

        接下来就可以在 SpannableString 或者 SpannableStringBuilder 中使用自定义的 CSClickableSpan 类。

    val tvNormal = findViewById<TextView>(R.id.tv_normal_clickable_span)
    // 必须设置 TextView 的 movementMethod 为 LinkMovementMethod,否则标记无法响应点击事件
    tvNormal.movementMethod = LinkMovementMethod.getInstance()
    tvNormal.setText(SpannableString("我是普通的ClickableSpan").apply {
        setSpan(CSClickableSpan(Color.BLUE, View.OnClickListener {
            Toast.makeText(this@ClickableSpanActivity, tvNormal.text, Toast.LENGTH_SHORT).show()
        }), 5, 18, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    注意事项:在 TextView 中使用 ClickableSpan 时,必须要设置 TextView 对象的 movementMethod 属性为 LinkMovementMethod,否则 ClickableSpan 标记不会响应点击事件。

        运行之后,可以看看效果。

    • 点击前
      自定义ClickableSpan-点击前
    • 点击后
      自定义ClickableS-点击后
          根据上面的例子,我们会发现标记点击后,会有一个背景色,其实这个背景色是 TextView 的高亮颜色,因为 LinkMovementMethod 在标记点击后,会选中标记部分文本。解决这个问题也很简单,只要将 TextViewhighlightColor 设置为透明即可,如下示例:
    val tvNormalNoSelection = findViewById<TextView>(R.id.tv_normal_clickable_span_no_selection)
    // 将 TextView 的高两色设置为透明,可去除点击后的选择高亮色
    tvNormalNoSelection.highlightColor = Color.TRANSPARENT
    // 必须设置 TextView 的 movementMethod 为 LinkMovementMethod,否则标记无法响应点击事件
    tvNormalNoSelection.movementMethod = LinkMovementMethod.getInstance()
    tvNormalNoSelection.setText(SpannableString("我是普通的ClickableSpan(无选中背景)").apply {
        setSpan(CSClickableSpan(Color.BLUE, View.OnClickListener {
            Toast.makeText(this@ClickableSpanActivity, tvNormalNoSelection.text, Toast.LENGTH_SHORT).show()
        }), 5, 18, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

        运行之后看效果,点击标记之后选中高亮色为透明,看起来就是没有高两色的效果,如下图:
    ClickableSpan去除点击后的高亮选中颜色

    三、高手进阶

    3.1 在 ClickableSpan 中实现点击效果

        在前面篇幅中,虽然可以去掉标记选中高亮色,但是这样也并不完美,没有点击效果,用户体验还是有所欠缺。我们首先会想到用TextView 的高亮色,然而高亮色只能设置整型的颜色值,并不能设置ColorList。于是就猜想通过 TextView 的高亮色结合自定义的 CSClickableSpan 实现,笔者刚开始也是从这个角度着手,预想将高亮色设置成按下状态颜色,然后再将高亮色设置为透明色,后来发现这样无法实现,因为 ClickableSpan 这个过程中,会在 onClick() 方法调用之前,前后均会调用 updateDrawState() 更新绘制文本,在如此的调用逻辑下,这种方案是不可行的。既然无法从 TextView 下手,在示例代码中,我们唯一能寄予希望的就是 TextViewmovementMethod 属性了(也就是 LinkMovementMethod)。

    • LinkMovementMethod类源码剖析
    package android.text.method;
    
    import android.annotation.UnsupportedAppUsage;
    import android.os.Build;
    import android.text.Layout;
    import android.text.NoCopySpan;
    import android.text.Selection;
    import android.text.Spannable;
    import android.text.style.ClickableSpan;
    import android.view.KeyEvent;
    import android.view.MotionEvent;
    import android.view.View;
    import android.view.textclassifier.TextLinks.TextLinkSpan;
    import android.widget.TextView;
    
    /**
     * A movement method that traverses links in the text buffer and scrolls if necessary.
     * Supports clicking on links with DPad Center or Enter.
     */
    public class LinkMovementMethod extends ScrollingMovementMethod {
        private static final int CLICK = 1;
        private static final int UP = 2;
        private static final int DOWN = 3;
    
        private static final int HIDE_FLOATING_TOOLBAR_DELAY_MS = 200;
    
        @Override
        public boolean canSelectArbitrarily() {
            return true;
        }
    
        @Override
        protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
                int movementMetaState, KeyEvent event) {
            switch (keyCode) {
                case KeyEvent.KEYCODE_DPAD_CENTER:
                case KeyEvent.KEYCODE_ENTER:
                    if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
                        if (event.getAction() == KeyEvent.ACTION_DOWN &&
                                event.getRepeatCount() == 0 && action(CLICK, widget, buffer)) {
                            return true;
                        }
                    }
                    break;
            }
            return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
        }
    
        @Override
        protected boolean up(TextView widget, Spannable buffer) {
            if (action(UP, widget, buffer)) {
                return true;
            }
    
            return super.up(widget, buffer);
        }
    
        @Override
        protected boolean down(TextView widget, Spannable buffer) {
            if (action(DOWN, widget, buffer)) {
                return true;
            }
    
            return super.down(widget, buffer);
        }
    
        @Override
        protected boolean left(TextView widget, Spannable buffer) {
            if (action(UP, widget, buffer)) {
                return true;
            }
    
            return super.left(widget, buffer);
        }
    
        @Override
        protected boolean right(TextView widget, Spannable buffer) {
            if (action(DOWN, widget, buffer)) {
                return true;
            }
    
            return super.right(widget, buffer);
        }
    
        private boolean action(int what, TextView widget, Spannable buffer) {
            Layout layout = widget.getLayout();
    
            int padding = widget.getTotalPaddingTop() +
                          widget.getTotalPaddingBottom();
            int areaTop = widget.getScrollY();
            int areaBot = areaTop + widget.getHeight() - padding;
    
            int lineTop = layout.getLineForVertical(areaTop);
            int lineBot = layout.getLineForVertical(areaBot);
    
            int first = layout.getLineStart(lineTop);
            int last = layout.getLineEnd(lineBot);
    
            ClickableSpan[] candidates = buffer.getSpans(first, last, ClickableSpan.class);
    
            int a = Selection.getSelectionStart(buffer);
            int b = Selection.getSelectionEnd(buffer);
    
            int selStart = Math.min(a, b);
            int selEnd = Math.max(a, b);
    
            if (selStart < 0) {
                if (buffer.getSpanStart(FROM_BELOW) >= 0) {
                    selStart = selEnd = buffer.length();
                }
            }
    
            if (selStart > last)
                selStart = selEnd = Integer.MAX_VALUE;
            if (selEnd < first)
                selStart = selEnd = -1;
    
            switch (what) {
                case CLICK:
                    if (selStart == selEnd) {
                        return false;
                    }
    
                    ClickableSpan[] links = buffer.getSpans(selStart, selEnd, ClickableSpan.class);
    
                    if (links.length != 1) {
                        return false;
                    }
    
                    ClickableSpan link = links[0];
                    if (link instanceof TextLinkSpan) {
                        ((TextLinkSpan) link).onClick(widget, TextLinkSpan.INVOCATION_METHOD_KEYBOARD);
                    } else {
                        link.onClick(widget);
                    }
                    break;
    
                case UP:
                    int bestStart, bestEnd;
    
                    bestStart = -1;
                    bestEnd = -1;
    
                    for (int i = 0; i < candidates.length; i++) {
                        int end = buffer.getSpanEnd(candidates[i]);
    
                        if (end < selEnd || selStart == selEnd) {
                            if (end > bestEnd) {
                                bestStart = buffer.getSpanStart(candidates[i]);
                                bestEnd = end;
                            }
                        }
                    }
    
                    if (bestStart >= 0) {
                        Selection.setSelection(buffer, bestEnd, bestStart);
                        return true;
                    }
    
                    break;
    
                case DOWN:
                    bestStart = Integer.MAX_VALUE;
                    bestEnd = Integer.MAX_VALUE;
    
                    for (int i = 0; i < candidates.length; i++) {
                        int start = buffer.getSpanStart(candidates[i]);
    
                        if (start > selStart || selStart == selEnd) {
                            if (start < bestStart) {
                                bestStart = start;
                                bestEnd = buffer.getSpanEnd(candidates[i]);
                            }
                        }
                    }
    
                    if (bestEnd < Integer.MAX_VALUE) {
                        Selection.setSelection(buffer, bestStart, bestEnd);
                        return true;
                    }
    
                    break;
            }
    
            return false;
        }
    
        @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[] links = buffer.getSpans(off, off, ClickableSpan.class);
    
                if (links.length != 0) {
                    ClickableSpan link = links[0];
                    if (action == MotionEvent.ACTION_UP) {
                        if (link instanceof TextLinkSpan) {
                            ((TextLinkSpan) link).onClick(
                                    widget, TextLinkSpan.INVOCATION_METHOD_TOUCH);
                        } else {
                            link.onClick(widget);
                        }
                    } else if (action == MotionEvent.ACTION_DOWN) {
                        if (widget.getContext().getApplicationInfo().targetSdkVersion
                                >= Build.VERSION_CODES.P) {
                            // Selection change will reposition the toolbar. Hide it for a few ms for a
                            // smoother transition.
                            widget.hideFloatingToolbar(HIDE_FLOATING_TOOLBAR_DELAY_MS);
                        }
                        Selection.setSelection(buffer,
                                buffer.getSpanStart(link),
                                buffer.getSpanEnd(link));
                    }
                    return true;
                } else {
                    Selection.removeSelection(buffer);
                }
            }
    
            return super.onTouchEvent(widget, buffer, event);
        }
    
        @Override
        public void initialize(TextView widget, Spannable text) {
            Selection.removeSelection(text);
            text.removeSpan(FROM_BELOW);
        }
    
        @Override
        public void onTakeFocus(TextView view, Spannable text, int dir) {
            Selection.removeSelection(text);
    
            if ((dir & View.FOCUS_BACKWARD) != 0) {
                text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT);
            } else {
                text.removeSpan(FROM_BELOW);
            }
        }
    
        public static MovementMethod getInstance() {
            if (sInstance == null)
                sInstance = new LinkMovementMethod();
    
            return sInstance;
        }
    
        @UnsupportedAppUsage
        private static LinkMovementMethod sInstance;
        private static Object FROM_BELOW = new NoCopySpan.Concrete();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265

        源码有点多,但是我们的目标是实现点击效果,那么肯定跟触摸事件相关,所以,我们需要处理的也就是 onTouchEvent() 方法。接下来,我们通过继承 LinkMovementMethod 来自定义一个MovementMethod 类,在onTouchEvent() 方法中的 MotionEvent.ACTION_DOWNMotionEvent.ACTION_UP 事件中添加处理逻辑。

    /**
     * 可点击标记 MovementMethod
     * @param clickedBgColor 按下背景颜色
     */
    class ClickableSpanMovementMethod(@ColorInt val clickedBgColor: Int) : LinkMovementMethod() {
        override fun onTouchEvent(widget: TextView?, buffer: Spannable?,  event: MotionEvent?): Boolean {
            if(null == event || null == widget || null == buffer) {
                return false
            }
            val action = event.action
            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
                var x = event.x.toInt()
                var y = event.y.toInt()
                x -= widget.totalPaddingLeft
                y -= widget.totalPaddingTop
                x += widget.scrollX
                y += widget.scrollY
                val layout = widget.layout
                val line = layout.getLineForVertical(y)
                val off = layout.getOffsetForHorizontal(line, x.toFloat())
                val links = buffer!!.getSpans(off, off, ClickableSpan::class.java)
                if (links.isNotEmpty()) {
                    val link = links[0]
                    if (action == MotionEvent.ACTION_UP) {
                        // ACTION_UP 移除选中
                        Selection.removeSelection(buffer)
                        link.onClick(widget)
                    } else if (action == MotionEvent.ACTION_DOWN) {
                        // ACTION_DOWN 设置高亮色为点击色,并选中标记
                        widget.highlightColor = clickedBgColor
                        Selection.setSelection(
                            buffer,
                            buffer.getSpanStart(link),
                            buffer.getSpanEnd(link)
                        )
                    }
                    return true
                } else {
                    Selection.removeSelection(buffer)
                }
            }
            return super.onTouchEvent(widget, buffer, event)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

        然后将 TextViewmovementMethod 属性值设置为自定义的 MovementMethod 实例对象即可:

    val tvStyle = findViewById<TextView>(R.id.tv_clickstyle_clickable_span)
    tvStyle.movementMethod = ClickableSpanMovementMethod(Color.argb(0x20, 0x33, 0x33, 0x33))
    tvStyle.setText(SpannableString("我是带点击效果的ClickableSpan").apply {
        setSpan(CSClickableSpan(Color.BLUE, View.OnClickListener {
            Toast.makeText(this@ClickableSpanActivity, tvStyle.text, Toast.LENGTH_SHORT).show()
        }), 8, 21, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 实现效果
      ClickableSpan点击效果
          至此,已经完美实现 ClickableSpan 点击效果。上面的示例是通过改变选中高亮色来实现的,下面是通过给 ClickableSpan 重叠一个 BackgroundColorSpan 的实现方案,效果完全一致,代码如下所示:
    /**
     * 可点击标记 MovementMethod
     * @param clickedBgColor 按下背景颜色
     */
    class ClickableSpanMovementMethod(@ColorInt val clickedBgColor: Int) : LinkMovementMethod() {
        override fun onTouchEvent(widget: TextView?, buffer: Spannable?, event: MotionEvent?): Boolean {
            if(null == event || null == widget || null == buffer) {
                return false
            }
            val action = event.action
            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
                var x = event.x.toInt()
                var y = event.y.toInt()
                x -= widget.totalPaddingLeft
                y -= widget.totalPaddingTop
                x += widget.scrollX
                y += widget.scrollY
                val layout = widget.layout
                val line = layout.getLineForVertical(y)
                val off = layout.getOffsetForHorizontal(line, x.toFloat())
                val links = buffer!!.getSpans(off, off, ClickableSpan::class.java)
                if (links.isNotEmpty()) {
                    val link = links[0]
                    if (action == MotionEvent.ACTION_UP) {
                        // ACTION_UP 给当前标记添加一个透明色的背景Span
                        buffer.setSpan(
                            BackgroundColorSpan(Color.TRANSPARENT), buffer.getSpanStart(link),
                            buffer.getSpanEnd(link), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
                        // 移除选中(如果将TextView高亮色设置为透明,可忽略此行代码)
                        Selection.removeSelection(buffer)
                        link.onClick(widget)
                    } else if (action == MotionEvent.ACTION_DOWN) {
                        // ACTION_DOWN 给当前标记添加一个点击色的背景Span
                        buffer.setSpan(BackgroundColorSpan(clickedBgColor), buffer.getSpanStart(link),
                            buffer.getSpanEnd(link), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
                        // 移除选中(如果将TextView高亮色设置为透明,可忽略此行代码)
                        Selection.removeSelection(buffer)
                    }
                    return true
                } else {
                    Selection.removeSelection(buffer)
                }
            }
    //            return false
            return super.onTouchEvent(widget, buffer, event)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    3.2 在ClickableSpan中实现点击改变标记字体颜色

        我们知道,在实例化 Span 对象时,我们只能传 int 类型的颜色值,无法传入 ColorList 类型,因此点击时改变标记字体颜色,也必须通过自定义才能实现效果。有了前车之鉴,实现点击时改变字体颜色就很容易了。在上一章节中提到的实现点击效果,可以通过给文字一个叠加的背景色标记来实现,那么改变文字颜色,就可以通过叠加一个前景色标记来实现。下面直接上代码:

    /**
     * 可点击标记 MovementMethod
     * @param clickedBgColor 按下背景颜色
     * @param normalTextColor 普通模式下文字颜色
     * @param clickedTextColor 按下文字颜色
     */
    class ClickableSpanMovementMethod(@ColorInt val clickedBgColor: Int, @ColorInt val normalTextColor : Int,
                                      @ColorInt val clickedTextColor: Int) : LinkMovementMethod() {
        override fun onTouchEvent(widget: TextView?, buffer: Spannable?, event: MotionEvent?): Boolean {
            if(null == event || null == widget || null == buffer) {
                return false
            }
            val action = event.action
            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
                var x = event.x.toInt()
                var y = event.y.toInt()
                x -= widget.totalPaddingLeft
                y -= widget.totalPaddingTop
                x += widget.scrollX
                y += widget.scrollY
                val layout = widget.layout
                val line = layout.getLineForVertical(y)
                val off = layout.getOffsetForHorizontal(line, x.toFloat())
                val links = buffer!!.getSpans(off, off, ClickableSpan::class.java)
                if (links.isNotEmpty()) {
                    val link = links[0]
                    if (action == MotionEvent.ACTION_UP) {
                        // ACTION_UP 给当前标记添加一个透明色的背景Span
                        buffer.setSpan(
                            BackgroundColorSpan(Color.TRANSPARENT), buffer.getSpanStart(link),
                            buffer.getSpanEnd(link), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
                        // ACTION_UP 恢复普通字体颜色
                        buffer.setSpan(ForegroundColorSpan(normalTextColor), buffer.getSpanStart(link),
                            buffer.getSpanEnd(link), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
    
                        // ACTION_UP 移除选中
                        // 移除选中(如果将TextView高亮色设置为透明,可忽略此行代码)
                        Selection.removeSelection(buffer)
                        link.onClick(widget)
                    } else if (action == MotionEvent.ACTION_DOWN) {
                        // ACTION_DOWN 给当前标记添加一个点击色的背景Span
                        buffer.setSpan(BackgroundColorSpan(clickedBgColor), buffer.getSpanStart(link),
                            buffer.getSpanEnd(link), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
                        // ACTION_DOWN 给当前标记添加一个点击色的前景Span
                        buffer.setSpan(ForegroundColorSpan(clickedTextColor), buffer.getSpanStart(link),
                            buffer.getSpanEnd(link), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
                        // 移除选中(如果将TextView高亮色设置为透明,可忽略此行代码)
                        Selection.removeSelection(buffer)
                    }
                    return true
                } else {
                    Selection.removeSelection(buffer)
                }
            }
            return super.onTouchEvent(widget, buffer, event)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 实现效果
    • ClickableSpan-点击改变文字颜色
  • 相关阅读:
    vue导出Excel
    深度学习之基于YoloV5苹果新鲜程度检测识别系统
    助力道路场景下智能环境识别,基于YOLOv8全系列【n/s/m/l/x】参数模型开发构建道路场景下的道路边侧裸土检测识别分析系统
    Spring注解系列——@PropertySource
    手机NFC录入门禁数据,实现手机开门
    分割学习(loss and Evaluation)
    msvcr120.dll缺失怎么修复,快速修复msvcr120.dll丢失的三个有效方法
    云计算实验2 Spark分布式内存计算框架配置及编程案例
    一文拿捏线程和线程池的创建方式
    设计模式:桥接模式
  • 原文地址:https://blog.csdn.net/yingaizhu/article/details/128200289