From 573aaeb21357411df4806e4e59347c78cdfb085f Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Thu, 9 May 2024 16:22:53 +0700 Subject: [PATCH] Fix cursor goes to the front when typing fast --- .../views/PhoneNumberViewHolderFactory.kt | 13 ++--- .../android/fhir/datacapture/views/Util.kt | 58 +++++++++++++++++++ .../EditTextDecimalViewHolderFactory.kt | 12 ++-- .../EditTextIntegerViewHolderFactory.kt | 8 +-- .../EditTextStringViewHolderDelegate.kt | 16 ++--- .../factories/EditTextViewHolderFactory.kt | 21 +++---- 6 files changed, 87 insertions(+), 41 deletions(-) create mode 100644 android/datacapture/src/main/java/com/google/android/fhir/datacapture/views/Util.kt diff --git a/android/datacapture/src/main/java/com/google/android/fhir/datacapture/contrib/views/PhoneNumberViewHolderFactory.kt b/android/datacapture/src/main/java/com/google/android/fhir/datacapture/contrib/views/PhoneNumberViewHolderFactory.kt index 1fafb176de..c344bea285 100644 --- a/android/datacapture/src/main/java/com/google/android/fhir/datacapture/contrib/views/PhoneNumberViewHolderFactory.kt +++ b/android/datacapture/src/main/java/com/google/android/fhir/datacapture/contrib/views/PhoneNumberViewHolderFactory.kt @@ -16,7 +16,6 @@ package com.google.android.fhir.datacapture.contrib.views -import android.text.Editable import android.text.InputType import com.google.android.fhir.datacapture.R import com.google.android.fhir.datacapture.views.QuestionnaireViewItem @@ -33,16 +32,12 @@ object PhoneNumberViewHolderFactory : override fun getQuestionnaireItemViewHolderDelegate(): QuestionnaireItemViewHolderDelegate = object : QuestionnaireItemEditTextViewHolderDelegate(InputType.TYPE_CLASS_PHONE) { - override suspend fun handleInput( - editable: Editable, + override suspend fun handleInputText( + input: String?, questionnaireViewItem: QuestionnaireViewItem, ) { - val input = getValue(editable.toString()) - if (input != null) { - questionnaireViewItem.setAnswer(input) - } else { - questionnaireViewItem.clearAnswer() - } + input?.let { getValue(input) }?.let { questionnaireViewItem.setAnswer(it) } + ?: questionnaireViewItem.clearAnswer() } private fun getValue( diff --git a/android/datacapture/src/main/java/com/google/android/fhir/datacapture/views/Util.kt b/android/datacapture/src/main/java/com/google/android/fhir/datacapture/views/Util.kt new file mode 100644 index 0000000000..e1b6c1e3b4 --- /dev/null +++ b/android/datacapture/src/main/java/com/google/android/fhir/datacapture/views/Util.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.fhir.datacapture.views + +import android.os.CountDownTimer +import android.text.Editable +import android.text.TextWatcher +import android.widget.TextView + +/** [delay] in milli seconds */ +fun TextView.afterTextChangedDelayed(delay: Long, afterTextChanged: (String?) -> Unit) { + this.addTextChangedListener( + object : TextWatcher { + var timer: CountDownTimer? = null + var firstCharacter: Boolean = true + + override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { + firstCharacter = p0.isNullOrEmpty() + } + + override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} + + override fun afterTextChanged(editable: Editable?) { + timer?.cancel() + // If editable has become empty or this is the first character then invoke afterTextChanged + // instantly else start a timer for user to finish typing + if (editable?.toString().isNullOrEmpty() || firstCharacter) { + afterTextChanged(editable?.toString()) + } else { + // countDownInterval is simply kept greater than delay as we don't need onTick + timer = + object : CountDownTimer(delay, delay * 2) { + override fun onTick(millisUntilFinished: Long) {} + + override fun onFinish() { + afterTextChanged(editable?.toString()) + } + } + .start() + } + } + }, + ) +} diff --git a/android/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/EditTextDecimalViewHolderFactory.kt b/android/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/EditTextDecimalViewHolderFactory.kt index c2418f0e7c..efedefb30f 100644 --- a/android/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/EditTextDecimalViewHolderFactory.kt +++ b/android/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/EditTextDecimalViewHolderFactory.kt @@ -30,17 +30,21 @@ internal object EditTextDecimalViewHolderFactory : override fun getQuestionnaireItemViewHolderDelegate() = object : QuestionnaireItemEditTextViewHolderDelegate(DECIMAL_INPUT_TYPE) { - override suspend fun handleInput( - editable: Editable, + override suspend fun handleInputText( + input: String?, questionnaireViewItem: QuestionnaireViewItem, ) { - editable.toString().toDoubleOrNull()?.let { + if (input.isNullOrEmpty()) { + questionnaireViewItem.clearAnswer() + return + } + input.toDoubleOrNull()?.let { questionnaireViewItem.setAnswer( QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent() .setValue(DecimalType(it.toString())), ) } - ?: questionnaireViewItem.setDraftAnswer(editable.toString()) + ?: questionnaireViewItem.setDraftAnswer(input) } override fun updateUI( diff --git a/android/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/EditTextIntegerViewHolderFactory.kt b/android/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/EditTextIntegerViewHolderFactory.kt index d7e7729201..8d60dd6b9f 100644 --- a/android/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/EditTextIntegerViewHolderFactory.kt +++ b/android/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/EditTextIntegerViewHolderFactory.kt @@ -19,7 +19,6 @@ package com.google.android.fhir.datacapture.views.factories import android.icu.number.NumberFormatter import android.icu.text.DecimalFormat import android.os.Build -import android.text.Editable import android.text.InputType import androidx.annotation.RequiresApi import com.google.android.fhir.datacapture.R @@ -37,12 +36,11 @@ internal object EditTextIntegerViewHolderFactory : QuestionnaireItemEditTextViewHolderDelegate( InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_SIGNED, ) { - override suspend fun handleInput( - editable: Editable, + override suspend fun handleInputText( + input: String?, questionnaireViewItem: QuestionnaireViewItem, ) { - val input = editable.toString() - if (input.isEmpty()) { + if (input.isNullOrEmpty()) { questionnaireViewItem.clearAnswer() return } diff --git a/android/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/EditTextStringViewHolderDelegate.kt b/android/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/EditTextStringViewHolderDelegate.kt index e95cbf9c04..e994419674 100644 --- a/android/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/EditTextStringViewHolderDelegate.kt +++ b/android/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/EditTextStringViewHolderDelegate.kt @@ -16,7 +16,6 @@ package com.google.android.fhir.datacapture.views.factories -import android.text.Editable import android.text.InputType import com.google.android.fhir.datacapture.views.QuestionnaireViewItem import com.google.android.material.textfield.TextInputEditText @@ -34,16 +33,12 @@ internal class EditTextStringViewHolderDelegate : QuestionnaireItemEditTextViewHolderDelegate( InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES, ) { - override suspend fun handleInput( - editable: Editable, + override suspend fun handleInputText( + input: String?, questionnaireViewItem: QuestionnaireViewItem, ) { - val input = getValue(editable.toString()) - if (input != null) { - questionnaireViewItem.setAnswer(input) - } else { - questionnaireViewItem.clearAnswer() - } + input?.let { getValue(input) }?.let { questionnaireViewItem.setAnswer(it) } + ?: questionnaireViewItem.clearAnswer() } private fun getValue( @@ -65,7 +60,8 @@ internal class EditTextStringViewHolderDelegate : ) { val text = questionnaireViewItem.answers.singleOrNull()?.valueStringType?.value ?: "" if ((text != textInputEditText.text.toString())) { - textInputEditText.setText(text) + textInputEditText.text?.clear() + textInputEditText.append(text) } } } diff --git a/android/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/EditTextViewHolderFactory.kt b/android/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/EditTextViewHolderFactory.kt index 84bf9c93cc..7920c292c0 100644 --- a/android/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/EditTextViewHolderFactory.kt +++ b/android/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/EditTextViewHolderFactory.kt @@ -17,8 +17,6 @@ package com.google.android.fhir.datacapture.views.factories import android.content.Context -import android.text.Editable -import android.text.TextWatcher import android.view.View import android.view.View.FOCUS_DOWN import android.view.View.GONE @@ -28,7 +26,6 @@ import android.view.inputmethod.InputMethodManager import android.widget.TextView import androidx.annotation.LayoutRes import androidx.appcompat.app.AppCompatActivity -import androidx.core.widget.doAfterTextChanged import androidx.lifecycle.lifecycleScope import com.google.android.fhir.datacapture.R import com.google.android.fhir.datacapture.extensions.getRequiredOrOptionalText @@ -39,6 +36,7 @@ import com.google.android.fhir.datacapture.extensions.unit import com.google.android.fhir.datacapture.validation.ValidationResult import com.google.android.fhir.datacapture.views.HeaderView import com.google.android.fhir.datacapture.views.QuestionnaireViewItem +import com.google.android.fhir.datacapture.views.afterTextChangedDelayed import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import kotlinx.coroutines.launch @@ -58,7 +56,6 @@ abstract class QuestionnaireItemEditTextViewHolderDelegate(private val rawInputT protected lateinit var textInputLayout: TextInputLayout private lateinit var textInputEditText: TextInputEditText private var unitTextView: TextView? = null - private var textWatcher: TextWatcher? = null override fun init(itemView: View) { context = itemView.context.tryUnwrapContext()!! @@ -74,7 +71,7 @@ abstract class QuestionnaireItemEditTextViewHolderDelegate(private val rawInputT // https://stackoverflow.com/questions/13614101/fatal-crash-focus-search-returned-a-view-that-wasnt-able-to-take-focus/47991577 textInputEditText.setOnEditorActionListener { view, actionId, _ -> if (actionId != EditorInfo.IME_ACTION_NEXT) { - false + return@setOnEditorActionListener false } view.focusSearch(FOCUS_DOWN)?.requestFocus(FOCUS_DOWN) ?: false } @@ -87,10 +84,14 @@ abstract class QuestionnaireItemEditTextViewHolderDelegate(private val rawInputT context.lifecycleScope.launch { // Update answer even if the text box loses focus without any change. This will mark the // questionnaire response item as being modified in the view model and trigger validation. - handleInput(textInputEditText.editableText, questionnaireViewItem) + handleInputText(textInputEditText.editableText.toString(), questionnaireViewItem) } } } + textInputEditText.afterTextChangedDelayed(500) { text: String? -> + // with a delay check that user has stopped typing + context.lifecycleScope.launch { handleInputText(text, questionnaireViewItem) } + } } override fun bind(questionnaireViewItem: QuestionnaireViewItem) { @@ -101,18 +102,12 @@ abstract class QuestionnaireItemEditTextViewHolderDelegate(private val rawInputT } displayValidationResult(questionnaireViewItem.validationResult) - textInputEditText.removeTextChangedListener(textWatcher) updateUI(questionnaireViewItem, textInputEditText, textInputLayout) unitTextView?.apply { text = questionnaireViewItem.questionnaireItem.unit?.code visibility = if (text.isNullOrEmpty()) GONE else VISIBLE } - - textWatcher = - textInputEditText.doAfterTextChanged { editable: Editable? -> - context.lifecycleScope.launch { handleInput(editable!!, questionnaireViewItem) } - } } private fun displayValidationResult(validationResult: ValidationResult) { @@ -126,7 +121,7 @@ abstract class QuestionnaireItemEditTextViewHolderDelegate(private val rawInputT } /** Handles user input from the `editable` and updates the questionnaire. */ - abstract suspend fun handleInput(editable: Editable, questionnaireViewItem: QuestionnaireViewItem) + abstract suspend fun handleInputText(input: String?, questionnaireViewItem: QuestionnaireViewItem) /** Handles the UI update. */ abstract fun updateUI(