Skip to content

Commit 742af71

Browse files
committed
feat: new (compact) candidate view using recyclerview
1 parent 54de655 commit 742af71

File tree

15 files changed

+489
-46
lines changed

15 files changed

+489
-46
lines changed

app/src/main/java/com/osfans/trime/ime/bar/QuickBar.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ import com.osfans.trime.data.schema.SchemaManager
1919
import com.osfans.trime.data.theme.ColorManager
2020
import com.osfans.trime.data.theme.Theme
2121
import com.osfans.trime.ime.bar.ui.AlwaysUi
22+
import com.osfans.trime.ime.bar.ui.CandidateUi
2223
import com.osfans.trime.ime.bar.ui.TabUi
2324
import com.osfans.trime.ime.broadcast.InputBroadcastReceiver
24-
import com.osfans.trime.ime.candidates.CompatCandidateModule
25+
import com.osfans.trime.ime.candidates.CompactCandidateModule
2526
import com.osfans.trime.ime.core.TrimeInputMethodService
2627
import com.osfans.trime.ime.dependency.InputScope
2728
import com.osfans.trime.ime.window.BoardWindow
@@ -38,7 +39,7 @@ class QuickBar(
3839
private val service: TrimeInputMethodService,
3940
private val rime: RimeSession,
4041
private val theme: Theme,
41-
private val compatCandidate: CompatCandidateModule,
42+
private val compactCandidate: CompactCandidateModule,
4243
) : InputBroadcastReceiver {
4344
private val prefs = AppPrefs.defaultInstance()
4445

@@ -85,7 +86,7 @@ class QuickBar(
8586
}
8687

8788
private val candidateUi by lazy {
88-
compatCandidate.binding
89+
CandidateUi(context, compactCandidate.view)
8990
}
9091

9192
private val tabUi by lazy {
@@ -158,7 +159,7 @@ class QuickBar(
158159
override fun onRimeOptionUpdated(value: OptionNotification.Value) {
159160
when (value.option) {
160161
"_hide_comment" -> {
161-
candidateUi.candidates.shouldShowComment = !value.value
162+
// candidateUi.candidates.shouldShowComment = !value.value
162163
}
163164
"_hide_candidate", "_hide_bar" -> {
164165
view.visibility = if (value.value) View.GONE else View.VISIBLE
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.osfans.trime.ime.bar.ui
2+
3+
import android.content.Context
4+
import android.view.View
5+
import com.osfans.trime.R
6+
import splitties.dimensions.dp
7+
import splitties.views.dsl.constraintlayout.before
8+
import splitties.views.dsl.constraintlayout.centerVertically
9+
import splitties.views.dsl.constraintlayout.constraintLayout
10+
import splitties.views.dsl.constraintlayout.endOfParent
11+
import splitties.views.dsl.constraintlayout.lParams
12+
import splitties.views.dsl.constraintlayout.startOfParent
13+
import splitties.views.dsl.core.Ui
14+
import splitties.views.dsl.core.add
15+
16+
class CandidateUi(override val ctx: Context, private val compatView: View) : Ui {
17+
val unrollButton =
18+
ToolButton(ctx, R.drawable.ic_baseline_expand_more_24).apply {
19+
visibility = View.INVISIBLE
20+
}
21+
22+
override val root =
23+
ctx.constraintLayout {
24+
add(
25+
unrollButton,
26+
lParams(dp(40)) {
27+
centerVertically()
28+
endOfParent()
29+
},
30+
)
31+
add(
32+
compatView,
33+
lParams {
34+
centerVertically()
35+
startOfParent()
36+
before(unrollButton)
37+
},
38+
)
39+
}
40+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// SPDX-FileCopyrightText: 2015 - 2024 Rime community
2+
//
3+
// SPDX-License-Identifier: GPL-3.0-or-later
4+
package com.osfans.trime.ime.bar.ui
5+
6+
import android.content.Context
7+
import android.content.res.ColorStateList
8+
import android.graphics.Color
9+
import android.widget.FrameLayout
10+
import android.widget.ImageView
11+
import androidx.annotation.ColorInt
12+
import androidx.annotation.DrawableRes
13+
import com.osfans.trime.data.theme.ColorManager
14+
import com.osfans.trime.util.circlePressHighlightDrawable
15+
import splitties.dimensions.dp
16+
import splitties.views.dsl.core.add
17+
import splitties.views.dsl.core.imageView
18+
import splitties.views.dsl.core.lParams
19+
import splitties.views.dsl.core.wrapContent
20+
import splitties.views.gravityCenter
21+
import splitties.views.imageResource
22+
import splitties.views.padding
23+
24+
class ToolButton(context: Context) : FrameLayout(context) {
25+
val image =
26+
imageView {
27+
isClickable = false
28+
isFocusable = false
29+
padding = dp(10)
30+
scaleType = ImageView.ScaleType.CENTER_INSIDE
31+
}
32+
33+
constructor(
34+
context: Context,
35+
@DrawableRes icon: Int,
36+
) : this(context) {
37+
image.imageTintList = ColorStateList.valueOf(ColorManager.getColor("comment_text_color") ?: Color.WHITE)
38+
setIcon(icon)
39+
ColorManager.getColor("hilited_on_key_back_color")?.let { setPressHighlightColor(it) }
40+
add(image, lParams(wrapContent, wrapContent, gravityCenter))
41+
}
42+
43+
fun setIcon(
44+
@DrawableRes icon: Int,
45+
) {
46+
image.imageResource = icon
47+
}
48+
49+
fun setPressHighlightColor(
50+
@ColorInt color: Int,
51+
) {
52+
background = circlePressHighlightDrawable(color)
53+
}
54+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.osfans.trime.ime.candidates
2+
3+
import android.content.Context
4+
import android.view.View
5+
import com.osfans.trime.data.theme.ColorManager
6+
import com.osfans.trime.data.theme.FontManager
7+
import com.osfans.trime.data.theme.Theme
8+
import splitties.views.dsl.constraintlayout.above
9+
import splitties.views.dsl.constraintlayout.before
10+
import splitties.views.dsl.constraintlayout.centerHorizontally
11+
import splitties.views.dsl.constraintlayout.centerVertically
12+
import splitties.views.dsl.constraintlayout.constraintLayout
13+
import splitties.views.dsl.constraintlayout.endOfParent
14+
import splitties.views.dsl.constraintlayout.lParams
15+
import splitties.views.dsl.constraintlayout.startOfParent
16+
import splitties.views.dsl.constraintlayout.topOfParent
17+
import splitties.views.dsl.core.Ui
18+
import splitties.views.dsl.core.add
19+
import splitties.views.dsl.core.matchParent
20+
import splitties.views.dsl.core.textView
21+
import splitties.views.dsl.core.wrapContent
22+
import splitties.views.gravityCenter
23+
24+
class CandidateItemUi(override val ctx: Context, theme: Theme) : Ui {
25+
private val text =
26+
textView {
27+
textSize = theme.generalStyle.candidateTextSize.toFloat()
28+
typeface = FontManager.getTypeface("candidate_font")
29+
isSingleLine = true
30+
gravity = gravityCenter
31+
ColorManager.getColor("candidate_text_color")?.let { setTextColor(it) }
32+
}
33+
34+
private val comment =
35+
textView {
36+
textSize = theme.generalStyle.commentTextSize.toFloat()
37+
typeface = FontManager.getTypeface("comment_font")
38+
isSingleLine = true
39+
gravity = gravityCenter
40+
ColorManager.getColor("comment_text_color")?.let { setTextColor(it) }
41+
visibility = View.GONE
42+
}
43+
44+
override val root =
45+
constraintLayout {
46+
if (theme.generalStyle.commentOnTop) {
47+
add(
48+
comment,
49+
lParams(wrapContent, matchParent) {
50+
topOfParent()
51+
centerHorizontally()
52+
above(text)
53+
},
54+
)
55+
add(
56+
text,
57+
lParams(wrapContent, matchParent) {
58+
centerHorizontally()
59+
topOfParent()
60+
},
61+
)
62+
} else {
63+
add(
64+
text,
65+
lParams(wrapContent, matchParent) {
66+
startOfParent()
67+
centerVertically()
68+
before(comment)
69+
},
70+
)
71+
add(
72+
comment,
73+
lParams(wrapContent, matchParent) {
74+
centerVertically()
75+
endOfParent()
76+
},
77+
)
78+
}
79+
}
80+
81+
fun setText(str: String) {
82+
text.text = str
83+
}
84+
85+
fun setComment(str: String) {
86+
comment.run {
87+
if (str.isNotEmpty()) {
88+
text = str
89+
if (visibility == View.GONE) visibility = View.VISIBLE
90+
} else if (visibility != View.GONE) {
91+
visibility = View.GONE
92+
}
93+
}
94+
}
95+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.osfans.trime.ime.candidates
2+
3+
import androidx.recyclerview.widget.RecyclerView
4+
5+
class CandidateViewHolder(val ui: CandidateItemUi) : RecyclerView.ViewHolder(ui.root) {
6+
var idx = -1
7+
var text = ""
8+
var comment = ""
9+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package com.osfans.trime.ime.candidates
2+
3+
import android.content.Context
4+
import android.graphics.drawable.ShapeDrawable
5+
import android.graphics.drawable.shapes.RectShape
6+
import android.text.SpannableStringBuilder
7+
import android.view.View
8+
import android.widget.PopupMenu
9+
import androidx.core.text.bold
10+
import androidx.core.text.buildSpannedString
11+
import androidx.core.text.color
12+
import androidx.lifecycle.lifecycleScope
13+
import androidx.recyclerview.widget.RecyclerView
14+
import com.chad.library.adapter4.util.setOnDebouncedItemClick
15+
import com.google.android.flexbox.FlexboxLayoutManager
16+
import com.osfans.trime.R
17+
import com.osfans.trime.daemon.RimeSession
18+
import com.osfans.trime.daemon.launchOnReady
19+
import com.osfans.trime.data.theme.ColorManager
20+
import com.osfans.trime.data.theme.Theme
21+
import com.osfans.trime.ime.broadcast.InputBroadcastReceiver
22+
import com.osfans.trime.ime.candidates.adapter.CompactCandidateViewAdapter
23+
import com.osfans.trime.ime.candidates.unrolled.decoration.FlexboxVerticalDecoration
24+
import com.osfans.trime.ime.core.TrimeInputMethodService
25+
import com.osfans.trime.ime.dependency.InputScope
26+
import com.osfans.trime.ime.keyboard.InputFeedbackManager
27+
import kotlinx.coroutines.channels.BufferOverflow
28+
import kotlinx.coroutines.flow.MutableSharedFlow
29+
import kotlinx.coroutines.flow.asSharedFlow
30+
import kotlinx.coroutines.launch
31+
import kotlinx.coroutines.runBlocking
32+
import me.tatarka.inject.annotations.Inject
33+
import splitties.dimensions.dp
34+
import splitties.views.dsl.recyclerview.recyclerView
35+
import kotlin.math.max
36+
37+
@InputScope
38+
@Inject
39+
class CompactCandidateModule(
40+
val context: Context,
41+
val service: TrimeInputMethodService,
42+
val rime: RimeSession,
43+
val theme: Theme,
44+
) : InputBroadcastReceiver {
45+
private val _unrolledCandidateOffset =
46+
MutableSharedFlow<Int>(
47+
replay = 1,
48+
onBufferOverflow = BufferOverflow.DROP_OLDEST,
49+
)
50+
51+
val unrolledCandidateOffset = _unrolledCandidateOffset.asSharedFlow()
52+
53+
private fun refreshUnrolled() {
54+
runBlocking {
55+
_unrolledCandidateOffset.emit(adapter.stickyOffset + view.childCount)
56+
}
57+
}
58+
59+
val adapter by lazy {
60+
CompactCandidateViewAdapter(theme).apply {
61+
setOnDebouncedItemClick { _, _, position ->
62+
rime.launchOnReady { it.selectCandidate(stickyOffset + position) }
63+
}
64+
setOnItemLongClickListener { _, view, position ->
65+
showCandidateAction(stickyOffset + position, items[position].text, view)
66+
true
67+
}
68+
}
69+
}
70+
71+
val layoutManager by lazy {
72+
object : FlexboxLayoutManager(context) {
73+
override fun canScrollHorizontally(): Boolean = false
74+
75+
override fun canScrollVertically(): Boolean = false
76+
77+
override fun onLayoutCompleted(state: RecyclerView.State?) {
78+
super.onLayoutCompleted(state)
79+
refreshUnrolled()
80+
}
81+
}
82+
}
83+
84+
private val separatorDrawable by lazy {
85+
ShapeDrawable(RectShape()).apply {
86+
val spacing = theme.generalStyle.candidateSpacing
87+
val intrinsicSize = max(spacing, context.dp(spacing)).toInt()
88+
intrinsicWidth = intrinsicSize
89+
intrinsicHeight = intrinsicSize
90+
ColorManager.getColor("candidate_separator_color")?.let { paint.color = it }
91+
}
92+
}
93+
94+
val view by lazy {
95+
context.recyclerView(R.id.candidates) {
96+
adapter = this@CompactCandidateModule.adapter
97+
layoutManager = this@CompactCandidateModule.layoutManager
98+
addItemDecoration(FlexboxVerticalDecoration(separatorDrawable))
99+
}
100+
}
101+
102+
private var candidateActionMenu: PopupMenu? = null
103+
104+
fun showCandidateAction(
105+
idx: Int,
106+
text: String,
107+
view: View,
108+
) {
109+
candidateActionMenu?.dismiss()
110+
candidateActionMenu = null
111+
service.lifecycleScope.launch {
112+
InputFeedbackManager.keyPressVibrate(view, longPress = true)
113+
candidateActionMenu =
114+
PopupMenu(context, view).apply {
115+
menu.add(
116+
buildSpannedString {
117+
bold {
118+
fun coloredOrNot(action: SpannableStringBuilder.() -> Unit) =
119+
ColorManager.getColor("hilited_candidate_text_color")?.let {
120+
color(it) { action() }
121+
} ?: action(this)
122+
coloredOrNot { append(text) }
123+
}
124+
},
125+
).apply {
126+
isEnabled = false
127+
}
128+
menu.add(R.string.forget_this_word).setOnMenuItemClickListener {
129+
rime.runIfReady { forgetCandidate(idx) }
130+
true
131+
}
132+
setOnDismissListener {
133+
candidateActionMenu = null
134+
}
135+
show()
136+
}
137+
}
138+
}
139+
}

0 commit comments

Comments
 (0)