Jacky's blog
首页
  • 学习笔记

    • web
    • android
    • iOS
    • vue
  • 分类
  • 标签
  • 归档
收藏
  • tool
  • algo
  • python
  • java
  • server
  • growth
  • frida
  • blog
  • SP
  • more
GitHub (opens new window)

Jack Yang

编程; 随笔
首页
  • 学习笔记

    • web
    • android
    • iOS
    • vue
  • 分类
  • 标签
  • 归档
收藏
  • tool
  • algo
  • python
  • java
  • server
  • growth
  • frida
  • blog
  • SP
  • more
GitHub (opens new window)
  • tutorial
  • jetpack

  • components

  • androidx

  • 动态化
  • apm

  • module

  • harmony

  • tool

  • other

    • Flutter 高频面试问答
    • 生产环境Message分发处理设计
    • 事件分发机制
    • 调研抖音对harmonyOS4的优化
    • Android 评论at功能的实现
      • span
        • children
      • 开始
        • 抖音@提示效果
        • 抖音 SuggestionSpan
        • 处理表情面板 del
        • 占位符的拆分与组装
      • 参考
    • 探索抖音禁止录屏
    • 对32位手机崩溃的优化记录
    • GradientDrawable
    • android window
    • color
    • webview白屏检测
    • android Resource
    • deeplink技术
    • android-xml
    • ANDROID IPC
    • BottomSheetBehavior研究与思考
    • viewPager
    • Android密钥系统
    • compiler
    • 提升UI加载速度的几点思考
    • Android零耗时首帧体验
    • jsbridge
    • retrofit动态代理设计
    • gif与属性动画的对比
  • kotlin

  • 《android》
  • other
Jacky
2024-02-22
目录

Android 评论at功能的实现

逆向抖音, 获取 EditText 的实现是 CommentMentionEditText 组件, 借助, 这个名字在 github 上搜索相关的内容

# span

记住一句话: 一切皆 span

# children

  • BackgroundColorSpan: 设置文本背景颜色, 参数传入一个 int 类型的颜色
  • ForegroundColorSpan: 设置文本颜色, 参数传入一个 int 类型的颜色
  • ClickableSpan: 设置点击事件, 需要继承这个类重写 onClick 方法

更多 (opens new window)

如自定义 emoji, 继承关系可见如下:

- CharacterStyle/UpdateLayout
  - MetricAffectingSpan
    - ReplacementSpan
      - DynamicDrawableSpan
        - ImageSpan
          - EmojiSpan
            - SizeEmojiSpan
1
2
3
4
5
6
7

# 开始

# 抖音@提示效果

如下图:

通过 frida dump 出来的内存如下:

{
    "spans": [
        {
            "spanStart": 0,
            "spanEnd": 8,
            "clazz": "android.text.DynamicLayout$ChangeWatcher"
        },
        {
            "spanStart": 0,
            "spanEnd": 8,
            "clazz": "android.widget.TextView$ChangeWatcher"
        },
        {
            "spanStart": 0,
            "spanEnd": 8,
            "clazz": "android.text.method.TextKeyListener"
        },
        {
            "spanStart": 0,
            "spanEnd": 8,
            "clazz": "android.widget.Editor$SpanController"
        },
        { "spanStart": 1, "spanEnd": 1, "clazz": "android.text.Selection$START" },
        { "spanStart": 1, "spanEnd": 1, "clazz": "android.text.Selection$END" },
        {
            "spanStart": 1,
            "spanEnd": 8,
            "clazz": "com.ss.android.ugc.aweme.social.at.utils.MentionHintSpan"
        },
        {
            "spanStart": 0,
            "spanEnd": 8,
            "clazz": "android.text.style.SpellCheckSpan"
        }
    ],
    "text": "@输入搜索@的人"
}
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

二、在输入@1setText 的调用如下:

其实现也是通过 @1 + hint 然后 remove hint 的方式来实现, hint 的提示效果

