I am trying to implement a ViewGroup named MyRecyclerView that acts like a real RecyclerView. LayoutManager and ViewHolder are not implemented in MyReclerView, the whole layout process is done by MyRecyclerView on its own.
MyAdapter is a dummy interface for feeding data to MyRecyclerView:
val myAdapter = object: MyRecyclerAdapter{
override fun onCreateViewHolder(row: Int, convertView: View?, parent: ViewGroup): View {
val id = when(getItemViewType(row)) {
0 -> R.layout.item_custom_view0
1 -> R.layout.item_custom_view1
else -> -1
}
val resultView = convertView ?: layoutInflater.inflate(id, parent, false)
if(getItemViewType(row) == 1)
resultView.findViewById<TextView>(R.id.tv_item1).text = testList[row]
return resultView
}
override fun onBindViewHolder(row: Int, convertView: View?, parent: ViewGroup):View {
val id = when(getItemViewType(row)) {
0 -> R.layout.item_custom_view0
1 -> R.layout.item_custom_view1
else -> -1
}
val resultView = convertView ?: layoutInflater.inflate(id, parent, false)
if(getItemViewType(row) == 1)
resultView.findViewById<TextView>(R.id.tv_item1).text = testList[row]
return resultView
}
override fun getItemViewType(row: Int): Int = row%2
override fun getItemViewTypeCount(): Int {
return 2
}
override fun getItemCount(): Int = itemCount
override fun getItemHeight(row: Int): Int = itemHeight // fixed height
}
A diagram illustrates how I wish this MyRecyclerView to work out:
I did not override onMeasure in MyRecyclerView, since I could not determine how many items should be put on screen when data first loaded. And this job is handled in onLayout. Items are set fixed heights. When the sum of heights of items greater than the height of MyRecyclerView, no more items are layout.
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
if(mNeedRelayout || changed) {
mNeedRelayout = false
mItemViewList.clear() // mItemViewList cached all views on Screen
removeAllViews()
if (mAdapter != null) {
// heights of each item is fixed (as 80dp) and accessed by MyAdapter
for(i in 0 until mItemCount)
mHeights[i] += mAdapter!!.getItemHeight(i)
// since onMeasure is not implemented
// mWidth: the width of MyRecyclerView
// mHeight: the height of MyRecyclerView
mWidth = r - l
mHeight = b - t
var itemViewTop = 0
var itemViewBottom = 0
// foreach whole data list, and determine how many items should be
// put on screen when data is first loaded
for(i in 0 until mItemCount){
itemViewBottom = itemViewTop + mHeights[i]
// itemView is layout in makeAndSetUpView
val itemView = makeAndSetupView(i, l,itemViewTop,r,itemViewBottom)
// there is no gap between item views
itemViewTop = itemViewBottom
mItemViewList.add(itemView)
addView(itemView, 0)
// if top of current item view is below screen, it should not be
// displayed on screen
if(itemViewTop > mHeight) break
}
}
}
}
I know itemView returned by makeAndSetUpView is a ViewGroup, and I call layout on it, all of its children will be measured and layout too.
private fun makeAndSetupView(
index: Int,
left: Int,
top: Int,
right: Int,
bottom: Int
): View{
// a simple reuse scheme
val itemView = obtain(index, right-left, bottom-top)
// layout children
itemView.layout(left, top, right, bottom)
return itemView
}
When I scroll down or up MyRecyclerView, Views out of screen will be reused and new data (text strings) are set in TextView, these views fill the blank. This behavior is carried out via relayoutViews:
private fun relayoutViews(){
val left = 0
var top = -mScrollY
val right = mWidth
var bottom = 0
mItemViewList.forEachIndexed { index, view ->
bottom = top + mHeights[index]
view.layout(left, top, right, bottom)
top = bottom
}
// Relayout is finished
mItemViewList.forEachIndexed { index, view ->
val lastView = (view as ViewGroup).getChildAt(0)
Log.d("TEST", " i: $index top: ${lastView.top} bottom: ${lastView.bottom} left: ${lastView.left} right: ${lastView.right}")
Log.d("TEST", " i: $index width: ${lastView.width} height: ${lastView.height}")
}
}
Debug output here is normal, views in cached mItemViewList seem to carry everything needed. And the text string is also set into TexView in the ItemView ViewGroup.
But it is so confusing that it seems no TextView is even created when I scroll and reuse these views.
ItemViews here are wrapped by a RelativeLayout:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="#dimen/item_height">
<TextView
android:text=" i am a fixed Text"
android:id="#+id/tv_item0"
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
But when I just changed it to LinearLayout, everything is ok and views are reused and text strings are set.
<?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="#dimen/item_height"
android:orientation="vertical"
android:gravity="center"
android:layout_marginTop="2dp">
<TextView
android:text="i am a fixed text"
android:id="#+id/tv_item0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
Can anyone point out what I did wrong here? I am looking forward to your enlightment.
I am using the ViewPager2 for showing the 3 adapter item.. and each item has a different layout. so the height of the viwepager2 should be resized when changing the tab of the viewpager2.
Please suggest a solution for the Viewpager2.
I achieved something like that with
private fun updatePagerHeightForChild(view: View, pager: ViewPager2) {
view.post {
val wMeasureSpec = View.MeasureSpec.makeMeasureSpec(view.width, View.MeasureSpec.EXACTLY)
val hMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
view.measure(wMeasureSpec, hMeasureSpec)
pager.layoutParams = (pager.layoutParams).also { lp -> lp.height = view.measuredHeight }
pager.invalidate()
}
}
get the current page view using the below snippet:
viewPager2.setPageTransformer { page, position ->
updatePagerHeightForChild(page, viewPager2)
}
In recycler_view adapter class the following code is added for dynamically changing height of item_view
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TaskListViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.task_item_new, parent, false)
val vto = parentLayer!!.getViewTreeObserver()
vto.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
parentLayer!!.getViewTreeObserver().removeGlobalOnLayoutListener(this)
} else {
parentLayer!!.getViewTreeObserver().removeOnGlobalLayoutListener(this)
}
var quicktaskLayerwidth = parentLayer!!.getMeasuredWidth()
var quicktaskLayerheight = parentLayer!!.getMeasuredHeight()
Log.e("Parent_Ht", "" + quicktaskLayerheight/4)
itemView.getLayoutParams().height = quicktaskLayerheight / 4
}
})
return TaskListViewHolder(itemView)
}
In activity class there is a GridLayoutManager manager is used for showing 8 icons in recycler_view.
And there is a parent Linear layout, height of which is measured in ViewTreeObserver
IMP: At first time recycler_view item height works fine, after loading some other pages and returning to main page the height of item is changing. But the log shows height is calculated and which is correct.
If someone faced same issue and resolved, need your help here.
There are a few posts on getting ViewPager to work with varying height items that center around extending ViewPager itself to modify its onMeasure to support this.
However, given that ViewPager2 is marked as a final class, extending it isn't something that we can do.
Does anyone know if there's a way to make this work out?
E.g. let's say I have two views:
View1 = 200dp
View2 = 300dp
When the ViewPager2 (layout_height="wrap_content") loads -- looking at View1, its height will be 200dp.
But when I scroll over to View2, the height is still 200dp; the last 100dp of View2 is cut off.
The solution is to register a PageChangeCallback and adjust the LayoutParams of the ViewPager2 after asking the child to re-measure itself.
pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
val view = // ... get the view
view.post {
val wMeasureSpec = MeasureSpec.makeMeasureSpec(view.width, MeasureSpec.EXACTLY)
val hMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
view.measure(wMeasureSpec, hMeasureSpec)
if (pager.layoutParams.height != view.measuredHeight) {
// ParentViewGroup is, for example, LinearLayout
// ... or whatever the parent of the ViewPager2 is
pager.layoutParams = (pager.layoutParams as ParentViewGroup.LayoutParams)
.also { lp -> lp.height = view.measuredHeight }
}
}
}
})
Alternatively, if your view's height can change at some point due to e.g. asynchronous data load, then use a global layout listener instead:
pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
private val listener = ViewTreeObserver.OnGlobalLayoutListener {
val view = // ... get the view
updatePagerHeightForChild(view)
}
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
val view = // ... get the view
// ... IMPORTANT: remove the global layout listener from other views
otherViews.forEach { it.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener) }
view.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
}
private fun updatePagerHeightForChild(view: View) {
view.post {
val wMeasureSpec = MeasureSpec.makeMeasureSpec(view.width, MeasureSpec.EXACTLY)
val hMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
view.measure(wMeasureSpec, hMeasureSpec)
if (pager.layoutParams.height != view.measuredHeight) {
// ParentViewGroup is, for example, LinearLayout
// ... or whatever the parent of the ViewPager2 is
pager.layoutParams = (pager.layoutParams as ParentViewGroup.LayoutParams)
.also { lp -> lp.height = view.measuredHeight }
}
}
}
}
See discussion here:
https://issuetracker.google.com/u/0/issues/143095219
In my case, adding adapter.notifyDataSetChanged() in onPageSelected helped.
Just do this for the desired Fragment in ViewPager2:
override fun onResume() {
super.onResume()
layoutTaskMenu.requestLayout()
}
Jetpack: binding.root.requestLayout() (thanks #syed-zeeshan for the specifics)
Stumbled across this case myself however with fragments.
Instead of resizing the view as the accepted answer I decided to wrap the view in a ConstraintLayout. This requires you to specify a size of your ViewPager2 and not use wrap_content.
So Instead of changing size of our viewpager it will have to be minimum size of the largest view it handles.
A bit new to Android so don't know if this is a good solution or not, but it does the job for me.
In other words:
<androidx.constraintlayout.widget.ConstraintLayout
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:layout_width="match_parent"
android:layout_height="match_parent"
>
<!-- Adding transparency above your view due to wrap_content -->
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
>
<!-- Your view here -->
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
For me this worked perfectly:
viewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
super.onPageScrolled(position,positionOffset,positionOffsetPixels)
if (position>0 && positionOffset==0.0f && positionOffsetPixels==0){
viewPager2.layoutParams.height =
viewPager2.getChildAt(0).height
}
}
})
Just call .requestLayout() to the root view of layout in the onResume() of your Fragment class which is being used in ViewPager2
Just Add this small code in your all fragments of ViewPager2
#Override
public void onResume() {
super.onResume();
binding.getRoot().requestLayout();
}
This is working for me perfectly (If you are not using binding then Just get a root layout instance in place of binding)
I had a similar problem and solved it as below.
In my case I had ViewPager2 working with TabLayout with fragments with different heights.
In each fragment in the onResume() method, I added the following code:
#Override
public void onResume() {
super.onResume();
setProperHeightOfView();
}
private void setProperHeightOfView() {
View layoutView = getView().findViewById( R.id.layout );
if (layoutView!=null) {
ViewGroup.LayoutParams layoutParams = layoutView.getLayoutParams();
if (layoutParams!=null) {
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
layoutView.requestLayout();
}
}
}
R.id.layout is layout of particular fragment.
I hope I helped.
Best regards,
T.
No posted answer was entirely applicable for my case - not knowing the height of each page in advance - so I solved different ViewPager2 pages heights using ConstraintLayout in the following way:
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
>
<!-- ... -->
</com.google.android.material.appbar.AppBarLayout>
<!-- Wrapping view pager into constraint layout to make it use maximum height for each page. -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/viewPagerContainer"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="#id/bottomNavigationView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/appBarLayout"
>
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottomNavigationView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="#menu/bottom_navigation_menu"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
#Mephoros code works perfectly when swiped between views but won't work when views are peeked for first time. It works as intended after swiping it.
So, swipe viewpager programmatically:
binding.viewpager.setCurrentItem(1)
binding.viewpager.setCurrentItem(0) //back to initial page
I'm using the ViewPager2ViewHeightAnimator from here
I got stuck with this problem too. I was implementing TabLayout and ViewPager2 for several tabs with account information. Those tabs had to be with different heights, for example: View1 - 300dp, View2 - 200dp, Tab3 - 500dp. The height was locked within first view's height and the others were cut or extended to (example) 300dp. Like so:
So after two days of searches nothing helped me (or i had to try better) but i gave up and used NestedScrollView for all my views. For sure, now i don't have effect, that the header of profile scrolls with info in 3 views, but at least it now works somehow.
Hope this one helps someone! If you have some advices, feel free to reply!
P.s. I'm sorry for my bad english skills.
Only adapter.notifyDataSetChanged() worked for me in ViewPager2. Used below code in Kotlin.
viewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
adapter.notifyDataSetChanged()
}
})
why don't you do it by replacing not using ViewPager2.
like code in below:
private void fragmentController(Fragment newFragment){
FragmentTransaction ft;
ft = mainAct.getSupportFragmentManager().beginTransaction();
ft.replace(R.id.relMaster, newFragment);
ft.addToBackStack(null);
ft.commitAllowingStateLoss();
}
Where relMaster is RelativeLayout.
Answer by #Mephoros worked for me in the end. I had a Recyclerview with pagination(v3) in one of the fragments and it was behaving really strangely with page loads. Here is a working snippet based on the answer in case anyone has problems getting and cleaning views.
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
var view : View? = null
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
view?.let {
updatePagerHeightForChild(it)
}
}
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
// ... IMPORTANT: remove the global layout listener from other view
view?.viewTreeObserver?.removeOnGlobalLayoutListener(layoutListener)
view = (viewPager[0] as RecyclerView).layoutManager?.findViewByPosition(position)
view?.viewTreeObserver?.addOnGlobalLayoutListener(layoutListener)
}
private fun updatePagerHeightForChild(view: View) {
view.post {
val wMeasureSpec = View.MeasureSpec.makeMeasureSpec(view.width, View.MeasureSpec.EXACTLY)
val hMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
view.measure(wMeasureSpec, hMeasureSpec)
if (viewPager.layoutParams.height != view.measuredHeight) {
viewPager.layoutParams = (viewPager.layoutParams)
.also { lp -> lp.height = view.measuredHeight }
}
}
}
})
just add this code to each fragments :
override fun onResume() {
super.onResume()
binding.root.requestLayout()
}
Finally, I can fix this without requestLayout, notifyDataChanged, or the other solutions above!
It's really easy and simple!
You just need to save current height onPause, then load the saved height onResume.
Look at this example code:
public class MyTabbedFragment extends Fragment {
public MyTabbedFragmentViewBinding binding;
String TAG = "MyTabbedFragment";
int heightBeforePause;
// other code
#Override
public void onResume() {
super.onResume();
Log.d(TAG, "lifecycle | onResume | before set height | rec view height: " + binding.recycleView.getHeight() + " | height before pause: " + heightBeforePause);
// load the saved height
if(heightBeforePause > 0) {
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, heightBeforePause);
binding.recycleView.setLayoutParams(layoutParams);
}
}
#Override
public void onPause() {
super.onPause();
// save the current height
heightBeforePause = binding.recycleView.getHeight();
Log.d(TAG, "lifecycle | onPause | rec view height: " + binding.recycleView.getHeight());
}
viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
val view = (viewPager[0] as RecyclerView).layoutManager?.findViewByPosition(position)
view?.post {
val wMeasureSpec = MeasureSpec.makeMeasureSpec(view.width, MeasureSpec.EXACTLY)
val hMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
view.measure(wMeasureSpec, hMeasureSpec)
if (viewPager.layoutParams.height != view.measuredHeight) {
viewPager.layoutParams = (viewPager.layoutParams).also { lp -> lp.height = view.measuredHeight }
}
}
}
})
As stated in the question, I have a RecyclerView that displays images of different height.
The ImageView's height is initially 0dp and the width is match_parent, but in BindViewHolder I set the height to the correct value.
It works fine if you scroll down the list, but when you scroll back up:
As the previous ViewHolder is drawn it's height is initially set to something like 40dp as the image isn't loaded yet, but then suddenly it jumps to 400 as the new height is set to the ImgeView, which makes it very jittery and not smooth at all.
I tried prefetching and caching, but nothing worked.
Should I use ListView and load images on-demand knowing that the number of displayed items will reach 1000, or what can I do?
The is the ViewHolder layout
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardElevation="8dp"
app:cardCornerRadius="8dp"
android:layout_margin="8dp">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
android:id="#+id/post_image"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
android:visibility="gone"/>
</android.support.constraint.ConstraintLayout>
and this is OnBindViewHolder
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val target: SimpleTarget<File> = object: SimpleTarget<File>() {
override fun onResourceReady(resource: File, transition: Transition<in File>?) {
thumbnail.setImage(ImageSource.uri(Uri.fromFile(resource)).tilingEnabled())
}
}
if (post?.url?.endsWith(".jpg", true) == true|| post?.url?.endsWith(".png", true) == true) {
thumbnail.visibility = View.VISIBLE
thumbnail.recycle()
thumbnail.setDoubleTapZoomDuration(300)
if(post.preview != null) {
thumbnail.post {
val scale: Float = post.preview.images[0].source.height.toFloat()/post.preview.images[0].source.width.toFloat()
val layoutParams: ViewGroup.LayoutParams = thumbnail.layoutParams
layoutParams.height = (thumbnail.measuredWidth.toFloat() * scale).toInt()
thumbnail.layoutParams = layoutParams
glide.downloadOnly().load(post.preview.images[0].source.url).into(target)
}
}
} else {
thumbnail.visibility = View.GONE
glide.clear(target)
thumbnail.recycle()
}
}
}
Where thumbnail is a SSIV
The problem is I got the measured width asynchronously every time, which caused a delay in setting the height.
I calculated measuredHeight on OnCreateViewHolder and store it for later use.