From 079ad8150429ca400e99d0f53ece923af0ec499b Mon Sep 17 00:00:00 2001 From: fikrimilano Date: Tue, 28 Jan 2025 14:57:49 +0700 Subject: [PATCH] Refactor implementation - Simpler logic - Use `questionnaireViewItem.answers` state to check if the AutoComplete is editable or not - Remove keyListener to make the AutoComplete not editable - Hide keyboard after typing an option then clicking the shown option - Add cool ripple effect on clicking the clear input icon --- .../factories/DropDownViewHolderFactory.kt | 114 ++++++++---------- .../src/main/res/drawable/ic_clear.xml | 7 +- .../src/main/res/layout/drop_down_view.xml | 35 +++--- datacapture/src/main/res/values/dimens.xml | 4 +- 4 files changed, 75 insertions(+), 85 deletions(-) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DropDownViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DropDownViewHolderFactory.kt index ec515c44d6..b92fcba884 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DropDownViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DropDownViewHolderFactory.kt @@ -18,15 +18,18 @@ package com.google.android.fhir.datacapture.views.factories import android.content.Context import android.graphics.drawable.Drawable +import android.text.InputType +import android.text.Spanned +import android.text.method.TextKeyListener import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager import android.widget.AdapterView import android.widget.ArrayAdapter -import android.widget.ImageView +import android.widget.FrameLayout import android.widget.TextView 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.displayString @@ -35,12 +38,14 @@ import com.google.android.fhir.datacapture.extensions.getValidationErrorMessage import com.google.android.fhir.datacapture.extensions.identifierString import com.google.android.fhir.datacapture.extensions.itemAnswerOptionImage import com.google.android.fhir.datacapture.extensions.localizedFlyoverSpanned +import com.google.android.fhir.datacapture.extensions.toSpanned import com.google.android.fhir.datacapture.extensions.tryUnwrapContext 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.material.textfield.MaterialAutoCompleteTextView import com.google.android.material.textfield.TextInputLayout +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.hl7.fhir.r4.model.QuestionnaireResponse import timber.log.Timber @@ -52,17 +57,21 @@ internal object DropDownViewHolderFactory : private lateinit var header: HeaderView private lateinit var textInputLayout: TextInputLayout private lateinit var autoCompleteTextView: MaterialAutoCompleteTextView - private lateinit var clearIcon: ImageView + private lateinit var clearInputIcon: FrameLayout override lateinit var questionnaireViewItem: QuestionnaireViewItem private lateinit var context: AppCompatActivity - private var isDropdownEditable = true override fun init(itemView: View) { header = itemView.findViewById(R.id.header) textInputLayout = itemView.findViewById(R.id.text_input_layout) autoCompleteTextView = itemView.findViewById(R.id.auto_complete) + clearInputIcon = itemView.findViewById(R.id.clear_input_icon) context = itemView.context.tryUnwrapContext()!! - clearIcon = itemView.findViewById(R.id.clearIcon) + autoCompleteTextView.setOnFocusChangeListener { view, hasFocus -> + if (!hasFocus) { + (view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(view.windowToken, 0) + } + } } override fun bind(questionnaireViewItem: QuestionnaireViewItem) { @@ -97,8 +106,8 @@ internal object DropDownViewHolderFactory : answerOptionList .firstOrNull { it.answerId == selectedAnswerIdentifier } ?.let { - autoCompleteTextView.setText(it.answerOptionString) - autoCompleteTextView.setSelection(it.answerOptionString.length) + autoCompleteTextView.setText(it.answerOptionStringSpanned()) + autoCompleteTextView.setSelection(it.answerOptionStringSpanned().length) autoCompleteTextView.setCompoundDrawablesRelative( it.answerOptionImage, null, @@ -109,68 +118,41 @@ internal object DropDownViewHolderFactory : autoCompleteTextView.setAdapter(adapter) autoCompleteTextView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> - if (isDropdownEditable) { - val selectedItem = adapter.getItem(position) - autoCompleteTextView.setText(selectedItem?.answerOptionString, false) - autoCompleteTextView.setCompoundDrawablesRelative( - adapter.getItem(position)?.answerOptionImage, - null, - null, - null, - ) - - isDropdownEditable = false - val selectedAnswer = - questionnaireViewItem.enabledAnswerOptions - .firstOrNull { it.value.identifierString(context) == selectedItem?.answerId } - ?.value - - context.lifecycleScope.launch { - if (selectedAnswer == null) { - questionnaireViewItem.clearAnswer() - } else { - questionnaireViewItem.setAnswer( - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent() - .setValue(selectedAnswer), - ) - } + val selectedItem = adapter.getItem(position) + autoCompleteTextView.setText(selectedItem?.answerOptionStringSpanned(), false) + autoCompleteTextView.setCompoundDrawablesRelative( + adapter.getItem(position)?.answerOptionImage, + null, + null, + null, + ) + val selectedAnswer = + questionnaireViewItem.enabledAnswerOptions + .firstOrNull { it.value.identifierString(context) == selectedItem?.answerId } + ?.value + + context.lifecycleScope.launch { + if (selectedAnswer == null) { + questionnaireViewItem.clearAnswer() + } else { + questionnaireViewItem.setAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent() + .setValue(selectedAnswer), + ) } } } - - autoCompleteTextView.doAfterTextChanged { - if (it.isNullOrBlank()) { - // Hide the clear icon when the text is empty - clearIcon.visibility = View.GONE - - // Delay to ensure dropdown is displayed after text is cleared - // And after MaterialAutoCompleteTextView resets its state - autoCompleteTextView.postDelayed( - { - if (autoCompleteTextView.isPopupShowing.not() && isDropdownEditable) { - autoCompleteTextView.showDropDown() - } - }, - 100, - ) - } else { - // Show the clear icon when the text is not empty - clearIcon.visibility = View.VISIBLE + val isEditable = questionnaireViewItem.answers.isEmpty() + if (!isEditable) autoCompleteTextView.clearFocus() + autoCompleteTextView.keyListener = if (isEditable) TextKeyListener.getInstance() else null + clearInputIcon.visibility = if (isEditable) View.GONE else View.VISIBLE + clearInputIcon.setOnClickListener { + context.lifecycleScope.launch { + delay(200) // to show ripple effect on the icon before clearing the answer + questionnaireViewItem.clearAnswer() } } - clearIcon.setOnClickListener { - // Clear the text in the AutoCompleteTextView - autoCompleteTextView.text = null - - // Enable dropdown editing after text is cleared - isDropdownEditable = true - - // Clear the answer added in the questionnaireViewItem after clearIcon is clicked - context.lifecycleScope.launch { questionnaireViewItem.clearAnswer() } - setReadOnly(false) - } - displayValidationResult(questionnaireViewItem.validationResult) } @@ -185,8 +167,6 @@ internal object DropDownViewHolderFactory : override fun setReadOnly(isReadOnly: Boolean) { textInputLayout.isEnabled = !isReadOnly - autoCompleteTextView.isEnabled = isDropdownEditable && !isReadOnly - if (isReadOnly) clearIcon.visibility = View.GONE } private fun cleanupOldState() { @@ -209,7 +189,7 @@ internal class AnswerOptionDropDownArrayAdapter( val answerOption: DropDownAnswerOption? = getItem(position) val answerOptionTextView = listItemView?.findViewById(R.id.answer_option_textview) as TextView - answerOptionTextView.text = answerOption?.answerOptionString + answerOptionTextView.text = answerOption?.answerOptionStringSpanned() answerOptionTextView.setCompoundDrawablesRelative( answerOption?.answerOptionImage, null, @@ -231,4 +211,6 @@ internal data class DropDownAnswerOption( override fun toString(): String { return this.answerOptionString } + + fun answerOptionStringSpanned(): Spanned = answerOptionString.toSpanned() } diff --git a/datacapture/src/main/res/drawable/ic_clear.xml b/datacapture/src/main/res/drawable/ic_clear.xml index 625ec29530..584a2d08cd 100644 --- a/datacapture/src/main/res/drawable/ic_clear.xml +++ b/datacapture/src/main/res/drawable/ic_clear.xml @@ -1,12 +1,13 @@ diff --git a/datacapture/src/main/res/layout/drop_down_view.xml b/datacapture/src/main/res/layout/drop_down_view.xml index 318a75ce12..60422cf6be 100644 --- a/datacapture/src/main/res/layout/drop_down_view.xml +++ b/datacapture/src/main/res/layout/drop_down_view.xml @@ -13,7 +13,7 @@ --> + android:layout_height="wrap_content"> + android:layout_height="wrap_content"> + android:drawablePadding="@dimen/icon_drawable_padding" /> - + android:padding="@dimen/drop_down_clear_input_icon_ripple_padding" + android:layout_marginTop="@dimen/drop_down_clear_icon_margin_top" + android:layout_marginEnd="@dimen/drop_down_clear_icon_margin_end"> + + + + diff --git a/datacapture/src/main/res/values/dimens.xml b/datacapture/src/main/res/values/dimens.xml index 04e01f3162..f44604a271 100644 --- a/datacapture/src/main/res/values/dimens.xml +++ b/datacapture/src/main/res/values/dimens.xml @@ -95,7 +95,9 @@ 16dp - 28dp + 38dp + 4dp + 4dp 48dp