EditTextClz: com.ss.android.ugc.aweme.comment.widget.CommentMentionEditText
ViewId: 2131367606
text: @1输入搜索@的人
------------startFlag:48180395,objectHash:253684777,thread(id:2,name:main),timestamp:1709726438706---------------
android.widget.EditText.setText()
        at android.widget.TextView.setText(Native Method)
        at X.pSN.LIZ(SourceFile:17170465)
        at X.pT1.LIZ(SourceFile:50724951)
        at X.pTY.LIZIZ(SourceFile:50724882)
        at X.pTY.LIZ(SourceFile:50659349)
        at X.pT1.LIZ(SourceFile:17104935)
        at X.pT0.LIZ(SourceFile:178)
        at X.pSA.afterTextChanged(SourceFile:17236285)
        at X.pSL.afterTextChanged(SourceFile:17170432)
        at android.widget.TextView.sendAfterTextChanged(TextView.java:11066)
        at android.widget.TextView$ChangeWatcher.afterTextChanged(TextView.java:14164)
        at android.text.SpannableStringBuilder.sendAfterTextChanged(SpannableStringBuilder.java:1278)
        at android.text.SpannableStringBuilder.replace(SpannableStringBuilder.java:578)
        at android.text.SpannableStringBuilder.replace(SpannableStringBuilder.java:508)
        at android.text.SpannableStringBuilder.replace(SpannableStringBuilder.java:38)
        at android.view.inputmethod.BaseInputConnection.replaceText(BaseInputConnection.java:941)
        at android.view.inputmethod.BaseInputConnection.commitText(BaseInputConnection.java:219)
        at com.android.internal.inputmethod.EditableInputConnection.commitText(EditableInputConnection.java:201)
        at android.view.inputmethod.InputConnectionWrapper.commitText(InputConnectionWrapper.java:200)
        at X.pks.commitText(SourceFile:33685508)
        at com.android.internal.inputmethod.RemoteInputConnectionImpl.lambda$commitText$16$com-android-internal-inputmethod-RemoteInputConnectionImpl(RemoteInputConnectionImpl.java:569)
        at com.android.internal.inputmethod.RemoteInputConnectionImpl$$ExternalSyntheticLambda34.run(Unknown Source:8)
        at android.os.Handler.handleCallback(Handler.java:942)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loopOnce(Looper.java:201)
        at android.os.Looper.loop(Looper.java:288)
        at android.app.ActivityThread.main(ActivityThread.java:7898)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
------------endFlag:48180395,usedtime:3---------------

EditTextClz: com.ss.android.ugc.aweme.comment.widget.CommentMentionEditText
ViewId: 2131367606
text: @1
------------startFlag:11062723,objectHash:253684777,thread(id:2,name:main),timestamp:1709726438711---------------
android.widget.EditText.setText()
        at android.widget.TextView.setText(Native Method)
        at X.pSN.LIZ(SourceFile:17170465)
        at X.pT1.LIZ(SourceFile:50724951)
        at X.pTY.LIZIZ(SourceFile:50724882)
        at X.pTY.LIZ(SourceFile:50659349)
        at X.pT1.LIZ(SourceFile:17104935)
        at X.pT0.LIZ(SourceFile:178)
        at X.pSA.afterTextChanged(SourceFile:17236285)
        at X.pSL.afterTextChanged(SourceFile:17170432)
        at android.widget.TextView.sendAfterTextChanged(TextView.java:11066)
        at android.widget.TextView$ChangeWatcher.afterTextChanged(TextView.java:14164)
        at android.text.SpannableStringBuilder.sendAfterTextChanged(SpannableStringBuilder.java:1278)
        at android.text.SpannableStringBuilder.replace(SpannableStringBuilder.java:578)
        at android.text.SpannableStringBuilder.delete(SpannableStringBuilder.java:231)
        at android.text.SpannableStringBuilder.delete(SpannableStringBuilder.java:38)
        at kotlin.jvm.internal.ALambdaS661S0100000_34.invoke$28(SourceFile:50528282)
        at kotlin.jvm.internal.ALambdaS661S0100000_34.invoke(Unknown Source:18)
        at X.pUO.LIZ(SourceFile:17039414)
        at kotlin.jvm.internal.ALambdaS575S0100000_34.invoke$507(SourceFile:16973835)
        at kotlin.jvm.internal.ALambdaS575S0100000_34.invoke(Unknown Source:167)
        at X.pUO.LIZ(SourceFile:17104928)
        at X.pUO.LIZJ(SourceFile:196626)
        at com.ss.android.ugc.aweme.social.at.utils.MentionHintHelper$handleOnSelectionChanged$1.invoke(SourceFile:33816629)
        at X.pUO.LIZ(SourceFile:17039414)
        at X.pUO.LIZ(SourceFile:33947682)
        at X.pSD.LIZ(SourceFile:33751054)
        at com.ss.android.ugc.aweme.comment.widget.CommentMentionEditText.onSelectionChanged(SourceFile:33685511)
        at android.widget.TextView.spanChange(TextView.java:11224)
        at android.widget.TextView$ChangeWatcher.onSpanChanged(TextView.java:14176)
        at android.text.SpannableStringBuilder.sendSpanChanged(SpannableStringBuilder.java:1308)
        at android.text.SpannableStringBuilder.sendToSpanWatchers(SpannableStringBuilder.java:652)
        at android.text.SpannableStringBuilder.replace(SpannableStringBuilder.java:581)
        at android.text.SpannableStringBuilder.replace(SpannableStringBuilder.java:508)
        at android.text.SpannableStringBuilder.replace(SpannableStringBuilder.java:38)
        at android.view.inputmethod.BaseInputConnection.replaceText(BaseInputConnection.java:941)
        at android.view.inputmethod.BaseInputConnection.commitText(BaseInputConnection.java:219)
        at com.android.internal.inputmethod.EditableInputConnection.commitText(EditableInputConnection.java:201)
        at android.view.inputmethod.InputConnectionWrapper.commitText(InputConnectionWrapper.java:200)
        at X.pks.commitText(SourceFile:33685508)
        at com.android.internal.inputmethod.RemoteInputConnectionImpl.lambda$commitText$16$com-android-internal-inputmethod-RemoteInputConnectionImpl(RemoteInputConnectionImpl.java:569)
        at com.android.internal.inputmethod.RemoteInputConnectionImpl$$ExternalSyntheticLambda34.run(Unknown Source:8)
        at android.os.Handler.handleCallback(Handler.java:942)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loopOnce(Looper.java:201)
        at android.os.Looper.loop(Looper.java:288)
        at android.app.ActivityThread.main(ActivityThread.java:7898)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
