Skip to content

Commit

Permalink
Fix cursor goes to the front when typing fast
Browse files Browse the repository at this point in the history
  • Loading branch information
FikriMilano committed May 9, 2024
1 parent 7201bb1 commit 573aaeb
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
},
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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()!!
Expand All @@ -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
}
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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(
Expand Down

0 comments on commit 573aaeb

Please sign in to comment.