androidkotlinandroid-coordinatorlayout

How to get the Coordinator Layout to apply behavior after scrollTo function?


There is a CoordinatorLayout that contains an AppBarLayout and a NestedScrollView, which has a TextView. The TextView has Markdown text. I want to scroll to the anchor (#header). The code below works as expected, but after calling NestedScrollView.scrollTo, the CoordinatorLayout doesn't trigger the behaviour of hiding the AppBarLayout when scrolling.

What needs to be done in order for the CoordinatorLayout to receive a scroll event triggered by the code? I have tried to use smoothScrollTo, scrollBy, doesn't work. When the user scrolls by hand, the AppBarLayout hides as expected.

fun scrollToAnchor(view: View, anchor: String) {
    val textView = view as TextView
    textView.setTextIsSelectable(false)
    val spanned = textView.text as Spanned
    val spans = spanned.getSpans(0, spanned.length, AnchorSpan::class.java)
    if (spans != null) {
        for (span in spans) {
            if (anchor == span.anchor) {
                val start = spanned.getSpanStart(span)
                val line = textView.layout.getLineForOffset(start)
                val top = textView.layout.getLineTop(line)

                val scrollToFunc = (view.parent as? NestedScrollView)?.let { it::scrollTo } ?: view::scrollTo
                scrollToFunc.invoke(0, top)
                break
            }
        }
    }
    textView.setTextIsSelectable(true)
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/doc_page_coordinator_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true">

        <com.google.android.material.appbar.MaterialToolbar
            android:id="@+id/doc_paga_top_app_bar"
            style="@style/Widget.Material3.Toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:elevation="0dp"
            app:layout_collapseMode="pin"
            app:layout_scrollFlags="scroll|enterAlways|snap"
            app:menu="@menu/menu_only_close"
            app:title="@string/docs_title" />

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginBottom="?actionBarSize"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <TextView
            android:id="@+id/markdown_root"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginHorizontal="@dimen/view_indent_horizontal"
            android:isScrollContainer="true"
            android:scrollbarThumbVertical="@android:color/transparent"
            android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
            android:textColor="?attr/colorOnBackground"
            android:textIsSelectable="true"
            tools:text="Lorem ipsum dolor sit amet" />

    </androidx.core.widget.NestedScrollView>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

Solution

  • When you programmatically scroll the NestedScrollView, the AppBarLayout may not hide automatically because the scrolling behavior of the AppBarLayout relies on the user's scroll gestures, rather than programmatic scrolls. However, you can trigger the hide behavior of the AppBarLayout manually when you scroll programmatically.

    Use setExpanded(false, true) on the AppBarLayout after the scroll. This will collapse the app bar and ensure it hides as you scroll to the specified header. Steps:

    1. give id to app_bar Layout

    2. setExpanded(false, true) for it.

      fun scrollToAnchor(view: View, anchor: String) {
       val textView = view as TextView
       textView.setTextIsSelectable(false)
       val spanned = textView.text as Spanned
       val spans = spanned.getSpans(0, spanned.length, 
            AnchorSpan::class.java)
            if (spans != null) {
            for (span in spans) {
             if (anchor == span.anchor) {
              val start = spanned.getSpanStart(span)
              val line = textView.layout.getLineForOffset(start)
              val top = textView.layout.getLineTop(line)
      
               val appBarLayout = findViewById<AppBarLayout>(R.id.app_bar_layout) 
               appBarLayout.setExpanded(false, true)
      
      
              val scrollToFunc = (view.parent as? NestedScrollView)?.let { it::scrollTo } ?: view::scrollTo
              scrollToFunc.invoke(0, top)
              break
          }
      }
       }        textView.setTextIsSelectable(true)
            }