------------endFlag:11062723,usedtime:3---------------

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

# 抖音 SuggestionSpan

注意到 gg 下面有一条红色线条, 原来其为 SuggestionSpan, 内容如下:

{
    "spans": [
        {
            "spanStart": 0,
            "spanEnd": 10,
            "clazz": "android.text.DynamicLayout$ChangeWatcher"
        },
        {
            "spanStart": 0,
            "spanEnd": 10,
            "clazz": "android.widget.TextView$ChangeWatcher"
        },
        {
            "spanStart": 0,
            "spanEnd": 10,
            "clazz": "android.text.method.TextKeyListener"
        },
        {
            "spanStart": 0,
            "spanEnd": 10,
            "clazz": "android.widget.Editor$SpanController"
        },
        { "spanStart": 10, "spanEnd": 10, "clazz": "android.text.Selection$START" },
        { "spanStart": 10, "spanEnd": 10, "clazz": "android.text.Selection$END" },
        {
            "spanStart": 0,
            "spanEnd": 5,
            "clazz": "com.ss.android.ugc.aweme.social.at.MentionEditText$MentionSpan"
        },
        {
            "spanStart": 7,
            "spanEnd": 9,
            "clazz": "android.text.style.SuggestionSpan"
        }
    ],
    "text": "@福州沁儿 @gg "
}
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

# 处理表情面板 del

删除有两个地方可以触发: 键盘的 del、表情面板的 del。 这里存在一个问题在键盘 ACTION_DOWN 事件未处理, 会传递给 view 的 ACTION_DOWN, InputConnectionWrapper#sendKeyEvent 与 View#onKey 实现一致, 这里需要做事件的去重处理。 代码略

# 占位符的拆分与组装

代码牵扯到私密问题, 这里不提供。link

# 参考

  • Android 如何优雅地实现@人功能?——仿微博、仿 QQ、仿微信、零入侵、高扩展性 (opens new window)
  • Android 中 Spannable 的使用 (opens new window)
  • MentionEditText (opens new window)
  • github-search-result (opens new window)
#评论艾特
上次更新: 2025/10/09, 23:53:03
调研抖音对harmonyOS4的优化
探索抖音禁止录屏

← 调研抖音对harmonyOS4的优化 探索抖音禁止录屏→

最近更新
01
npx 使用指南
10-12
02
cursor
09-28
03
inspect
07-20
更多文章>
Theme by Vdoing | Copyright © 2019-2025 Jacky | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式