diff --git a/app/src/main/java/com/osfans/trime/ime/bar/QuickBar.kt b/app/src/main/java/com/osfans/trime/ime/bar/QuickBar.kt index 13bd6109a2..2ffb2085ab 100644 --- a/app/src/main/java/com/osfans/trime/ime/bar/QuickBar.kt +++ b/app/src/main/java/com/osfans/trime/ime/bar/QuickBar.kt @@ -9,6 +9,7 @@ import android.view.View import android.view.inputmethod.EditorInfo import android.widget.ViewAnimator import androidx.lifecycle.lifecycleScope +import com.osfans.trime.R import com.osfans.trime.core.RimeNotification.OptionNotification import com.osfans.trime.core.RimeProto import com.osfans.trime.core.SchemaItem @@ -23,9 +24,12 @@ import com.osfans.trime.ime.bar.ui.CandidateUi import com.osfans.trime.ime.bar.ui.TabUi import com.osfans.trime.ime.broadcast.InputBroadcastReceiver import com.osfans.trime.ime.candidates.CompactCandidateModule +import com.osfans.trime.ime.candidates.unrolled.window.FlexboxUnrolledCandidateWindow import com.osfans.trime.ime.core.TrimeInputMethodService import com.osfans.trime.ime.dependency.InputScope +import com.osfans.trime.ime.keyboard.KeyboardWindow import com.osfans.trime.ime.window.BoardWindow +import com.osfans.trime.ime.window.BoardWindowManager import kotlinx.coroutines.launch import me.tatarka.inject.annotations.Inject import splitties.views.dsl.core.add @@ -39,8 +43,11 @@ class QuickBar( private val service: TrimeInputMethodService, private val rime: RimeSession, private val theme: Theme, - private val compactCandidate: CompactCandidateModule, + private val windowManager: BoardWindowManager, + lazyCompactCandidate: Lazy, ) : InputBroadcastReceiver { + private val compactCandidate by lazyCompactCandidate + private val prefs = AppPrefs.defaultInstance() private val showSwitchers get() = prefs.keyboard.switchesEnabled @@ -93,14 +100,53 @@ class QuickBar( TabUi(context) } - private val barStateMachine = QuickBarStateMachine.new { - switchUiByState(it) + private val barStateMachine = + QuickBarStateMachine.new { + switchUiByState(it) + } + + val unrollButtonStateMachine = + UnrollButtonStateMachine.new { + when (it) { + UnrollButtonStateMachine.State.ClickToAttachWindow -> { + setUnrollButtonToAttach() + setUnrollButtonEnabled(true) + } + UnrollButtonStateMachine.State.ClickToDetachWindow -> { + setUnrollButtonToDetach() + setUnrollButtonEnabled(true) + } + UnrollButtonStateMachine.State.Hidden -> { + setUnrollButtonEnabled(false) + } + } + } + + private fun setUnrollButtonToAttach() { + candidateUi.unrollButton.setOnClickListener { + windowManager.attachWindow( + FlexboxUnrolledCandidateWindow(context, service, rime, theme, this, windowManager, compactCandidate), + ) + } + candidateUi.unrollButton.setIcon(R.drawable.ic_baseline_expand_more_24) + } + + private fun setUnrollButtonToDetach() { + candidateUi.unrollButton.setOnClickListener { + windowManager.attachWindow(KeyboardWindow) + } + candidateUi.unrollButton.setIcon(R.drawable.ic_baseline_expand_less_24) + } + + private fun setUnrollButtonEnabled(enabled: Boolean) { + candidateUi.unrollButton.visibility = if (enabled) View.VISIBLE else View.INVISIBLE } override fun onInputContextUpdate(ctx: RimeProto.Context) { barStateMachine.push( QuickBarStateMachine.TransitionEvent.CandidatesUpdated, - QuickBarStateMachine.BooleanKey.CandidateEmpty to ctx.menu.candidates.isEmpty()) + QuickBarStateMachine.BooleanKey.CandidateEmpty to ctx.menu.candidates.isEmpty(), + ) } private fun switchUiByState(state: QuickBarStateMachine.State) { diff --git a/app/src/main/java/com/osfans/trime/ime/bar/UnrollButtonStateMachine.kt b/app/src/main/java/com/osfans/trime/ime/bar/UnrollButtonStateMachine.kt new file mode 100644 index 0000000000..8f0364f281 --- /dev/null +++ b/app/src/main/java/com/osfans/trime/ime/bar/UnrollButtonStateMachine.kt @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2015 - 2024 Rime community +// +// SPDX-License-Identifier: GPL-3.0-or-later +package com.osfans.trime.ime.bar + +import com.osfans.trime.ime.bar.UnrollButtonStateMachine.BooleanKey.UnrolledCandidatesEmpty +import com.osfans.trime.ime.bar.UnrollButtonStateMachine.State.ClickToAttachWindow +import com.osfans.trime.ime.bar.UnrollButtonStateMachine.State.ClickToDetachWindow +import com.osfans.trime.ime.bar.UnrollButtonStateMachine.State.Hidden +import com.osfans.trime.util.BuildTransitionEvent +import com.osfans.trime.util.EventStateMachine +import com.osfans.trime.util.TransitionBuildBlock + +object UnrollButtonStateMachine { + enum class State { + ClickToAttachWindow, + ClickToDetachWindow, + Hidden, + } + + enum class BooleanKey : EventStateMachine.BooleanStateKey { + UnrolledCandidatesEmpty, + } + + enum class TransitionEvent(val builder: TransitionBuildBlock) : + EventStateMachine.TransitionEvent by BuildTransitionEvent(builder) { + UnrolledCandidatesUpdated({ + from(Hidden) transitTo ClickToAttachWindow on (UnrolledCandidatesEmpty to false) + from(ClickToAttachWindow) transitTo Hidden on (UnrolledCandidatesEmpty to true) + }), + UnrolledCandidatesAttached({ + from(ClickToAttachWindow) transitTo ClickToDetachWindow + }), + UnrolledCandidatesDetached({ + from(ClickToDetachWindow) transitTo Hidden on (UnrolledCandidatesEmpty to true) + from(ClickToDetachWindow) transitTo ClickToAttachWindow on (UnrolledCandidatesEmpty to false) + }), + } + + fun new(block: (State) -> Unit) = + EventStateMachine( + initialState = Hidden, + externalBooleanStates = + mutableMapOf( + UnrolledCandidatesEmpty to true, + ), + ).apply { + onNewStateListener = block + } +} diff --git a/app/src/main/java/com/osfans/trime/ime/candidates/CompactCandidateModule.kt b/app/src/main/java/com/osfans/trime/ime/candidates/CompactCandidateModule.kt index 3acec9a2d3..b23e79ef74 100644 --- a/app/src/main/java/com/osfans/trime/ime/candidates/CompactCandidateModule.kt +++ b/app/src/main/java/com/osfans/trime/ime/candidates/CompactCandidateModule.kt @@ -18,6 +18,8 @@ import com.osfans.trime.daemon.RimeSession import com.osfans.trime.daemon.launchOnReady import com.osfans.trime.data.theme.ColorManager import com.osfans.trime.data.theme.Theme +import com.osfans.trime.ime.bar.QuickBar +import com.osfans.trime.ime.bar.UnrollButtonStateMachine import com.osfans.trime.ime.broadcast.InputBroadcastReceiver import com.osfans.trime.ime.candidates.adapter.CompactCandidateViewAdapter import com.osfans.trime.ime.candidates.unrolled.decoration.FlexboxVerticalDecoration @@ -41,6 +43,7 @@ class CompactCandidateModule( val service: TrimeInputMethodService, val rime: RimeSession, val theme: Theme, + val bar: QuickBar, ) : InputBroadcastReceiver { private val _unrolledCandidateOffset = MutableSharedFlow( @@ -50,19 +53,24 @@ class CompactCandidateModule( val unrolledCandidateOffset = _unrolledCandidateOffset.asSharedFlow() - private fun refreshUnrolled() { + fun refreshUnrolled() { runBlocking { - _unrolledCandidateOffset.emit(adapter.stickyOffset + view.childCount) + _unrolledCandidateOffset.emit(adapter.run { max(sticky, previous) } + view.childCount) } + bar.unrollButtonStateMachine.push( + UnrollButtonStateMachine.TransitionEvent.UnrolledCandidatesUpdated, + UnrollButtonStateMachine.BooleanKey.UnrolledCandidatesEmpty to + (adapter.run { isLastPage && itemCount == layoutManager.childCount }), + ) } val adapter by lazy { CompactCandidateViewAdapter(theme).apply { setOnDebouncedItemClick { _, _, position -> - rime.launchOnReady { it.selectCandidate(stickyOffset + position) } + rime.launchOnReady { it.selectCandidate(sticky + position) } } setOnItemLongClickListener { _, view, position -> - showCandidateAction(stickyOffset + position, items[position].text, view) + showCandidateAction(sticky + position, items[position].text, view) true } } diff --git a/app/src/main/java/com/osfans/trime/ime/candidates/adapter/CompactCandidateViewAdapter.kt b/app/src/main/java/com/osfans/trime/ime/candidates/adapter/CompactCandidateViewAdapter.kt index 9127066b09..819bb45cc4 100644 --- a/app/src/main/java/com/osfans/trime/ime/candidates/adapter/CompactCandidateViewAdapter.kt +++ b/app/src/main/java/com/osfans/trime/ime/candidates/adapter/CompactCandidateViewAdapter.kt @@ -15,15 +15,25 @@ import splitties.views.dsl.core.wrapContent import splitties.views.setPaddingDp open class CompactCandidateViewAdapter(val theme: Theme) : BaseQuickAdapter() { - var stickyOffset: Int = 0 + var sticky: Int = 0 + private set + + var isLastPage: Boolean = false + private set + + var previous: Int = 0 private set fun updateCandidates( list: List, - offset: Int = 0, + isLastPage: Boolean, + previous: Int, + sticky: Int = 0, ) { - stickyOffset = offset - super.submitList(list.drop(offset)) + this.isLastPage = isLastPage + this.previous = previous + this.sticky = sticky + super.submitList(list.drop(sticky)) } override fun onCreateViewHolder( @@ -51,7 +61,7 @@ open class CompactCandidateViewAdapter(val theme: Theme) : BaseQuickAdapter { minWidth = 0 flexGrow = 1f diff --git a/app/src/main/java/com/osfans/trime/ime/candidates/adapter/PagingCandidateViewAdapter.kt b/app/src/main/java/com/osfans/trime/ime/candidates/adapter/PagingCandidateViewAdapter.kt new file mode 100644 index 0000000000..5481edd001 --- /dev/null +++ b/app/src/main/java/com/osfans/trime/ime/candidates/adapter/PagingCandidateViewAdapter.kt @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2015 - 2024 Rime community +// +// SPDX-License-Identifier: GPL-3.0-or-later +package com.osfans.trime.ime.candidates.adapter + +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import com.osfans.trime.core.CandidateItem +import com.osfans.trime.data.theme.Theme +import com.osfans.trime.ime.candidates.CandidateItemUi +import com.osfans.trime.ime.candidates.CandidateViewHolder + +open class PagingCandidateViewAdapter(val theme: Theme) : PagingDataAdapter(diffCallback) { + companion object { + private val diffCallback = + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: CandidateItem, + newItem: CandidateItem, + ): Boolean { + return oldItem === newItem + } + + override fun areContentsTheSame( + oldItem: CandidateItem, + newItem: CandidateItem, + ): Boolean { + return oldItem == newItem + } + } + } + + var offset: Int = 0 + private set + + fun refreshWithOffset(offset: Int) { + this.offset = offset + refresh() + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): CandidateViewHolder { + return CandidateViewHolder(CandidateItemUi(parent.context, theme)) + } + + override fun onBindViewHolder( + holder: CandidateViewHolder, + position: Int, + ) { + val (comment, text) = getItem(position)!! + holder.ui.setText(text) + holder.ui.setComment(comment) + holder.text = text + holder.comment = comment + holder.idx = position + offset + } +} diff --git a/app/src/main/java/com/osfans/trime/ime/candidates/unrolled/CandidatesPagingSource.kt b/app/src/main/java/com/osfans/trime/ime/candidates/unrolled/CandidatesPagingSource.kt new file mode 100644 index 0000000000..a1848b7009 --- /dev/null +++ b/app/src/main/java/com/osfans/trime/ime/candidates/unrolled/CandidatesPagingSource.kt @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2015 - 2024 Rime community +// +// SPDX-License-Identifier: GPL-3.0-or-later +package com.osfans.trime.ime.candidates.unrolled + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.osfans.trime.core.CandidateItem +import com.osfans.trime.daemon.RimeSession +import timber.log.Timber + +class CandidatesPagingSource(val rime: RimeSession, val num: Int, val offset: Int) : + PagingSource() { + override suspend fun load(params: LoadParams): LoadResult { + // use candidate index for key, null means load from beginning (including offset) + val startIndex = params.key ?: offset + val pageSize = params.loadSize + Timber.d("getCandidates(offset=$startIndex, limit=$pageSize)") + val candidates = + rime.runOnReady { + getCandidates(startIndex, pageSize) + } + val prevKey = if (startIndex >= pageSize) startIndex - pageSize else null + val nextKey = + if (num > 0) { + if (startIndex + pageSize + 1 >= num) null else startIndex + pageSize + } else { + if (candidates.size < pageSize) null else startIndex + pageSize + } + return LoadResult.Page(candidates.toList(), prevKey, nextKey) + } + + // always reload from beginning + override fun getRefreshKey(state: PagingState) = null +} diff --git a/app/src/main/java/com/osfans/trime/ime/candidates/unrolled/UnrolledCandidateLayout.kt b/app/src/main/java/com/osfans/trime/ime/candidates/unrolled/UnrolledCandidateLayout.kt new file mode 100644 index 0000000000..8cbe456d54 --- /dev/null +++ b/app/src/main/java/com/osfans/trime/ime/candidates/unrolled/UnrolledCandidateLayout.kt @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2015 - 2024 Rime community +// +// SPDX-License-Identifier: GPL-3.0-or-later +package com.osfans.trime.ime.candidates.unrolled + +import android.annotation.SuppressLint +import android.content.Context +import androidx.constraintlayout.widget.ConstraintLayout +import com.osfans.trime.R +import com.osfans.trime.data.theme.ColorManager +import com.osfans.trime.data.theme.Theme +import splitties.views.dsl.constraintlayout.centerInParent +import splitties.views.dsl.constraintlayout.lParams +import splitties.views.dsl.core.add +import splitties.views.dsl.recyclerview.recyclerView + +@SuppressLint("ViewConstructor") +class UnrolledCandidateLayout(context: Context, theme: Theme) : ConstraintLayout(context) { + val recyclerView = + recyclerView { + isVerticalScrollBarEnabled = false + } + + init { + id = R.id.unrolled_candidate_view + background = + ColorManager.getDrawable( + context, + "candidate_background", + theme.generalStyle.candidateBorder, + "candidate_border_color", + theme.generalStyle.candidateBorderRound, + ) + + add( + recyclerView, + lParams { + centerInParent() + }, + ) + } + + fun resetPosition() { + recyclerView.scrollToPosition(0) + } +} diff --git a/app/src/main/java/com/osfans/trime/ime/candidates/unrolled/decoration/FlexboxHorizontalDecoration.kt b/app/src/main/java/com/osfans/trime/ime/candidates/unrolled/decoration/FlexboxHorizontalDecoration.kt new file mode 100644 index 0000000000..1452bb934a --- /dev/null +++ b/app/src/main/java/com/osfans/trime/ime/candidates/unrolled/decoration/FlexboxHorizontalDecoration.kt @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2015 - 2024 Rime community +// +// SPDX-License-Identifier: GPL-3.0-or-later +package com.osfans.trime.ime.candidates.unrolled.decoration + +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.google.android.flexbox.FlexboxLayoutManager + +class FlexboxHorizontalDecoration(val drawable: Drawable) : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State, + ) { + outRect.bottom = drawable.intrinsicHeight + } + + override fun onDraw( + c: Canvas, + parent: RecyclerView, + state: RecyclerView.State, + ) { + val layoutManager = parent.layoutManager as FlexboxLayoutManager + for (i in 0 until layoutManager.childCount) { + val view = parent.getChildAt(i) + val left = view.left + val right = view.right + val top = view.bottom + val bottom = view.bottom + drawable.intrinsicHeight + drawable.setBounds(left, top, right, bottom) + drawable.draw(c) + } + } +} diff --git a/app/src/main/java/com/osfans/trime/ime/candidates/unrolled/window/BaseUnrolledCandidateWindow.kt b/app/src/main/java/com/osfans/trime/ime/candidates/unrolled/window/BaseUnrolledCandidateWindow.kt new file mode 100644 index 0000000000..469683d05b --- /dev/null +++ b/app/src/main/java/com/osfans/trime/ime/candidates/unrolled/window/BaseUnrolledCandidateWindow.kt @@ -0,0 +1,145 @@ +// SPDX-FileCopyrightText: 2015 - 2024 Rime community +// +// SPDX-License-Identifier: GPL-3.0-or-later +package com.osfans.trime.ime.candidates.unrolled.window + +import android.content.Context +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.shapes.RectShape +import android.view.View +import androidx.core.view.updateLayoutParams +import androidx.lifecycle.LifecycleCoroutineScope +import androidx.lifecycle.findViewTreeLifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.recyclerview.widget.RecyclerView +import com.osfans.trime.daemon.RimeSession +import com.osfans.trime.daemon.launchOnReady +import com.osfans.trime.data.theme.ColorManager +import com.osfans.trime.data.theme.Theme +import com.osfans.trime.ime.bar.QuickBar +import com.osfans.trime.ime.bar.UnrollButtonStateMachine +import com.osfans.trime.ime.broadcast.InputBroadcastReceiver +import com.osfans.trime.ime.candidates.CandidateViewHolder +import com.osfans.trime.ime.candidates.CompactCandidateModule +import com.osfans.trime.ime.candidates.adapter.PagingCandidateViewAdapter +import com.osfans.trime.ime.candidates.unrolled.CandidatesPagingSource +import com.osfans.trime.ime.candidates.unrolled.UnrolledCandidateLayout +import com.osfans.trime.ime.core.TrimeInputMethodService +import com.osfans.trime.ime.keyboard.KeyboardSwitcher +import com.osfans.trime.ime.keyboard.KeyboardWindow +import com.osfans.trime.ime.window.BoardWindow +import com.osfans.trime.ime.window.BoardWindowManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import splitties.dimensions.dp +import kotlin.math.max + +abstract class BaseUnrolledCandidateWindow( + protected val context: Context, + protected val service: TrimeInputMethodService, + protected val rime: RimeSession, + protected val theme: Theme, + private val bar: QuickBar, + private val windowManager: BoardWindowManager, + private val compactCandidate: CompactCandidateModule, +) : BoardWindow.NoBarBoardWindow(), InputBroadcastReceiver { + private lateinit var lifecycleCoroutineScope: LifecycleCoroutineScope + private lateinit var candidateLayout: UnrolledCandidateLayout + + protected val separatorDrawable by lazy { + ShapeDrawable(RectShape()).apply { + val spacing = theme.generalStyle.candidateSpacing + val intrinsicSize = max(spacing, context.dp(spacing)).toInt() + intrinsicWidth = intrinsicSize + intrinsicHeight = intrinsicSize + ColorManager.getColor("candidate_separator_color")?.let { paint.color = it } + } + } + + abstract fun onCreateCandidateLayout(): UnrolledCandidateLayout + + final override fun onCreateView(): View { + candidateLayout = + onCreateCandidateLayout().apply { + recyclerView.apply { + // disable item cross-fade animation + itemAnimator = null + } + } + return candidateLayout + } + + abstract val adapter: PagingCandidateViewAdapter + abstract val layoutManager: RecyclerView.LayoutManager + + private var offsetJob: Job? = null + + private val candidatesPager by lazy { + Pager(PagingConfig(pageSize = 48)) { + CandidatesPagingSource( + rime, + num = compactCandidate.adapter.itemCount, + offset = adapter.offset, + ) + } + } + + private var candidatesSubmitJob: Job? = null + + override fun onAttached() { + candidateLayout.updateLayoutParams { + height = KeyboardSwitcher.currentKeyboard.keyboardHeight + } + lifecycleCoroutineScope = candidateLayout.findViewTreeLifecycleOwner()!!.lifecycleScope + bar.unrollButtonStateMachine.push(UnrollButtonStateMachine.TransitionEvent.UnrolledCandidatesAttached) + offsetJob = + lifecycleCoroutineScope.launch { + compactCandidate.unrolledCandidateOffset.collect { + updateCandidatesWithOffset(it) + } + } + candidatesSubmitJob = + lifecycleCoroutineScope.launch { + candidatesPager.flow.collect { + adapter.submitData(it) + } + } + } + + fun bindCandidateUiViewHolder(holder: CandidateViewHolder) { + holder.itemView.run { + setOnClickListener { + rime.launchOnReady { it.selectCandidate(holder.idx) } + } + setOnLongClickListener { view -> + compactCandidate.showCandidateAction(holder.idx, holder.text, view) + true + } + } + } + + private fun updateCandidatesWithOffset(offset: Int) { + val candidates = compactCandidate.adapter.items + if (candidates.isEmpty()) { + windowManager.attachWindow(KeyboardWindow) + } else { + adapter.refreshWithOffset(offset) + lifecycleCoroutineScope.launch(Dispatchers.Main) { + candidateLayout.resetPosition() + } + } + } + + override fun onDetached() { + bar.unrollButtonStateMachine.push( + UnrollButtonStateMachine.TransitionEvent.UnrolledCandidatesDetached, + UnrollButtonStateMachine.BooleanKey.UnrolledCandidatesEmpty to + (compactCandidate.adapter.run { isLastPage && itemCount == adapter.offset }), + ) + offsetJob?.cancel() + candidatesSubmitJob?.cancel() + } +} diff --git a/app/src/main/java/com/osfans/trime/ime/candidates/unrolled/window/FlexboxUnrolledCandidateWindow.kt b/app/src/main/java/com/osfans/trime/ime/candidates/unrolled/window/FlexboxUnrolledCandidateWindow.kt new file mode 100644 index 0000000000..743c09969e --- /dev/null +++ b/app/src/main/java/com/osfans/trime/ime/candidates/unrolled/window/FlexboxUnrolledCandidateWindow.kt @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2015 - 2024 Rime community +// +// SPDX-License-Identifier: GPL-3.0-or-later +package com.osfans.trime.ime.candidates.unrolled.window + +import android.content.Context +import android.view.Gravity +import android.view.ViewGroup +import androidx.transition.Slide +import androidx.transition.Transition +import com.google.android.flexbox.AlignItems +import com.google.android.flexbox.FlexboxLayoutManager +import com.google.android.flexbox.JustifyContent +import com.osfans.trime.daemon.RimeSession +import com.osfans.trime.data.theme.Theme +import com.osfans.trime.ime.bar.QuickBar +import com.osfans.trime.ime.candidates.CandidateViewHolder +import com.osfans.trime.ime.candidates.CompactCandidateModule +import com.osfans.trime.ime.candidates.adapter.PagingCandidateViewAdapter +import com.osfans.trime.ime.candidates.unrolled.UnrolledCandidateLayout +import com.osfans.trime.ime.candidates.unrolled.decoration.FlexboxHorizontalDecoration +import com.osfans.trime.ime.core.TrimeInputMethodService +import com.osfans.trime.ime.window.BoardWindow +import com.osfans.trime.ime.window.BoardWindowManager +import splitties.dimensions.dp +import splitties.views.dsl.core.wrapContent +import splitties.views.setPaddingDp + +class FlexboxUnrolledCandidateWindow( + context: Context, + service: TrimeInputMethodService, + rime: RimeSession, + theme: Theme, + bar: QuickBar, + windowManager: BoardWindowManager, + compactCandidate: CompactCandidateModule, +) : BaseUnrolledCandidateWindow(context, service, rime, theme, bar, windowManager, compactCandidate) { + override fun exitAnimation(nextWindow: BoardWindow): Transition = + Slide().apply { + slideEdge = Gravity.TOP + } + + override val adapter by lazy { + object : PagingCandidateViewAdapter(theme) { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): CandidateViewHolder { + return super.onCreateViewHolder(parent, viewType).apply { + itemView.apply { + minimumWidth = dp(40) + val size = theme.generalStyle.candidatePadding + setPaddingDp(size, 0, size, 0) + layoutParams = + FlexboxLayoutManager.LayoutParams(wrapContent, dp(40)) + .apply { flexGrow = 1f } + } + } + } + + override fun onBindViewHolder( + holder: CandidateViewHolder, + position: Int, + ) { + super.onBindViewHolder(holder, position) + bindCandidateUiViewHolder(holder) + } + } + } + + override val layoutManager by lazy { + FlexboxLayoutManager(context).apply { + justifyContent = JustifyContent.SPACE_AROUND + alignItems = AlignItems.FLEX_START + } + } + + override fun onCreateCandidateLayout(): UnrolledCandidateLayout = + UnrolledCandidateLayout(context, theme).apply { + recyclerView.apply { + adapter = this@FlexboxUnrolledCandidateWindow.adapter + layoutManager = this@FlexboxUnrolledCandidateWindow.layoutManager + addItemDecoration(FlexboxHorizontalDecoration(separatorDrawable)) + } + } +} diff --git a/app/src/main/java/com/osfans/trime/ime/core/InputView.kt b/app/src/main/java/com/osfans/trime/ime/core/InputView.kt index e0c9831b2c..0271e82790 100644 --- a/app/src/main/java/com/osfans/trime/ime/core/InputView.kt +++ b/app/src/main/java/com/osfans/trime/ime/core/InputView.kt @@ -336,11 +336,16 @@ class InputView( if (ctx != null) { broadcaster.onInputContextUpdate(ctx) val candidates = ctx.menu.candidates.map { CandidateItem(it.comment ?: "", it.text) } + val isLastPage = ctx.menu.isLastPage + val previous = ctx.menu.run { pageSize * pageNumber } if (composition.isPopupWindowEnabled) { - val offset = composition.binding.composition.update(ctx) - compactCandidate.adapter.updateCandidates(candidates, offset) + val sticky = composition.binding.composition.update(ctx) + compactCandidate.adapter.updateCandidates(candidates, isLastPage, previous, sticky) } else { - compactCandidate.adapter.updateCandidates(candidates) + compactCandidate.adapter.updateCandidates(candidates, isLastPage, previous) + } + if (candidates.isEmpty()) { + compactCandidate.refreshUnrolled() } } } diff --git a/app/src/main/res/drawable/ic_baseline_expand_less_24.xml b/app/src/main/res/drawable/ic_baseline_expand_less_24.xml new file mode 100644 index 0000000000..f8002be77f --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_expand_less_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values/input_view_ids.xml b/app/src/main/res/values/input_view_ids.xml new file mode 100644 index 0000000000..4c0d81708c --- /dev/null +++ b/app/src/main/res/values/input_view_ids.xml @@ -0,0 +1,4 @@ + + + +