ViewPager2 with differing item heights and WRAP_CONTENT - android

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 }
}
}
}
})

Related

dynamic ViewPager2 height calculation for each item

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)
}

How to wrap height of Android ViewPager2 to height of current item?

This question is for the new ViewPager2 class.
There is a similar question for the old ViewPager, but the solution requires extending ViewPager. However, ViewPager2 is final so cannot be extended.
In my situation, I have a ViewPager2 that contains three different fragments. One of these fragments is much taller than the other two, which means when you swipe to one of the shorter fragments, there is a lot of empty space. So how do I effectively wrap the ViewPager2 to the height of the current fragment?
The solution is to add the following in each fragment that is part of the ViewPager2:
override fun onResume() {
super.onResume()
binding.root.requestLayout()
}
binding.root is from ViewBinding/DataBinding but it's is the main container of your layout, i.e. ConstraintLayout, LinearLayout, etc.
I'm not at all happy with this workaround, but it does solve the rendering problems I had when trying to use ViewPager2 with fragments of different heights. It will obviously slow down rendering and consume more memory.
myStupidViewPager2.offscreenPageLimit = fragments.size
I had the same problem, ViewPager2 was in ScrollView, which is under TabLayout. After navigating through the tabs, the height of the ScrollView became equal to the height of the maximum fragment height.
The problem was solved by transferring the ScrollView to the fragment.
I have found an answer to this question where i access the child through it's adapter as when you access the child at any given position rather than Zer0 index the view is null.
class ViewPager2PageChangeCallback() : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
val view = (pager.adapter as pagerAdapter).getViewAtPosition(position)
view?.let {
updatePagerHeightForChild(view, pager)
}
}
}
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)
if (pager.layoutParams.height != view.measuredHeight) {
pager.layoutParams = (pager.layoutParams)
.also { lp ->
lp.height = view.measuredHeight
}
}
}
}
pager.offscreenPageLimit = 1
pagerpager.registerOnPageChangeCallback(ViewPager2PageChangeCallback())
override fun onDestroy() {
super.onDestroy()
pager.unregisterOnPageChangeCallback(ViewPager2PageChangeCallback())
}
class OrderDetailsPager(
private val arrayList: ArrayList<Fragment>,
fragmentManger: FragmentManager,
lifecycle: Lifecycle
) : FragmentStateAdapter(fragmentManger, lifecycle) {
override fun createFragment(position: Int): Fragment {
return arrayList[position]
}
override fun getItemCount(): Int {
return arrayList.size
}
fun getViewAtPosition(position: Int): View? {
return arrayList[position].view
}
}
This worked for me after spending a lot of time, My viewpager2 is inside a Nesterscrollview
binding.viewpager.offscreenPageLimit = 2
binding.viewpager.isUserInputEnabled = false
binding.viewpager.registerOnPageChangeCallback(object :ViewPager2.OnPageChangeCallback(){
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
val myFragment = childFragmentManager.findFragmentByTag("f$position")
myFragment?.view?.let { updatePagerHeightForChild(it,binding.viewpager) }
}
})
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)
if (pager.layoutParams.height != view.measuredHeight) {
pager.layoutParams = (pager.layoutParams)
.also { lp ->
lp.height = view.measuredHeight
}
}
}
}
I have an issue with dynamic pages height which could change in the runtime. After page change, some of the content on the highest page is cut off.
I was able to fix that by re-measuring pager content (i.e page) and pager itself.
viewPager2.setPageTransformer { page, _ ->
val wMeasureSpec =
View.MeasureSpec.makeMeasureSpec(page.width, View.MeasureSpec.EXACTLY)
val hMeasureSpec =
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
page.measure(wMeasureSpec, hMeasureSpec)
measure(wMeasureSpec, hMeasureSpec)
post {
adapter?.notifyDataSetChanged()
}
}
Based on Lucas Nobile I found out another way of implementing. Instead of putting the following code on the OnResume method of each affected fragment:
override fun onResume() {
super.onResume()
binding.root.requestLayout()
}
Just add this override to the adapter of the viewPager2:
override fun onBindViewHolder(
holder: FragmentViewHolder,
position: Int,
payloads: MutableList<Any>
){
holder.itemView.requestLayout()
super.onBindViewHolder(holder, position, payloads)
}
Obs: Code made in kotlin.
For me set viewpager2's recyclerview layout params resolve this problem.
Try
<androidx.viewpager2.widget.ViewPager2
android:layout_width="match_parent"
android:layout_height="wrap_content" />
and set recyclerview layout params
RecyclerView recyclerView = (RecyclerView) viewPager.getChildAt(0);
recyclerView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
https://gist.github.com/safaorhan/1a541af729c7657426138d18b87d5bd4
this work for me;
Thanks this work for me ,when we want to use viewpager as Book ' pages. There are different kinds of page height. Sometimes , we need to scroll action when page is too long :3 At that time, just wrap your viewpager's item layout with NestedScroll View..
Thanks to #safaorhan
/**
* Disables the child attach listener so that inflated children with wrap_content heights can pass.
*
* This is very fragile and depends on the implementation details of [ViewPager2].
*
* #see ViewPager2.enforceChildFillListener (the removed listener)
*/
// call this method with your viewpager...
private fun ViewPager2.hackMatchParentCheckInViewPager() {
(getChildAt(0) as RecyclerView).clearOnChildAttachStateChangeListeners()
}
//View Pager
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/page_view_pager"
android:layout_width="match_parent"
android:layout_height="0dp">
//Your Child Viewpager item
<androidx.core.widget.NestedScrollView
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="wrap_content">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
....
</LinearLayout>
</androidx.core.widget.NestedScrollView>
I was able to do so by overriding onPageSelected callback and remeasuring the current view's height.
It would look something like this:
mViewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
#Override
public void onPageSelected(int position) {
mTabLayout.selectTab(mTabLayout.getTabAt(position));
View view = mViewPagerAdapter.getViewAtPosition(position); // this is a method i have in the adapter that returns the fragment's view, by calling fragment.getview()
if (view != null) {
view.post(() -> {
int wMeasureSpec = View.MeasureSpec.makeMeasureSpec(view.getWidth(), View.MeasureSpec.EXACTLY);
int hMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
view.measure(wMeasureSpec, hMeasureSpec);
viewPager.getLayoutParams().height = view.getMeasuredHeight();
mViewPagerAdapter.notifyDataSetChanged();
});
}
}
});
mViewPager.setOffscreenPageLimit(mTabLayout.getTabCount());
The layout of TabLayout and Viewpager2 looks the following (inside a NestedScrollView)
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="#+id/data_collection_tab_layout"
android:layout_width="match_parent"
android:layout_marginBottom="12dp"
android:layout_height="wrap_content" />
<androidx.viewpager2.widget.ViewPager2
android:id="#+id/data_collection_viewpager2"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
Also, please make sure all the Fragment's layout height it's wrap content.

