From debca2b5371d70bb17f5476da3800cf473b33ca6 Mon Sep 17 00:00:00 2001 From: WhiredPlanck Date: Tue, 7 May 2024 19:08:21 +0800 Subject: [PATCH] refactor: simplify the logic to build composition view content This time there is no more blinking. --- .../trime/ime/composition/Composition.kt | 458 +++++++----------- .../trime/ime/core/TrimeInputMethodService.kt | 2 +- 2 files changed, 188 insertions(+), 272 deletions(-) diff --git a/app/src/main/java/com/osfans/trime/ime/composition/Composition.kt b/app/src/main/java/com/osfans/trime/ime/composition/Composition.kt index 629cd41aa4..f94eaeac71 100644 --- a/app/src/main/java/com/osfans/trime/ime/composition/Composition.kt +++ b/app/src/main/java/com/osfans/trime/ime/composition/Composition.kt @@ -10,6 +10,7 @@ import android.content.res.Configuration import android.graphics.Typeface import android.text.Layout import android.text.SpannableStringBuilder +import android.text.Spanned import android.text.TextPaint import android.text.method.LinkMovementMethod import android.text.style.AbsoluteSizeSpan @@ -17,12 +18,17 @@ import android.text.style.AlignmentSpan import android.text.style.BackgroundColorSpan import android.text.style.ClickableSpan import android.text.style.ForegroundColorSpan +import android.text.style.ScaleXSpan import android.text.style.UnderlineSpan import android.util.AttributeSet import android.view.MotionEvent import android.view.View import android.widget.TextView +import androidx.core.text.buildSpannedString +import androidx.core.text.inSpans +import com.osfans.trime.core.CandidateListItem import com.osfans.trime.core.Rime +import com.osfans.trime.core.RimeComposition import com.osfans.trime.data.theme.ColorManager import com.osfans.trime.data.theme.EventManager import com.osfans.trime.data.theme.FontManager @@ -35,7 +41,6 @@ import com.osfans.trime.ime.text.Candidate import com.osfans.trime.ime.text.TextInputManager import com.osfans.trime.util.sp import splitties.dimensions.dp -import timber.log.Timber /** 編碼區,顯示已輸入的按鍵編碼,可使用方向鍵或觸屏移動光標位置 */ @SuppressLint("AppCompatCustomView") @@ -44,31 +49,52 @@ class Composition(context: Context, attrs: AttributeSet?) : TextView(context, at private val textInputManager = TextInputManager.instanceOrNull() private val keyTextSize = theme.generalStyle.keyTextSize - private val keyTextColor = ColorManager.getColor("key_text_color")!! + private val labelTextSize = theme.generalStyle.labelTextSize + private val candidateTextSize = theme.generalStyle.candidateTextSize + private val commentTextSize = theme.generalStyle.commentTextSize + private val textColor = ColorManager.getColor("text_color") + private val backColor = ColorManager.getColor("back_color") + private val keyTextColor = ColorManager.getColor("key_text_color") + private val keyBackColor = ColorManager.getColor("key_back_color") + private val labelColor = ColorManager.getColor("label_color") + private val candidateTextColor = ColorManager.getColor("candidate_text_color") + private val commentTextColor = ColorManager.getColor("comment_text_color") + private val highlightTextColor = ColorManager.getColor("hilited_text_color") + private val highlightBackColor = ColorManager.getColor("hilited_back_color") + private val highlightLabelColor = ColorManager.getColor("hilited_label_color") + private val highlightCommentTextColor = ColorManager.getColor("hilited_comment_text_color") + private val highlightCandidateTextColor = ColorManager.getColor("hilited_candidate_text_color") + private val highlightCandidateBackColor = ColorManager.getColor("hilited_candidate_back_color") private val candidateUseCursor = theme.generalStyle.candidateUseCursor private val movable = Movable.fromString(theme.generalStyle.layout.movable) private val showComment = !Rime.getOption("_hide_comment") - private val maxEntries = + private val allPhrases = theme.generalStyle.layout.allPhrases + private val maxCount = theme.generalStyle.layout.maxEntries.takeIf { it > 0 } ?: Candidate.MAX_CANDIDATE_COUNT + private val maxLength = theme.generalStyle.layout.maxLength + private val minCheckLength = theme.generalStyle.layout.minLength // 候选词长度大于设定,才会显示到悬浮窗中 + private val minCheckCount = theme.generalStyle.layout.minCheck // 检查至少多少个候选词。当首选词长度不足时,继续检查后方候选词 + + private val windowComponents = theme.generalStyle.window - private val windowComponents = theme.generalStyle.window ?: listOf() - private val liquidWindowComponents = theme.generalStyle.liquidKeyboardWindow ?: listOf() + private val highlightIndex get() = if (candidateUseCursor) Rime.candHighlightIndex else -1 + private val preeditRange = intArrayOf(0, 0) + private val movableRange = intArrayOf(0, 0) - private var highlightIndex = 0 - private val compositionPos = IntArray(2) - private val movePos = IntArray(2) + private val keyTextSizeSpan by lazy { AbsoluteSizeSpan(sp(keyTextSize)) } + private val labelTextSizeSpan by lazy { AbsoluteSizeSpan(sp(labelTextSize)) } + private val candidateTextSizeSpan by lazy { AbsoluteSizeSpan(sp(candidateTextSize)) } + private val commentTextSizeSpan by lazy { AbsoluteSizeSpan(sp(commentTextSize)) } + private val keyTextColorSpan by lazy { keyTextColor?.let { ForegroundColorSpan(it) } } + private val highlightTextColorSpan by lazy { highlightTextColor?.let { ForegroundColorSpan(it) } } + private val highlightBackColorSpan by lazy { highlightBackColor?.let { BackgroundColorSpan(it) } } - private var ss: SpannableStringBuilder? = null private var firstMove = true private var mDx = 0f private var mDy = 0f private var mCurrentX = 0 private var mCurrentY = 0 - private var candidateNum = 0 - private val allPhrases = theme.generalStyle.layout.allPhrases - - private var isToolbarMode = false private val stickyLines: Int get() = @@ -100,17 +126,17 @@ class Composition(context: Context, attrs: AttributeSet?) : TextView(context, at private inner class CompositionSpan : UnderlineSpan() { override fun updateDrawState(ds: TextPaint) { ds.typeface = FontManager.getTypeface("text_font") - ds.color = ColorManager.getColor("text_color")!! - ds.bgColor = ColorManager.getColor("back_color")!! + textColor?.let { ds.color = it } + backColor?.let { ds.bgColor = it } } } private inner class CandidateSpan( private val index: Int, private val typeface: Typeface?, - private val highlightTextColor: Int, - private val highlightBackColor: Int, - private val textColor: Int, + private val highlightTextColor: Int?, + private val highlightBackColor: Int?, + private val textColor: Int?, ) : ClickableSpan() { override fun onClick(tv: View) { textInputManager?.onCandidatePressed(index) @@ -120,10 +146,10 @@ class Composition(context: Context, attrs: AttributeSet?) : TextView(context, at ds.isUnderlineText = false ds.typeface = typeface if (index == highlightIndex) { - ds.color = highlightTextColor - ds.bgColor = highlightBackColor + highlightTextColor?.let { ds.color = it } + highlightBackColor?.let { ds.bgColor = it } } else { - ds.color = textColor + textColor?.let { ds.color = it } } } } @@ -136,20 +162,19 @@ class Composition(context: Context, attrs: AttributeSet?) : TextView(context, at override fun updateDrawState(ds: TextPaint) { ds.isUnderlineText = false - ds.color = keyTextColor - ColorManager.getColor("key_back_color")?.let { - ds.bgColor = it - } + keyTextColor?.let { ds.color = it } + keyBackColor?.let { ds.bgColor = it } } } - /** - * @param letterSpacing 字符間距 - */ - class LetterSpacingSpan(private val letterSpacing: Float) : UnderlineSpan() { - override fun updateDrawState(ds: TextPaint) { - ds.letterSpacing = letterSpacing - } + private fun alignmentSpan(alignment: String): AlignmentSpan { + val align = + when (alignment) { + "right", "opposite" -> Layout.Alignment.ALIGN_OPPOSITE + "center" -> Layout.Alignment.ALIGN_CENTER + else -> Layout.Alignment.ALIGN_NORMAL // "left", "normal" or else + } + return AlignmentSpan.Standard(align) } init { @@ -176,15 +201,14 @@ class Composition(context: Context, attrs: AttributeSet?) : TextView(context, at @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent): Boolean { val action = event.action - if (isToolbarMode) return super.onTouchEvent(event) if (action == MotionEvent.ACTION_UP) { var n = getOffsetForPosition(event.x, event.y) - if (n in compositionPos[0]..compositionPos[1]) { + if (n in preeditRange[0]..preeditRange[1]) { val s = text .toString() - .substring(n, compositionPos[1]) + .substring(n, preeditRange[1]) .replace(" ", "") .replace("‸", "") n = Rime.getRimeRawInput()!!.length - s.length // 從右側定位 @@ -196,7 +220,7 @@ class Composition(context: Context, attrs: AttributeSet?) : TextView(context, at (action == MotionEvent.ACTION_MOVE || action == MotionEvent.ACTION_DOWN) ) { val n = getOffsetForPosition(event.x, event.y) - if (n in movePos[0]..movePos[1]) { + if (n in movableRange[0]..movableRange[1]) { if (action == MotionEvent.ACTION_DOWN) { if (firstMove || movable == Movable.ONCE) { firstMove = false @@ -216,248 +240,146 @@ class Composition(context: Context, attrs: AttributeSet?) : TextView(context, at return super.onTouchEvent(event) } - private fun getAlign(m: CompositionComponent): Any { - var i = Layout.Alignment.ALIGN_NORMAL - if (m.align.isNotBlank()) { - when (m.align) { - "left", "normal" -> i = Layout.Alignment.ALIGN_NORMAL - "right", "opposite" -> i = Layout.Alignment.ALIGN_OPPOSITE - "center" -> i = Layout.Alignment.ALIGN_CENTER - } - } - return AlignmentSpan.Standard(i) - } - - private fun appendComposition(m: CompositionComponent) { - val r = Rime.composition!! - val s = r.preedit - var start: Int - var end: Int - var sep = m.start - if (sep.isNotEmpty()) { - start = ss!!.length - ss!!.append(sep) - end = ss!!.length - ss!!.setSpan(getAlign(m), start, end, 0) - } - start = ss!!.length - ss!!.append(s) - end = ss!!.length - ss!!.setSpan(getAlign(m), start, end, 0) - compositionPos[0] = start - compositionPos[1] = end - ss!!.setSpan(CompositionSpan(), start, end, 0) - val textSize = sp(theme.generalStyle.textSize) - ss!!.setSpan(AbsoluteSizeSpan(textSize), start, end, 0) - m.letterSpacing.toFloat().takeIf { it > 0 } - ?.let { ss?.setSpan(LetterSpacingSpan(it), start, end, 0) } - start = compositionPos[0] + r.selStart - end = compositionPos[0] + r.selEnd - ss!!.setSpan(ForegroundColorSpan(ColorManager.getColor("hilited_text_color")!!), start, end, 0) - ss!!.setSpan(BackgroundColorSpan(ColorManager.getColor("hilited_back_color")!!), start, end, 0) - sep = m.end - if (sep.isNotEmpty()) ss!!.append(sep) - } - - /** - * 计算悬浮窗显示候选词后,候选栏从第几个候选词开始展示 注意当 all_phrases==true 时,悬浮窗显示的候选词数量和候选栏从第几个开始,是不一致的 - * - * @param minLength 候选词长度大于设定,才会显示到悬浮窗中 - * @param minCheck 检查至少多少个候选词。当首选词长度不足时,继续检查后方候选词 - * @return j - */ - private fun calcStartNum( - minLength: Int, - minCheck: Int, - ): Int { - Timber.d("setWindow calcStartNum() getCandidates") - val candidates = Rime.candidatesOrStatusSwitches - if (candidates.isEmpty()) return 0 - Timber.d("setWindow calcStartNum() getCandidates finish, size=%s", candidates.size) - var j = if (minCheck > maxEntries) maxEntries - 1 else minCheck - 1 - if (j >= candidates.size) j = candidates.size - 1 - while (j >= 0) { - val cand = candidates[j].text - if (cand.length >= minLength) break - j-- - } - if (j < 0) j = 0 - while (j < maxEntries && j < candidates.size) { - val cand = candidates[j].text - if (cand.length < minLength) { - return j - } - j++ + private fun SpannableStringBuilder.buildSpannedComposition( + m: CompositionComponent, + composition: RimeComposition, + ) { + val alignmentSpan = alignmentSpan(m.align) + val preeditSpans = + listOf( + alignmentSpan, + CompositionSpan(), + AbsoluteSizeSpan(sp(theme.generalStyle.textSize)), + m.letterSpacing.toFloat().takeIf { it > 0 }?.let { ScaleXSpan(it) }, + ).mapNotNull { it }.toTypedArray() + val colorSpans = listOf(highlightTextColorSpan, highlightBackColorSpan).mapNotNull { it } + inSpans(alignmentSpan) { append(m.start) } + preeditRange[0] = length + inSpans(*preeditSpans) { append(composition.preedit) } + preeditRange[1] = length + val selStart = preeditRange[0] + composition.selStart + val selEnd = preeditRange[0] + composition.selEnd + for (span in colorSpans) { + setSpan(span, selStart, selEnd, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) } - return j + inSpans(alignmentSpan) { append(m.end) } } /** 生成悬浮窗内的文本 */ - private fun appendCandidates( + private fun SpannableStringBuilder.buildSpannedCandidates( m: CompositionComponent, - length: Int, - endNum: Int, + candidates: Array, + selectLabels: Array, + offset: Int, ) { - Timber.d("appendCandidates(): length = %s", length) - var start: Int - var end: Int - val candidates = Rime.candidatesOrStatusSwitches if (candidates.isEmpty()) return - val prefix = m.start - highlightIndex = if (candidateUseCursor) Rime.candHighlightIndex else -1 - val labelFormat = m.label - val candidateFormat = m.candidate - val commentFormat = m.comment - val line = m.sep - var lineLength = 0 - val labels = Rime.selectLabels - var i = -1 - candidateNum = 0 - val maxLength = theme.generalStyle.layout.maxLength - val hilitedCandidateBackColor = ColorManager.getColor("hilited_candidate_back_color")!! - for (o in candidates) { - var cand = o.text - i++ - if (candidateNum >= maxEntries) break - if (!allPhrases && candidateNum >= endNum) break - if (allPhrases && cand.length < length) { + var currentLineLength = 0 + val alignmentSpan = alignmentSpan(m.align) + for ((i, candidate) in candidates.withIndex()) { + val text = String.format(m.candidate, candidate.text) + val comment = String.format(m.comment, candidate.comment) + val label = String.format(m.label, selectLabels[i]) + if (i >= maxCount) break + if (!allPhrases && i >= offset) break + if (allPhrases && text.length < minCheckLength) { continue } - cand = String.format(candidateFormat, cand) + val lineSep = - if (candidateNum == 0) { - prefix - } else if (stickyLines > 0 && stickyLines >= i || maxLength > 0 && lineLength + cand.length > maxLength) { - "\n".also { lineLength = 0 } + if (i == 0) { + m.start + } else if (i <= stickyLines || currentLineLength + text.length > maxLength) { + "\n".also { currentLineLength = 0 } } else { - line + m.sep } - if (lineSep.isNotEmpty()) { - start = ss!!.length - ss!!.append(lineSep) - end = ss!!.length - ss!!.setSpan(getAlign(m), start, end, 0) - } - if (labelFormat.isNotEmpty() && labels.isNotEmpty()) { - val label = String.format(labelFormat, labels[i]) - start = ss!!.length - ss!!.append(label) - end = ss!!.length - ss!!.setSpan( - CandidateSpan( - i, - FontManager.getTypeface("label_font"), - ColorManager.getColor("hilited_label_color")!!, - hilitedCandidateBackColor, - ColorManager.getColor("label_color")!!, - ), - start, - end, - 0, + inSpans(alignmentSpan) { append(lineSep) } + + val labelSpan = + CandidateSpan( + i, + FontManager.getTypeface("label_font"), + highlightLabelColor, + highlightCandidateBackColor, + labelColor, ) - val labelTextSize = sp(theme.generalStyle.labelTextSize) - ss!!.setSpan(AbsoluteSizeSpan(labelTextSize), start, end, 0) - } - start = ss!!.length - ss!!.append(cand) - end = ss!!.length - lineLength += cand.length - ss!!.setSpan(getAlign(m), start, end, 0) - ss!!.setSpan( + inSpans(alignmentSpan, labelSpan, labelTextSizeSpan) { append(label) } + + val candidateSpan = CandidateSpan( i, FontManager.getTypeface("candidate_font"), - ColorManager.getColor("hilited_candidate_text_color")!!, - hilitedCandidateBackColor, - ColorManager.getColor("candidate_text_color")!!, - ), - start, - end, - 0, - ) - val candidateTextSize = sp(theme.generalStyle.candidateTextSize) - ss!!.setSpan(AbsoluteSizeSpan(candidateTextSize), start, end, 0) - if (showComment && commentFormat.isNotEmpty() && o.comment.isNotEmpty()) { - val comment = String.format(commentFormat, o.comment) - start = ss!!.length - ss!!.append(comment) - end = ss!!.length - ss!!.setSpan(getAlign(m), start, end, 0) - ss!!.setSpan( + highlightCandidateTextColor, + highlightCandidateBackColor, + candidateTextColor, + ) + inSpans(alignmentSpan, candidateSpan, candidateTextSizeSpan) { append(text) } + currentLineLength += text.length + + if (showComment) { + val commentSpan = CandidateSpan( i, FontManager.getTypeface("comment_font"), - ColorManager.getColor("hilited_comment_text_color")!!, - hilitedCandidateBackColor, - ColorManager.getColor("comment_text_color")!!, - ), - start, - end, - 0, - ) - val commentTextSize = sp(theme.generalStyle.commentTextSize) - ss!!.setSpan(AbsoluteSizeSpan(commentTextSize), start, end, 0) - lineLength += comment.length + highlightCommentTextColor, + highlightCandidateBackColor, + commentTextColor, + ) + inSpans(alignmentSpan, commentSpan, commentTextSizeSpan) { append(comment) } + currentLineLength += comment.length } - candidateNum++ } - val suffix = m.end - if (suffix.isNotEmpty()) ss!!.append(suffix) + inSpans(alignmentSpan) { append(m.end) } } - private fun appendButton(m: CompositionComponent) { - if (m.whenStr.isNotBlank()) { - val `when` = m.whenStr - if (`when`.contentEquals("paging") && !Rime.hasLeft()) return - if (`when`.contentEquals("has_menu") && !Rime.hasMenu()) return + private fun SpannableStringBuilder.buildSpannedButton(m: CompositionComponent) { + when (m.whenStr) { + "paging" -> if (!Rime.hasLeft()) return + "has_menu" -> if (!Rime.hasMenu()) return } - val e = EventManager.getEvent(m.click) - val label = - if (m.label.isNotBlank()) { - m.label - } else { - e.getLabel(KeyboardSwitcher.currentKeyboard) - } - var start: Int - var end: Int - val prefix = m.start - if (prefix.isNotEmpty()) { - start = ss!!.length - ss!!.append(prefix) - end = ss!!.length - ss!!.setSpan(getAlign(m), start, end, 0) + val alignmentSpan = alignmentSpan(m.align) + val event = EventManager.getEvent(m.click) + val label = m.label.ifBlank { event.getLabel(KeyboardSwitcher.currentKeyboard) } + val buttonSpans = listOf(alignmentSpan, EventSpan(event), keyTextSizeSpan).toTypedArray() + inSpans(alignmentSpan) { append(m.start) } + inSpans(*buttonSpans) { append(label) } + inSpans(alignmentSpan) { append(m.end) } + } + + private fun SpannableStringBuilder.buildSpannedMove(m: CompositionComponent) { + val alignmentSpan = alignmentSpan(m.align) + val moveSpans = + listOf(alignmentSpan, keyTextSizeSpan, keyTextColorSpan) + .mapNotNull { it }.toTypedArray() + + inSpans(alignmentSpan) { append(m.start) } + inSpans(*moveSpans) { + movableRange[0] = length + append(m.move) + movableRange[1] = length } - start = ss!!.length - ss!!.append(label) - end = ss!!.length - ss!!.setSpan(getAlign(m), start, end, 0) - ss!!.setSpan(EventSpan(e), start, end, 0) - ss!!.setSpan(AbsoluteSizeSpan(sp(keyTextSize)), start, end, 0) - val suffix = m.end - if (suffix.isNotEmpty()) ss!!.append(suffix) + inSpans(alignmentSpan) { append(m.end) } } - private fun appendMove(m: CompositionComponent) { - val s = m.move - var start: Int - var end: Int - val prefix = m.start - if (prefix.isNotEmpty()) { - start = ss!!.length - ss!!.append(prefix) - end = ss!!.length - ss!!.setSpan(getAlign(m), start, end, 0) + /** + * 计算悬浮窗显示候选词后,候选栏从第几个候选词开始展示 注意当 all_phrases==true 时,悬浮窗显示的候选词数量和候选栏从第几个开始,是不一致的 + */ + private fun calculateOffset(candidates: Array): Int { + if (candidates.isEmpty()) return 0 + var j = minOf(minCheckCount, candidates.size, maxCount) - 1 + while (j > 0) { + val text = candidates[j].text + if (text.length >= minCheckLength) break + j-- } - start = ss!!.length - ss!!.append(s) - end = ss!!.length - ss!!.setSpan(getAlign(m), start, end, 0) - movePos[0] = start - movePos[1] = end - ss!!.setSpan(AbsoluteSizeSpan(sp(keyTextSize)), start, end, 0) - ss!!.setSpan(ForegroundColorSpan(keyTextColor), start, end, 0) - val suffix = m.end - if (suffix.isNotEmpty()) ss!!.append(suffix) + while (j < minOf(maxCount, candidates.size)) { + val text = candidates[j].text + if (text.length < minCheckLength) { + return j + } + j++ + } + return j } /** @@ -465,31 +387,25 @@ class Composition(context: Context, attrs: AttributeSet?) : TextView(context, at * * @return 悬浮窗显示的候选词数量 */ - fun setWindowContent(): Int { + fun update(): Int { if (visibility != VISIBLE) return 0 Rime.composition?.preedit?.takeIf { it.isNotBlank() } ?: return 0 - isSingleLine = true // 設置單行 - ss = SpannableStringBuilder() - var startNum = 0 - val minLength = theme.generalStyle.layout.minLength // 候选词长度大于设定,才会显示到悬浮窗中 - val minCheck = theme.generalStyle.layout.minCheck // 检查至少多少个候选词。当首选词长度不足时,继续检查后方候选词 - for (m in windowComponents) { - if (m.composition.isNotBlank()) { - appendComposition(m) - } else if (m.candidate.isNotBlank()) { - startNum = calcStartNum(minLength, minCheck) - Timber.d("start_num = %s, min_length = %s, min_check = %s", startNum, minLength, minCheck) - appendCandidates(m, minLength, startNum) - } else if (m.click.isNotBlank()) { - appendButton(m) - } else if (m.move.isNotBlank()) { - appendMove(m) + val candidates = Rime.candidatesWithoutSwitch + val startNum = calculateOffset(candidates.map { it.text }) + val content = + buildSpannedString { + for (component in windowComponents) { + when { + component.move.isNotBlank() -> buildSpannedMove(component) + component.composition.isNotBlank() -> buildSpannedComposition(component, Rime.composition!!) + component.click.isNotBlank() -> buildSpannedButton(component) + component.candidate.isNotBlank() -> buildSpannedCandidates(component, candidates, Rime.selectLabels, startNum) + } + } } - } - if (candidateNum > 0 || ss.toString().contains("\n")) isSingleLine = false // 設置單行 - text = ss + isSingleLine = startNum == 0 + text = content movementMethod = LinkMovementMethod.getInstance() - isToolbarMode = false return startNum } } diff --git a/app/src/main/java/com/osfans/trime/ime/core/TrimeInputMethodService.kt b/app/src/main/java/com/osfans/trime/ime/core/TrimeInputMethodService.kt index 229d503161..a14ca68458 100644 --- a/app/src/main/java/com/osfans/trime/ime/core/TrimeInputMethodService.kt +++ b/app/src/main/java/com/osfans/trime/ime/core/TrimeInputMethodService.kt @@ -1096,7 +1096,7 @@ open class TrimeInputMethodService : LifecycleInputMethodService() { if (mCompositionPopupWindow?.isPopupWindowEnabled == true) { val composition = mCompositionPopupWindow!!.composition composition.compositionView.visibility = View.VISIBLE - startNum = composition.compositionView.setWindowContent() + startNum = composition.compositionView.update() mCandidate?.setText(startNum) // if isCursorUpdated, showCompositionView will be called in onUpdateCursorAnchorInfo // otherwise we need to call it here