Search code examples
androidkotlinandroid-edittext

Achieving Smooth Scrolling in EditText with BottomSheetDialogFragment / DialogFragment


I recently explored an app that appears to be built using a BottomSheetDialogFragment combined with an EditText. This design choice allows for smooth scrolling when the amount of text entered into the EditText exceeds a certain threshold. This functionality is particularly effective in maintaining a seamless user experience during extensive data input.

Smooth scrolling in EditText

enter image description here


I am attempting to replicate a similar effect in my application. Below is the code I'm using to achieve this:

TodoInputDialogFragment.kt

class TodoInputDialogFragment : BottomSheetDialogFragment() {
    private var _binding: TodoInputDialogFragmentBinding? = null

    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!

    private var keyboardVisible = false

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = TodoInputDialogFragmentBinding.inflate(inflater, container, false)

        val root: View = binding.root

        // https://stackoverflow.com/questions/64947700/how-to-adjust-dialog-layout-when-soft-keyboard-appears-using-the-latest-windowin
        ViewCompat.setOnApplyWindowInsetsListener(root) { _, insets ->

            // https://stackoverflow.com/a/63595830/72437
            val currKeyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
            if (!currKeyboardVisible && (this.keyboardVisible != currKeyboardVisible)) {
                dismiss()
            }
            this.keyboardVisible = currKeyboardVisible

            insets
        }

        binding.submitButton.setOnClickListener {
            submit()
        }

        return root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        focusAndShowKeyboard(requireActivity(), binding.editText)
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val dialog = super.onCreateDialog(savedInstanceState)


        // https://stackoverflow.com/questions/64947700/how-to-adjust-dialog-layout-when-soft-keyboard-appears-using-the-latest-windowin
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
            dialog.window?.also {
                it.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
            }
        }

        // https://stackoverflow.com/questions/46861306/how-to-disable-bottomsheetdialogfragment-dragging
        dialog.setOnShowListener { dialogInterface ->
            val bottomSheet = dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)

            if (bottomSheet != null) {
                val behavior: BottomSheetBehavior<*> = BottomSheetBehavior.from(bottomSheet)
                behavior.isDraggable = false
            }
        }

        return dialog
    }

    override fun getTheme(): Int {
        return com.xxx.R.style.TodoInputDialogFragmentStyle
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    private fun submit() {
    }
}

styles.xml

<!-- https://stackoverflow.com/a/58107195/72437 -->
<style name="TodoInputDialogFragmentStyle" parent="@style/ThemeOverlay.MaterialComponents.BottomSheetDialog">
    <item name="bottomSheetStyle">@style/BottomSheet_Rounded</item>
    <item name="android:windowSoftInputMode">adjustResize</item>
    <item name="android:windowIsFloating">false</item>
</style>

todo_input_dialog_fragment.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical">

    <com.yocto.wetodo.input.FocusableEditText
        android:scrollbars="vertical"

        android:id="@+id/edit_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:maxHeight="144dp"
        android:background="@android:color/transparent"
        android:fontFamily="@font/open_sans_regular"
        android:hint="Add a Todo"
        android:inputType="textMultiLine"
        android:padding="16dp"
        android:textColor="?attr/primaryTextColor"
        android:textSize="18sp" />


    <!--
        If we place the button in bottom sheet, we need to specific app:rippleColor explicitly, to
        prevent ripple color from using colorPrimary
    -->
    <com.google.android.material.button.MaterialButton
        android:elevation="4dp"
        android:translationZ="4dp"
        android:stateListAnimator="@null"

        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        android:layout_marginBottom="8dp"

        android:id="@+id/submit_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:minHeight="56dp"
        app:cornerRadius="28dp"
        android:gravity="center"
        android:layout_gravity="end"
        app:backgroundTint="@color/colorAccentLight"

        app:rippleColor="#7fffffff"
        app:iconTint="@color/primaryTextColorDark"
        app:iconGravity="textStart"
        app:iconPadding="0dp"
        app:icon="@drawable/send_24px" />
</LinearLayout>

However, I am struggling to achieve the smooth scrolling effect in EditText. Despite trying several methods, the scrolling remains jittery and unresponsive. Does anyone have suggestions on how to resolve this?

Laggy scrolling in EditText (BottomSheetDialogFragment)

enter image description here


I try another implementation, by using DialogFragment combined with an EditText.

CustomDialogFragment.kt

class CustomDialogFragment : DialogFragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.dialog_custom, container, false)
    }

    override fun onStart() {
        super.onStart()
        dialog?.window?.apply {
            // Set the layout parameters of the dialog window
            setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)

            // Adjust the window to be resized when the keyboard appears
            setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
        }
    }


}

dialog_custom.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp">

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Enter text"
        android:scrollbars="vertical" />

</LinearLayout>

Laggy scrolling in EditText (DialogFragment)

enter image description here


Do you have idea why it is so? Why certain apps able to achieve smooth scrolling in EditText, when they use it with BottomSheetDialogFragment or DialogFragment?


Solution

  • We manage to solve this issue, by wrapping EditText with a NestedScrollView.

    Here's the solution.

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:orientation="vertical">
    
        <!--
        It appears that setting android:fillViewport to either true or false makes no difference
        in our case. We are following the approach used in the noteplus project.
        -->
        <!--
        Use NestedScrollView, if our bottom sheet dialog is draggable.
        -->
        <androidx.core.widget.NestedScrollView
            android:id="@+id/scroll_view"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:fillViewport="true">
            <EditText
                android:id="@+id/edit_text"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@android:color/transparent"
                android:fontFamily="@font/open_sans_regular"
                android:hint="Add a Todo"
                android:inputType="textMultiLine"
                android:padding="16dp"
                android:textColor="?attr/primaryTextColor"
                android:textSize="18sp" />
        </androidx.core.widget.NestedScrollView>
    
    
        <!--
            If we place the button in bottom sheet, we need to specific app:rippleColor explicitly, to
            prevent ripple color from using colorPrimary
        -->
        <com.google.android.material.button.MaterialButton
            android:elevation="4dp"
            android:translationZ="4dp"
            android:stateListAnimator="@null"
    
            android:layout_marginStart="16dp"
            android:layout_marginEnd="16dp"
            android:layout_marginBottom="8dp"
    
            android:id="@+id/submit_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:minHeight="56dp"
            app:cornerRadius="28dp"
            android:gravity="center"
            android:layout_gravity="end"
            app:backgroundTint="@color/colorAccentLight"
    
            app:rippleColor="#7fffffff"
            app:iconTint="@color/primaryTextColorDark"
            app:iconGravity="textStart"
            app:iconPadding="0dp"
            app:icon="@drawable/send_24px" />
    </LinearLayout>