Android 评论at功能的实现
逆向抖音, 获取 EditText 的实现是 CommentMentionEditText 组件, 借助, 这个名字在 github 上搜索相关的内容
# span
记住一句话: 一切皆 span
# children
- BackgroundColorSpan: 设置文本背景颜色, 参数传入一个 int 类型的颜色
- ForegroundColorSpan: 设置文本颜色, 参数传入一个 int 类型的颜色
- ClickableSpan: 设置点击事件, 需要继承这个类重写 onClick 方法
如自定义 emoji, 继承关系可见如下:
- CharacterStyle/UpdateLayout
- MetricAffectingSpan
- ReplacementSpan
- DynamicDrawableSpan
- ImageSpan
- EmojiSpan
- SizeEmojiSpan
1
2
3
4
5
6
7
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
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
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
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
# 参考
上次更新: 2025/10/09, 23:53:03