constraintlayout.widget.Group animation not working with TransitionManager

does anyone has any idea why animating constraintlayout.widget.Group visibility with TransitionManager is not working? Isn't this widget made for these kind of things?
It is working if hiding or showing items after separating views from Group
<androidx.constraintlayout.widget.Group
android:id="#+id/cardHeadersGroup"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="invisible"
app:constraint_referenced_ids="cardSystemHeader,cardSimpleHeader,cardCombinedHeader"
app:layout_constraintBottom_toBottomOf="#+id/cardCombinedHeader"
app:layout_constraintEnd_toEndOf="#+id/cardSystemHeader"
app:layout_constraintStart_toStartOf="#+id/cardSimpleHeader"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible"/>
val headersGroup = binding.cardHeadersGroup
val slideIn = Slide()
slideIn.slideEdge = Gravity.BOTTOM
slideIn.mode = Slide.MODE_IN
slideIn.addTarget(headersGroup)
TransitionManager.beginDelayedTransition(binding.root as ViewGroup, slideIn)
headersGroup.visibility = VISIBLE
I've been recently working with TransitionManager and ConstraintLayout.Group and found it to be very buggy.
Eventually I decided to dump the whole ConstraintLayout.Group and created an in-code AnimationGroup (similar to the in-xml ConstraintLayout.Group):
class AnimationGroup(vararg val views: View) {
var visibility: Int = View.INVISIBLE
set(value) {
views.forEach { it.visibility = value }
field = value
}
}
and an extension function for the Transition:
private fun Transition.addTarget(animationGroup: AnimationGroup) {
animationGroup.views.forEach { viewInGroup -> this.addTarget(viewInGroup) }
}
That way you can do the following (almost exactly the same code, but simpler xml - no ConstraintLayout.Group):
val headersGroup = AnimationGroup(
binding.cardSystemHeader,
binding.cardSimpleHeader,
binding.cardCombinedHeader
)
val slideIn = Slide()
slideIn.slideEdge = Gravity.BOTTOM
slideIn.mode = Slide.MODE_IN
slideIn.addTarget(headersGroup)
TransitionManager.beginDelayedTransition(binding.root as ViewGroup, slideIn)
headersGroup.visibility = VISIBLE
We can also extract the Group's referenced views with simple extension function:
fun Group.getReferencedViews() = referencedIds.map { rootView.findViewById<View>(it) }

RecyclerView scroll up is not smooth when the height isn't fixed

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.

Set runtime margin to any view using Kotlin

