Recycler item_view height is changing after first initialisation in android - android

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.

Related

Custom ViewGroup for behaving like RecyclerView: TextView not displaying Text in RelativeLayout but show Text in LinearLayout

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.

Animate single item in RecyclerView on data change

I have complex and generic RecyclerView design and List Adapter.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
val layoutInflater: LayoutInflater = LayoutInflater.from(parent.context)
val binding: ViewDataBinding =
DataBindingUtil.inflate(layoutInflater, viewType, parent, false)
return object : BaseViewHolder(binding) {
override fun bindData(position: Int) {
val model = getItem(position).data
itemBinding.setVariable(BR.model, model)
viewModel?.let {
itemBinding.setVariable(BR.viewModel, it)
}
}
}
}
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
holder.run {
bindData(position)
if (itemBinding.hasPendingBindings()) {
itemBinding.executePendingBindings()
}
}
}
It has RecyclerView inside RecyclerView as item and handle multi layout by itself. I update list and items with databinding adapters. When I need to update single item; I search all tree in LiveData list, modify value and post value updated list to LiveData again.
I want to update each view with animation(item inside of RecyclerView inside of RecyclerView) when it's value changed.
here is my update code;
#BindingAdapter("setTransactionBgAnimation")
fun View.setTransactionBgAnimation(ratio: Double?) {
ratio?.let { value ->
val colorAnim = ObjectAnimator.ofInt(
this, "backgroundColor", getEvaluateColor(context, value), Color.WHITE
)
colorAnim.duration = 500
colorAnim.repeatCount = 1
colorAnim.start()
val alphaAnim = ObjectAnimator.ofFloat(
this, "alpha", 0.40f, 0.0f
)
alphaAnim.duration = 500
alphaAnim.repeatCount = 1
alphaAnim.start()
}
}
When value updated; it has called from all views for each change.
I tried to give unique tag to view and check tag in binding adapter but it is not worked for me.
I solve the problem with not -so clean- way.
First of all; animation was called for every visible item's count for each row, I fix it by controlling with giving view tag with changing value and check that tag that is same with new value.
After first fix, only really changed item animated but it animates multiple times. It was causing because of ObjectAnimator's backgroundColor animations. I have no idea why did I even change backgroundColor with animation. I remove it and multiple flickering animation fixed too.
For better understanding please see my code part
fun View.setTransactionBgAnimation(ratio: Double?) {
if (tag != ratio.toString()) {
ratio?.let { value ->
setBackgroundColor(getEvaluateColor(context, value))
val alphaAnim = ObjectAnimator.ofFloat(
this, "alpha", 0.40f, 0.0f
)
alphaAnim.duration = 500
alphaAnim.start()
}
tag = ratio.toString()
}
}

Android TextView inside ListView does not measure the correct height until manually scrolling

