I have a very wide image inside my ViewHolder. The default ImageView behavior using centerCrop is should the image like this -
But I want to crop it so it will focus on the left side like this -
Things that needs to take into consideration are the fact that this ViewHolder is being resized very often.
Here is my ViewHolder XML & Adapter. Please note that I used fitXY just for display purpose, this is not what I actually want so feel free to ignore that line. -
<?xml version="1.0" encoding="utf-8"?>
<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:id="#+id/vendors_row_item_root_layout"
android:layout_width="152dp"
android:layout_height="match_parent">
<androidx.cardview.widget.CardView
android:id="#+id/search_image_contact_cardview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="5dp"
app:cardCornerRadius="8dp"
tools:layout_height="160dp">
<ImageView
android:id="#+id/vendors_row_item_imageview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
tools:src="#drawable/kikiandggcover" />
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
Adapter -
class VendorAdapter(private val miniVendorModels: List<MiniVendorModel>, private val context: Context) : RecyclerView.Adapter<VendorsHolder>() {
companion object {
const val EXTRA_VENDOR_MODEL = "EVM"
}
private val vendorsHoldersList = mutableListOf<VendorsHolder>()
override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): VendorsHolder {
val view = LayoutInflater.from(viewGroup.context).inflate(R.layout.fragment_marketplace_vendor_row_item, viewGroup, false)
val vendorsHolder = VendorsHolder(view)
vendorsHoldersList.add(vendorsHolder)
vendorsHolder.rootLayout.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
vendorsHolder.rootLayout.updateLayoutParams {
this.width = 250 + bottom
}
}
vendorsHolder.rootLayout.pivotX = 0f
vendorsHolder.rootLayout.pivotY = vendorsHolder.rootLayout.measuredHeight.toFloat()
return vendorsHolder
}
override fun onBindViewHolder(vendorsHolder: VendorsHolder, i: Int) {
val model = miniVendorModels[i]
Picasso.get().load(model.bannerPicture).into(vendorsHolder.vendorImageView)
vendorsHolder.vendorImageView.setOnClickListener { v: View? ->
try {
val intent = Intent(context, VendorPageActivity::class.java)
intent.putExtra(EXTRA_VENDOR_MODEL, model)
context.startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()
Toast.makeText(context, ResourceHelper.getString(R.string.marketplace_vendor_unavailable), Toast.LENGTH_SHORT).show()
}
}
}
override fun getItemCount(): Int = miniVendorModels.size
}
This should be a really simple thing but I am strugling with this one for HOURS on-end.
Has anyone an idea?
Related
I have a VideoView (circle) and custom animation (Falling cards) on the screen, in Fragment, as in the screenshot:
I haven't figured out how to do it any other way, so I've inserted four CardViews, and I'm scrolling through them using AnimationUtils. In code it looks like this:
onViewCreated()
viewLifecycleOwner.lifecycleScope.launch {
// withContext(Dispatchers.Main) {
startAnimation()
// }
}
private var count = 0
private var cardsCount = 0
private var animTime = 0L
private suspend fun startAnimation() {
cardsCount = Utils.getScannerTime3(appsList.size).first
repeatCount = Utils.getScannerTime3(appsList.size).third
val cardsList = arrayListOf(binding.cardAnim, binding.cardAnim2, binding.cardAnim3, binding.cardAnim4)
repeat(repeatCount) {
cardsList.forEach {
if (count < cardsCount) {
cardAnimation(it.animCardView, it.appsName, it.appsIcon, appsList[count])
delay(300)
}
count++
}
}
}
private suspend fun cardAnimation(cardView: CardView, textView: TextView, imageView: ImageView, appInfo: AppInfo) {
val scaleUp = AnimationUtils.loadAnimation(requireActivity(), R.anim.cardview_loader_animation_best)
cardView.startAnimation(scaleUp)
scaleUp.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(p0: Animation?) {
viewLifecycleOwner.lifecycleScope.launch {
withContext(Dispatchers.Main) {
textView.text = Utils.refactorLongAppName(appInfo.appName)
imageView.setImageDrawable(appInfo.appIcon)
// lifecycleScope.launch {
for (i in 0..11) {
cardView.elevation = i.toFloat()
delay(i * 100L)
}
// }
}
}
}
override fun onAnimationEnd(p0: Animation?) {
}
override fun onAnimationRepeat(p0: Animation?) {
}
})
}
I launch VideoView in the standard way in onResume
fun startVideoAnim(view: VideoView, context: Context, resourcesRawId: Int) {
view.setVideoURI(Uri.parse("android.resource://" + context.packageName + "/" + resourcesRawId))
view.setOnPreparedListener {
it.isLooping = true
}
view.requestFocus()
view.start()
}
CardView:
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/animCardView"
style="#style/Widget.CardView"
android:layout_width="match_parent"
android:layout_height="54dp"
android:layout_marginHorizontal="24dp"
android:layout_marginVertical="3dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<ImageView
android:id="#+id/appsIcon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginVertical="7dp"
android:layout_marginStart="7dp" />
<TextView
android:id="#+id/appsName"
style="#style/TextStyleBold700"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center|start"
android:layout_marginStart="55dp"
android:letterSpacing="0.025"
android:textColor="#color/text_dark"
android:textSize="16sp" />
</androidx.cardview.widget.CardView>
The problem is that the animation does not work as it should, I know that the animation is correct, because if I remove the VideoView completely, everything works as it should. If you simply do not show the video, the animation does not work as it should. I tried to replace VideoView with ExoPlayer and Lottie, although this does not suit me. Unsuccessfully. Please tell me what could be wrong and how to fix it? And if there are tips, ideas on how to make such an animation differently, write too.
I want to animate TextView width when I change visibility of TextView. I don't wanna achieve generic "fade in/out" effect, but I wanna collapse TextView from sides to 0 width.
Here are my functions:
fun fadeInTextViewSize(){
val parentWidth = (buttonText.parent as View).measuredWidth
val widthAnimator = ValueAnimator.ofInt(buttonText.width, parentWidth)
widthAnimator.duration = 500
widthAnimator.interpolator = DecelerateInterpolator()
widthAnimator.addUpdateListener { animation ->
buttonText.layoutParams.width = animation.animatedValue as Int
buttonText.requestLayout()
}
widthAnimator.start()
}
fun fadeOutTextViewSize(){
val widthAnimator = ValueAnimator.ofInt(buttonText.width, 0)
widthAnimator.duration = 500
widthAnimator.interpolator = DecelerateInterpolator()
widthAnimator.addUpdateListener { animation ->
buttonText.layoutParams.width = animation.animatedValue as Int
buttonText.requestLayout()
}
widthAnimator.start()
}
Issue is that with this function, my TextView height is for some reason set to MATCH_PARENT.
I assume that you need the TextView to appear as if erased from both ends equally. Here is a technique that will do that:
private lateinit var buttonText: TextView
private var viewWidth = 0
fun fadeOutTextViewSize() {
with(buttonText) {
// Enable scrolling for the view since we will need to scroll horizontally to center text.
setMovementMethod(ScrollingMovementMethod())
setHorizontallyScrolling(true)
// Lock in the starting width and height.
viewWidth = buttonText.width
layoutParams.width = buttonText.width
layoutParams.height = buttonText.height
visibility = View.VISIBLE
}
with(ValueAnimator.ofInt(viewWidth, 0)) {
duration = 1000
interpolator = DecelerateInterpolator()
addUpdateListener { animation ->
val newWidth = animation.animatedValue as Int
buttonText.layoutParams.width = newWidth
// Shift text left so it stays centered in the initial bounds.
val scrollX = (viewWidth - newWidth) / 2
buttonText.scrollTo(scrollX, 0)
buttonText.requestLayout()
}
doOnEnd {
// Make the view invisible to seal its disappeared status. This could also be
// "GONE" depending on the desired effect.
buttonText.visibility = View.INVISIBLE
}
start()
}
}
The technique is to lock in the size and height of the TextView to its initial size. From that initial size, the animation shrinks the view's width from the initial size to zero. Since the layout will position the start of the text to the start of the view, the text is scrolled left to maintain its position on the screen as the width shrinks.
The red line is there just to mark the center of the TextView and is not needed.
Causing the view to reappear is mostly the opposite of the code here.
The test layout:
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/holo_blue_dark"
tools:context=".MainActivity">
<TextView
android:id="#+id/buttonText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#android:color/holo_blue_light"
android:text="Hello World!"
android:textSize="48sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:layout_width="1dp"
android:layout_height="0dp"
android:background="#android:color/holo_red_light"
app:layout_constraintBottom_toBottomOf="#id/buttonText"
app:layout_constraintEnd_toEndOf="#id/buttonText"
app:layout_constraintStart_toStartOf="#id/buttonText"
app:layout_constraintTop_toTopOf="#id/buttonText" />
<Button
android:id="#+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Click Here"
app:layout_constraintEnd_toEndOf="#+id/buttonText"
app:layout_constraintStart_toStartOf="#id/buttonText"
app:layout_constraintTop_toBottomOf="#id/buttonText" />
</androidx.constraintlayout.widget.ConstraintLayout>
There is another way to accomplish this which is to use a custom TextView that will clip its canvas to make the view shrink and expand. This method has the following advantages:
It is probably more efficient in that it does not require additional layout of the TextView.
If the TextView is within a ConstraintLayout and there are other views that are constrained to the start and/or end of the TextView, those views will not shift since the boundaries of the TextView remain unchanged.
private lateinit var buttonText: ClippedTextView
fun fadeOutTextViewSize() {
with(ValueAnimator.ofInt(buttonText.width, 0)) {
duration = 1000
interpolator = DecelerateInterpolator()
addUpdateListener { animation ->
val newWidth = animation.animatedValue as Int
buttonText.setClippedWidth(newWidth)
buttonText.invalidate()
}
start()
}
}
ClippedTextView.kt
class ClippedTextView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : androidx.appcompat.widget.AppCompatTextView(context, attrs) {
private var mClipWidth = 0
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
mClipWidth = right - left
}
override fun draw(canvas: Canvas) {
val sideClipWidth = (width - mClipWidth) / 2
canvas.withClip(sideClipWidth, 0, width - sideClipWidth, height) {
super.draw(this)
}
}
fun setClippedWidth(clipWidth: Int) {
mClipWidth = clipWidth
}
}
We can also bring all the logic into the custom TextView as follows:
ClippedTextView.kt
class ClippedTextView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : androidx.appcompat.widget.AppCompatTextView(context, attrs) {
private var mClipWidth = 0
private val mAnimator: ValueAnimator by lazy {
ValueAnimator().apply {
duration = 1000
interpolator = DecelerateInterpolator()
addUpdateListener { animation ->
setClippedWidth(animation.animatedValue as Int)
invalidate()
}
}
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
setClippedWidth(right - left)
}
override fun draw(canvas: Canvas) {
val sideClipWidth = (width - mClipWidth) / 2
canvas.withClip(sideClipWidth, 0, width - sideClipWidth, height) {
super.draw(this)
}
}
fun expandView() {
doWidthAnimation(mClipWidth, 0)
}
fun shrinkView() {
doWidthAnimation(mClipWidth, width)
}
private fun doWidthAnimation(startWidth: Int, endWidth: Int) {
animation?.cancel()
with(mAnimator) {
setIntValues(startWidth, endWidth)
start()
}
}
private fun setClippedWidth(clipWidth: Int) {
mClipWidth = clipWidth
}
}
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 develop a simple table-app, and the problem is, that the switch-box will change the state or doesn't reset the state correcly on change rotation.
befor rotating it looks like
this
after rotating it looks like
this
The hint state will set correct on change but the box is not displayed correctly. What has gone wrong? Actually the views should be updated on any changes in the same way...
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="wrap_content"
android:layout_height="wrap_content">
<Switch
android:id="#+id/tv_outputItemBoolean"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:trackTint="#color/switch_track_selector"
android:thumbTint="#color/switch_thumb_selector"
android:clickable="true"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:paddingBottom="2.5dp"
android:paddingTop="2.5dp"
android:textSize="20sp"
android:gravity="center"
/>
</LinearLayout>
fun getView(position: Int, rowView: TableRow?, parent: TableLayout): TableRow {
val rowView = TableRow(parent!!.context)
val rowData = this.getItem(position)
for (i in 0..columnCounts - 1) {
var data: ColumnData<*>? = rowData[i]
rowView.addView(this.getColumnView(data, null, rowView))
}
return rowView
}
private fun getColumnView(
col: ColumnData<*>?,
resycleView: View?,
parent: ViewGroup
): View {
return when (col?.type) {
DataType.TimeStamp -> {
val view = TimeStampPicker.View(parent.context)
view.timeStampPicker.timeStampInMillis = col.data as Long
view
}
DataType.Boolean ->
{
val c= parent.context
val view =
LayoutInflater.from(c)
.inflate(R.layout.output_item_boolean, null)
val switch: Switch = view.findViewById(R.id.tv_outputItemBoolean)
val value = col.data as Boolean
var state = switch.isChecked
Log.v("XXXXXXXXXXXXXXXX", value.toString()+","+state.toString())
switch.isChecked=value
state = switch.isChecked
Log.v("XXXXXXXXXXXXXXXX", value.toString()+","+state.toString())
switch.hint=c.getText(DataType.stringResourceOf(value))
view
}
else -> {
val view =
LayoutInflater.from(parent.context)
.inflate(R.layout.ouput_item_simple_text, null)
val tv: TextView = view.findViewById(R.id.tv_outputItemSimpleText)
tv.text = if (col != null) col.data.toString() else "NULL"
view
}
}
}
the output log print shows the right state befor and after setting.
V/XXXXXXXXXXXXXXXX: false,false
V/XXXXXXXXXXXXXXXX: false,false
V/XXXXXXXXXXXXXXXX: true,false
V/XXXXXXXXXXXXXXXX: true,true
V/XXXXXXXXXXXXXXXX: true,false
V/XXXXXXXXXXXXXXXX: true,true
V/XXXXXXXXXXXXXXXX: false,false
V/XXXXXXXXXXXXXXXX: false,false
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.