I am a beginner in Kotlin .I am not too much familier with this language. I am making one example and playing with code. I Just want to set runtime margin to any view. I also trying to google it but not getting any proper solution for this task.
Requirement
Set runtime margin to any View.
Description
I have taking one xml file which is contain on Button and I want to set runtime margin to this button.
Code
I also try below thing but it's not work.
class MainActivity : AppCompatActivity() {
//private lateinit var btnClickMe: Button
//var btnClickMe=Button();
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//For setting runtime text to any view.
btnClickMe.text = "Chirag"
//For getting runtime text to any view
var str: String = btnClickMe.text as String;
//For setting runtimer drawable
btnClickMe.background=ContextCompat.getDrawable(this,R.drawable.abc_ab_share_pack_mtrl_alpha)//this.getDrawable(R.drawable.abc_ab_share_pack_mtrl_alpha)
/*
//For Setting Runtime Margine to any view.
var param:GridLayout.LayoutParams
param.setMargins(10,10,10,10);
btnClickMe.left=10;
btnClickMe.right=10;
btnClickMe.top=10;
btnClickMe.bottom=10;
*/
// Set OnClick Listener.
btnClickMe.setOnClickListener {
Toast.makeText(this,str,5000).show();
}
}
}
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:orientation="vertical"
tools:context="chirag.iblazing.com.stackoverflowapp.MainActivity"
android:layout_height="match_parent">
<Button
android:id="#+id/btnClickMe"
android:text="Click Me"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
How can I proceed?
You need to get the layoutParams object from button and cast it to ViewGroup.MarginLayoutParams (which is a parent class of LinearLayout.LayoutParams, RelativeLayout.LayoutParams and others and you don't have to check which is btnClickMe's actual parent) and set margins to whatever you want.
Check following code:
val param = btnClickMe.layoutParams as ViewGroup.MarginLayoutParams
param.setMargins(10,10,10,10)
btnClickMe.layoutParams = param // Tested!! - You need this line for the params to be applied.
This is how I would like to do in Kotlin -
fun View.margin(left: Float? = null, top: Float? = null, right: Float? = null, bottom: Float? = null) {
layoutParams<ViewGroup.MarginLayoutParams> {
left?.run { leftMargin = dpToPx(this) }
top?.run { topMargin = dpToPx(this) }
right?.run { rightMargin = dpToPx(this) }
bottom?.run { bottomMargin = dpToPx(this) }
}
}
inline fun <reified T : ViewGroup.LayoutParams> View.layoutParams(block: T.() -> Unit) {
if (layoutParams is T) block(layoutParams as T)
}
fun View.dpToPx(dp: Float): Int = context.dpToPx(dp)
fun Context.dpToPx(dp: Float): Int = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics).toInt()
now we just have to call this on a view like
textView.margin(left = 16F)
Here's a useful Kotlin extension method:
fun View.setMargins(
left: Int = this.marginLeft,
top: Int = this.marginTop,
right: Int = this.marginRight,
bottom: Int = this.marginBottom,
) {
layoutParams = (layoutParams as ViewGroup.MarginLayoutParams).apply {
setMargins(left, top, right, bottom)
}
}
Use it like this:
myView.setMargins(
top = someOtherView.height
bottom = anotherView.height
)
EDIT: the solution is similar to the answer from Hitesh, but I'm using the (original) ViewGroup.setMargins in pixels. Of course you can make your own setMarginsDp variant based on these examples, or use Hitesh's dpToPx extension before calling my implementation. Whichever solution you choose depends on your own taste.
Also take note that my solution (re)sets all margins, although this won't be an issue in most cases.
If you want to change specific margin like top or bottom you can use below code with Data binding .
#BindingAdapter("android:layout_marginTop")
#JvmStatic
fun setLayoutMarginTop(view: View, marginTop: Float) {
val layoutParams = view.layoutParams as ViewGroup.MarginLayoutParams
layoutParams.topMargin = marginTop.toInt()
view.layoutParams = layoutParams
}
and in .xml file you can write like below code
<ImageView
android:id="#+id/imageView3"
android:layout_width="#dimen/_15dp"
android:layout_height="#dimen/_15dp"
android:layout_marginTop="#{homeViewModel.getLanguage() ? #dimen/_14dp : #dimen/_32dp }"
android:contentDescription="#string/health_indicator"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/imageView1"
app:layout_constraintEnd_toStartOf="#+id/textView3"
android:src="#{ homeViewModel.remoteStatusVisible ? #drawable/green_rectangle : #drawable/gray_rectangle}"/>
Here is another sample of CardView
myCardView.elevation = 0F
myCardView.radius = 0F
val param = (myCardView.layoutParams as ViewGroup.MarginLayoutParams).apply {
setMargins(0,0,0,0)
}
myCardView.layoutParams = param

Categories

Resources