I have RecyclerView inside NestedScrollView. What I want is to scroll entire content of NestedScrollView if NestedScrollViews content (including list passed to RecyclerViews adapter) is above certain height.
I have custom NestedScrollView class to handle this which is measuring height of all children inside onMeasure and if total children height will pass certain value, it should enable scrolling instead of expanding NestedScrollView height. (I need this feature because NestedScrollView is allowed to be expanded to maximum of 40% of display height).
This feature is working well if I'm filling NestedScrollView with generic components containing RelativeLayouts, TextViews, Buttons etc. But RecyclerView is completely blocking scrolling of entire NestedScrollView for some reason.
Here is logging from NestedScrollView:
ScrollViewWithMaxHeight: onMeasure curr_height: 11678, max: 966
NestedScrollView:
class ScrollViewWithMaxHeight: NestedScrollView{
private var maxHeight = WITHOUT_MAX_HEIGHT_VALUE
constructor(context: Context) : super(context) {}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {}
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) {}
var onHeightChangeListener: ((Int)->Unit)? = null
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var measuredHeight = heightMeasureSpec
val currHeight = getCurrContentHeight()
try {
val heightSize: Int
App.log("ScrollViewWithMaxHeight: onMeasure curr_height: $currHeight, max: $maxHeight")
if (maxHeight != WITHOUT_MAX_HEIGHT_VALUE && currHeight > maxHeight) {
heightSize = maxHeight
} else {
heightSize = currHeight
}
measuredHeight = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.AT_MOST)
layoutParams.height = heightSize
onHeightChangeListener?.invoke(heightSize)
} catch (e: Exception) {
} finally {
App.log("ScrollViewWithMaxHeight: onMeasure final: $measuredHeight")
super.onMeasure(widthMeasureSpec, measuredHeight)
}
}
private fun getCurrContentHeight(): Int{
var height = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
val h = child.measuredHeight
App.log("ScrollViewWithMaxHeight: getCurrChildHeight: $h")
if (h > height) height = h
}
return height
}
fun setOnDynamicHeightChangeListener(l: (Int)->Unit){
onHeightChangeListener = l
}
fun setMaxHeight(maxHeight: Int) {
this.maxHeight = maxHeight
}
companion object {
var WITHOUT_MAX_HEIGHT_VALUE = -1
}
}
RecyclerView init + layout:
list = findViewById<RecyclerView>(R.id.list).also { rv ->
rv.isNestedScrollingEnabled = false
rv.setOnTouchListener { _, _ -> false }
rv.adapter = adapter
rv.layoutManager = LinearLayoutManager(a)
}
Layout:
<RelativeLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:clipChildren="false">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:clipToPadding="false"
android:clipChildren="false"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<components.loading_indicator.LoadingIndicator
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
</RelativeLayout>
NestedScrollView init:
it.findViewById<ScrollViewWithMaxHeight>(R.id.scrollParent).also { sv->
sv.setMaxHeight(getScrollViewHeight())
sv.setOnDynamicHeightChangeListener { h-> }
sv.isFillViewport = true
sv.viewTreeObserver.addOnScrollChangedListener {
isScrollOnTop = sv.scrollY == 0
}
}
Here is issue what is causing Scroll lags:
2023-03-28 09:48:18.430 I RecyclerViewAdapter: onCreateViewHolder: creating item
2023-03-28 09:48:18.435 I RecyclerViewAdapter: onCreateViewHolder: creating item
2023-03-28 09:48:18.440 I RecyclerViewAdapter: onCreateViewHolder: creating item
2023-03-28 09:48:18.445 I RecyclerViewAdapter: onCreateViewHolder: creating item
2023-03-28 09:48:27.955 I RecyclerViewAdapter: onCreateViewHolder: creating item
2023-03-28 09:48:27.962 I RecyclerViewAdapter: onCreateViewHolder: creating item
2023-03-28 09:48:27.967 I RecyclerViewAdapter: onCreateViewHolder: creating item
2023-03-28 09:48:27.973 I RecyclerViewAdapter: onCreateViewHolder: creating item
2023-03-28 09:48:27.979 I RecyclerViewAdapter: onCreateViewHolder: creating item
2023-03-28 09:48:27.985 I RecyclerViewAdapter: onCreateViewHolder: creating item
2023-03-28 09:48:27.990 I RecyclerViewAdapter: onCreateViewHolder: creating item
2023-03-28 09:48:27.996 I RecyclerViewAdapter: onCreateViewHolder: creating item
2023-03-28 09:48:28.002 I RecyclerViewAdapter: onCreateViewHolder: creating item
2023-03-28 09:48:28.008 I RecyclerViewAdapter: onCreateViewHolder: creating item
2023-03-28 09:48:28.013 I RecyclerViewAdapter: onCreateViewHolder: creating item
2023-03-28 09:48:28.019 I RecyclerViewAdapter: onCreateViewHolder: creating item
2023-03-28 09:48:28.025 I RecyclerViewAdapter: onCreateViewHolder: creating item
2023-03-28 09:48:28.031 I RecyclerViewAdapter: onCreateViewHolder: creating item
2023-03-28 09:48:28.037 I RecyclerViewAdapter: onCreateViewHolder: creating item
...
This is log inside onCreateViewHolder. Its called on every single item in list. Even as I try to scroll, its called again. Not sure what is causing this. I have RecyclerViews all over my app and only this one is not recycling.
Ok I found it what is causing this. Its really weird. I have another horizontal RecyclerView above NestedScrollView (which is obviously not part of NestedScrollView because its static and its not suppose to scroll vertically. Its just horizontal picker with items in it.
As soon as I removed it from layout, vertical scrolling worked as usual with proper recycling.
My layout design and component design is modular and I inject layout with inflation and addView function to parents so I can make standalone components within code which can be injected into any empty layout.
I removed NestedScrollView from it because its not necessary to reconstruct the bug. It is happening even if there is only RecyclerView as component (so I dont need nested scrolling at all).
As soon as horizontal_picker_content is injected there and used, its blocking recycling of vertical_list_container which is place where my vertical RecyclerView is injected.
So I reconstructed how my final layout would look like in my DialogBottomSheet so you can see how it is structured:
<RelativeLayout
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="vertical"
tools:ignore="UselessParent">
<LinearLayout
android:id="@+id/topInfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/bottomContentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:clipChildren="false">
<FrameLayout
android:id="@+id/bottomSheetContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_insetEdge="bottom"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<RelativeLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:clipChildren="false"
android:background="@color/background_paper"
app:layout_insetEdge="bottom"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<RelativeLayout
android:id="@+id/peekSwiper"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/dialog_title_block"
android:layout_width="match_parent"
android:layout_height="20dp"
android:orientation="horizontal"
android:elevation="0dp"
android:translationZ="4dp"
android:background="@drawable/bg_bottomsheet_top"/>
</RelativeLayout>
<LinearLayout
android:id="@+id/dialog_bottom_block"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_below="@id/peekSwiper"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal">
<LinearLayout
android:id="@+id/dialog_title_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/background_paper"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/horizontal_picker_content"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:clipChildren="false"
android:gravity="center">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/horizontalPickList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:scrollbars="horizontal"
android:clipToPadding="false"
android:clipChildren="false"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"/>
<include
layout="@layout/menu_item_separator"
android:id="@+id/separator"
android:layout_width="wrap_content"
android:layout_height="1dp"
android:layout_below="@+id/horizontalPickList"/>
</RelativeLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/dialog_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:background="@color/background_paper">
<LinearLayout
android:id="@+id/vertical_content_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:clipChildren="false"
android:orientation="vertical">
<LinearLayout
android:id="@+id/vertical_content_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:clipChildren="false"
android:orientation="vertical">
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/vertical_list_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/vertical_content_parent"
android:clipToPadding="false"
android:clipChildren="false"
android:orientation="vertical">
</LinearLayout>
</RelativeLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/buttonLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/dialog_bottom_block"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal"
android:layout_centerHorizontal="true"
android:orientation="vertical">
</LinearLayout>
</RelativeLayout>
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</RelativeLayout>
rv.isNestedScrollingEnabled = falsetry removing this line and letting the RecyclerView handle the nested scrolling. You can also remove the rv.setOnTouchListener { _, _ -> false } line, as it doesn't seem to be doing anything.