I have custom LinearLayout which height can be set to maximum or minimum based on content it holds. But if there is RecyclerView inside this LinearLayout, onMeasure is still returning 0 when I call adapter.notifyDataSetChange() in order to refresh RecyclerView content.
There is like 300 items in RecyclerView but onMeasure for parent of RecyclerView is still measuring 0 height. I need to cap height of RecyclerView to certain value which is set on view initialization.
Custom LinearLayout
class LinearLayoutWithVariableHeight: LinearLayout {
companion object {
var WITHOUT_HEIGHT_VALUE = -1
}
private var maxHeight = WITHOUT_HEIGHT_VALUE
private var minHeight = WITHOUT_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) {}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var measuredHeight = heightMeasureSpec
val currHeight = getCurrContentHeight()
try {
val heightSize: Int
App.log("LinearLayoutWithMaxHeight: onMeasure curr_height: $currHeight, max: $maxHeight")
if (maxHeight != WITHOUT_HEIGHT_VALUE && currHeight > maxHeight) {
App.log("LinearLayoutWithMaxHeight: onMeasure set xy max height")
heightSize = maxHeight
} else if (minHeight != WITHOUT_HEIGHT_VALUE && currHeight < minHeight){
App.log("LinearLayoutWithMaxHeight: onMeasure set xy min height")
heightSize = min(minHeight, maxHeight)
} else {
heightSize = currHeight
}
App.log("LinearLayoutWithMaxHeight: onMeasure heightSize: $heightSize")
measuredHeight = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.AT_MOST)
layoutParams.height = heightSize
} catch (e: Exception) {
} finally {
App.log("LinearLayoutWithMaxHeight: 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("LinearLayoutWithMaxHeight: getCurrChildHeight: $h")
if (h > height) height = h
}
return height
}
fun setMaxHeight(maxHeight: Int) {
this.maxHeight = maxHeight
}
fun setMinHeight(minHeight: Int) {
this.minHeight = minHeight
}
}
Update: I tried to make something similar to RecyclerView instead of its parent, it worked well but RecyclerView is not scrollable now:
class RecyclerViewWithMaxHeight: RecyclerView {
companion object {
var WITHOUT_HEIGHT_VALUE = -1
}
private var maxHeight = WITHOUT_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) {}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
App.log("RecyclerViewWithMaxHeight: onMeasure")
super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST))
}
fun setMaxHeight(maxHeight: Int) {
this.maxHeight = maxHeight
}
}
main_content_layout.xml:
<RelativeLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:clipChildren="false"
android:background="@color/content"
app:layout_insetEdge="bottom"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<RelativeLayout
android:id="@+id/dialog_top"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/dialog_top_corners"
android:layout_width="match_parent"
android:layout_height="20dp"
android:orientation="horizontal"
android:elevation="0dp"
android:translationZ="@dimen/padding_small"
android:background="@drawable/bg_bottomsheet_top">
</LinearLayout>
</RelativeLayout>
<RelativeLayout
android:id="@+id/dialog_main_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/dialog_top">
<LinearLayout
android:id="@+id/dialog_title_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/content"
android:orientation="vertical">
</LinearLayout>
<LinearLayout
android:id="@+id/dialog_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/dialog_title_content"
android:orientation="vertical">
<com.app.components.RecyclerViewWithMaxHeight
android:id="@+id/list"
android:clipToPadding="false"
android:clipChildren="false"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/test_item"/>
</LinearLayout>
<LinearLayout
android:id="@+id/buttonLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/dialog_content"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal"
android:layout_centerHorizontal="true"
android:orientation="vertical">
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
UPDATE 2:
So if I use only this layout above, It is scrolling perfectly. But as this layout is part of bottomsheet which is part of another layout, I have to wrap it with CoordinatorLayout and some parent(in this case FrameLayout). If I do it, then scrolling of RecyclerView is not working. But I'm not sure why it is happening. I need to have this scrolling inside BottomSheet which is logically part of CoordinatorLayout because in other case I can't use BottomSheetBehavior.
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/bottomContentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:clipChildren="false">
... some other content anchored to top of content below
<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">
... main_content_layout.xml
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
I've tried to add app:layout_behavior="@string/appbar_scrolling_view_behavior" into my RecyclerView layout but same result - can't scroll it.
UPDATE 3
As I'm going now with non-NestedScrollView version using single RecyclerView inside CoordinatorLayout, I found some issue with touch events.
I can scroll RecyclerView if I start to drag it right on boundaries between RecyclerView and non-scrollable View above. I have to put my finger there specifically to start scrolling. If I do it in the middle of RecyclerView for example, it won't scroll.
Added small onTouchListener and indeed its not firing at all if RecyclerView is under CoordinatorLayout.
list.setOnTouchListener { view, motionEvent ->
App.log("ListOnTouch -> motionEvent: $motionEvent")
true
}
I'm not sure why CoordinatorLayout is blocking touch events of my RecyclerView and why its not blocked right on boundaries between list and other View above.
I've tried to add android:descendantFocusability="afterDescendants" to CoordinatorLayout top-most View (In my case FrameLayout) but its not helping at all.
UPDATE 4
Since I possibly found an issue which is causing this weird behavior here is some insight to it.
As I mentioned I'm using this CoordinatorLayout to have BottomsheetBehavior attached to its top-most child.
As I found out CoordinatorLayout is not supporting multiple scrollable children within its top-most child because BottomsheetBehavior is working only with 1st scroll child in View hierarchy.
I have horizontal RecyclerView on top and then vertical one below it.
I made this custom BottomsheetBehavior which should handle focus on each RecyclerView inside CoordinatorLayout.
And its finally working, but I have an issue with scrolling behavior in it:
- If I touch it, it always scrolls my RecyclerView to top and start from there
- If I scroll and lift my finger from touchscreen, it stops scrolling (there is not that generic fast-scroll effect).
I need to solve these issues and it should work correctly.
class MultiScrollRecyclerViewBottomsheetBehavior<V : View>(
context: Context,
attrs: AttributeSet
) : BottomSheetBehavior<V>(context, attrs) {
private var activeRecyclerView: RecyclerView? = null
private var coordinatorHeight = 0
override fun onLayoutChild(parent: CoordinatorLayout, child: V, layoutDirection: Int): Boolean {
val lp = child.layoutParams as CoordinatorLayout.LayoutParams
if (lp.height == CoordinatorLayout.LayoutParams.MATCH_PARENT) {
child.layout(0, parent.height - peekHeight, parent.width, parent.height)
} else {
super.onLayoutChild(parent, child, layoutDirection)
}
// get the height of the CoordinatorLayout
App.log("MultiScrollBottomsheetBehavior: onLayoutChild: parent: ${parent.height}")
coordinatorHeight = parent.height
return true
}
override fun onInterceptTouchEvent(
parent: CoordinatorLayout,
child: V,
event: MotionEvent
): Boolean {
if (event.action == MotionEvent.ACTION_DOWN) {
// Find the topmost RecyclerView that is below the touch point
activeRecyclerView = findChildRecyclerView(parent, event.x.toInt(), event.y.toInt())
}
return super.onInterceptTouchEvent(parent, child, event)
}
override fun onTouchEvent(parent: CoordinatorLayout, child: V, event: MotionEvent): Boolean {
// Forward touch events to the topmost RecyclerView
return activeRecyclerView?.onTouchEvent(event) ?: super.onTouchEvent(parent, child, event)
}
override fun onNestedPreScroll(
coordinatorLayout: CoordinatorLayout, child: V,
target: View, dx: Int, dy: Int, consumed: IntArray,
type: Int
) {
if (type == ViewCompat.TYPE_TOUCH && state == STATE_EXPANDED) {
// Forward scroll events to the topmost RecyclerView
activeRecyclerView?.let {
if (dy > 0 && !it.canScrollVertically(1)) {
// If the RecyclerView is scrolled to the bottom and the user tries to scroll further down,
// let the default behavior handle the event
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
} else {
// Otherwise, forward the event to the RecyclerView
it.scrollBy(0, dy)
consumed[1] = dy
}
}
} else {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
}
}
/**
* Recursively searches the [parent] view hierarchy for a [RecyclerView] that contains the given [x] and [y] coordinates.
*/
private fun findChildRecyclerView(parent: ViewGroup, x: Int, y: Int): RecyclerView? {
for (i in 0 until parent.childCount) {
val child = parent.getChildAt(i)
if (child is RecyclerView && isPointInsideView(x, y, child)) {
return child
} else if (child is ViewGroup) {
val recyclerView = findChildRecyclerView(child, x, y)
if (recyclerView != null) {
return recyclerView
}
}
}
return null
}
/**
* Returns true if the given [x] and [y] coordinates are inside the bounds of the [view].
*/
private fun isPointInsideView(x: Int, y: Int, view: View): Boolean {
val location = IntArray(2)
view.getLocationOnScreen(location)
val viewX = location[0]
val viewY = location[1]
return (x >= viewX && x <= viewX + view.width && y >= viewY && y <= viewY + view.height)
}
}
And I just add it to my top-most parent like this:
<FrameLayout
android:id="@+id/bottomSheetContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_insetEdge="bottom"
app:layout_behavior="com.project.utils.MultiScrollRecyclerViewBottomsheetBehavior">
</FrameLayout>

I have stuck with a similar issue but with
CollapsingToolbarthough maybe the same thing might get applied in it too. Try to do the following changes in yourmain_content_layout.xmlNestedScrollBarhaving this propertyapp:layout_behavior="@string/appbar_scrolling_view_behavior"having inner parent layout of your choice.android:nestedScrollingEnabled="false"in yourRecyclerViewwhich is inside the Scrollable container for proper scrolling.