I have a listView filled with multi-line TextViews. Each TextView has a different amount of text. After pressing a button, the user is taken to another Activity where they can change the font and the font size. Upon reEntry into the Fragment, if these settings have changed, the listView is reset and the measurements of the TextViews are changed.
I need to know the measured height of the first TextView in view after these settings have changed. For some reason, the measured height is different at first after it is measured. Once I manually scroll the list, the real height measurement is recorded.
Log output:
After measured: tv height = 2036
After measured: tv height = 2036
After scroll: tv height = 7950
Minimal Code:
class FragmentRead : Fragment() {
private var firstVisiblePos = 0
lateinit var adapterRead: AdapterRead
lateinit var lvTextList: ListView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
lvTextList = view.findViewById(R.id.read_listview)
setListView(lvTextList)
lvTextList.setOnScrollListener(object : AbsListView.OnScrollListener {
var offset = 0
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
if(scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
offset = if(lvTextList.getChildAt(0) == null) 0 else lvTextList.getChildAt(0).top - lvTextList.paddingTop
println("After scroll: tv height = ${lvTextList[0].height}")
}
}
override fun onScroll(view: AbsListView, firstVisibleItem: Int, visibleItemCount: Int, totalItemCount: Int) {
firstVisiblePos = firstVisibleItem
}
})
}
/*=======================================================================================================*/
fun setListView(lv: ListView) {
adapterRead = AdapterRead(Data.getTextList(), context!!)
lv.apply {this.adapter = adapterRead}
}
/*=======================================================================================================*/
inline fun <T : View> T.afterMeasured(crossinline f: T.() -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if(measuredWidth > 0 && measuredHeight > 0) {
println("After measured: tv height = ${lvTextList[0].height}")
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
}
})
}
/*=======================================================================================================*/
override fun onStart() {
if(Settings.settingsChanged) {
setListView(lvTextList)
lvTextList.afterMeasured {
println("After measured: tv height = ${lvTextList[0].height}")
}
}
}
}
What I have tried:
I have tried setting a TextView with the text and layoutParams and reading the height as explained here (Getting height of text view before rendering to layout) but the results are the same. The measured height is much less than after I scroll the list.
I have also tried to programatically scroll the list using lvTextList.scrollBy(0,1) in order to trigger the scroll listener or whatever else is triggered when the correct height is read.
EDIT: I put a delay in after coming back to the Fragment:
Handler().postDelayed({
println("tv height after delay = ${lvScriptureList[0].height}")}, 1000)
And this reports the correct height. So my guess is that the OnGlobalLayoutListener is being called to early. Any way to fix this?
Here is my solution. The reason I need to know the height of the TextView is because after the user changes settings (e.g. font, font size, line spacing) the size of the TextView changes. In order to return to the same spot the TextView was in previously, I need to know the height of the newly measured TextView. Then I can go to the same spot (or very close) based on the position previously and recalculating it based on the new height.
So after the settings are changed and the Fragment is loaded back up:
override fun onStart(){
if(Settings.settingsChanged) {
setListView(lvTextList)
lvTextList.afterMeasured {
lvTextList.post { lvTextList.setSelectionFromTop(readPos, 0) }
Handler().postDelayed({
val newOffset = getNewOffset() // Recalculates the new offset based on the last offset and the new TextView height
lvTextList.post { lvTextList.setSelectionFromTop(readPos, newOffset) }
}, 500)
}
}
}
For some reason I had to scroll to a position first before scheduling the delay so I simply just scrolled to the beginning of the TextView.
The 500ms is goofy and is just an estimate but it works. It actually works with a value of 100ms on my phone but I want to ensure a better chance of success across devices.

Android recycler view position occurs index error

I tried to control recycler view item visibility when i click button but it does not work
I'm using databinding in xml
this is error message
java.lang.IndexOutOfBoundsException: Index: 5, Size: 4
In my code, recycler view item has a constraintLayout and button
And constraintLayout has recycler view
I want to show constraintLayout of item that has clicked button and hide other item's constraintLayout
The way that i tried to resolve this problem is to use previous position
When button is clicked, hide previous position's item and show current position's item
the code below is what i tried
this is clickListener in activity code
answerAdapter.onItemClickListener = object : QnaDetailAdapter.OnItemClickListener {
override fun onClick(
view: View,
position: Int,
holder: QnaDetailAdapter.AnswerHolder
) {
if (prePosition != -1)
binding.recyclerViewAnswer[prePosition].comment_holder.visibility = View.GONE
if (binding.recyclerViewAnswer[position].comment_holder.visibility == View.GONE) {
binding.recyclerViewAnswer[position].comment_holder.visibility = View.VISIBLE
prePosition = position
} else {
binding.recyclerViewAnswer[position-1].comment_holder.visibility = View.GONE
prePosition = -1
}
}
}
And this is adapter's onBindViewHodler
override fun onBindViewHolder(holder: AnswerHolder, position: Int) {
if (onItemClickListener != null) {
holder.btnComment.setOnClickListener { v ->
onItemClickListener?.onClick(v, position, holder)
}
}
holder.layout.recycler_view_comment.layoutManager = LinearLayoutManager(context)
holder.layout.recycler_view_comment.setHasFixedSize(true)
holder.layout.recycler_view_comment.adapter = adapter
val item = answerList[position]
holder.bind(item)
}
And this is AnswerHodler class
class AnswerHolder(private val binding: QnaDetailItemBinding) :
RecyclerView.ViewHolder(binding.root) {
val btnComment: Button = binding.btnComment
val layout: ConstraintLayout = binding.commentHolder
fun bind(item: Answer) {
binding.item = item
}
}
binding.comment is constraintLayout i touched on
thank for your help
Rather thaan usingposition, use holder.getAdapterPosition()

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.

Categories

Resources