How to restore translationX of custom view on Android - android

I have a custom tab view derived from LinearLayout which inflates an xml layout.
Inside it I have a view (llIndicator) which is used as an indicator bar and I achive this with animating the translationX property of the view like this:
val offset = llIndicator.width * activeTabIndex
llIndicator.animate().translationX(offset.toFloat())
It all works fine, the only problem is when I am trying to restore the state of the view after activity is destroyed and re-created. Most of the properties does not even need to be saved because the setup code in the fragment is called again, but i would like to save the selected tab and restore the translationX property and restore it without animation.
But I cannot seem to find the correct solution. I save the selected tab to a bundle, successfully restore it, calculate the correct position, but when I set the translationX property, it does not do anything...
It does not work if I do it in the onRestoreInstanceState() method because it happens before the layout, so I tried to set an isInstanceBeingRestored variable to true in the onRestoreInstanceState() and set translationX in the onLayout() method, but with no success.
So i tried using an OnGlobalLayoutListner() as well by placing the following code in the init{ } of the view.
viewTreeObserver.addOnGlobalLayoutListener(object: ViewTreeObserver.OnGlobalLayoutListener{
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
// Restore view if needed
if(isInstanceBeingRestored){
llIndicator.translationX = offset
}
}
})
It still does nothing... offset is correctly calculated, I checked by using Log.d(), and also replacing it with by any number has no effect.
So I tried using the post() method, as following:
llIndicator.post({
llIndicator.translationX = offset
})
I tried placing it in the onLayout() method (of course checking the isInstanceBeingRestored variable first) and also to the OnGlobalLayoutListener() but no effect.
But what is interesting, if I use postDelayed() with a large enough delay, it works.
llIndicator.postDelayed({
llIndicator.translationX = offset
}, 1500)
But obviously, this solution cannot remain, I just tried it out...
So I am confused... This sounds quite a simple thing to solve but after hours of Googling and trying different codes, and scratching my head, I still couldn't solve it. Surely it cannot be so difficult, can it?
What am I doing wrong? Can anyone point me in the right direction?
Thank's very much in advance for any help.
Edit:
I found something interesting...
If I use marginStart instead of translationX as below to set the position it works. What would be the reason for that? I don't get it...
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
if(isInstanceBeingRestored){
val p = llIndicator.layoutParams as MarginLayoutParams
p.marginStart = offset.toInt()
llIndicator.layoutParams = p
//llIndicator.translationX = offset
}
}
Edit 2:
Well it only works with marginStart if the margin start was 0 before activity gets destroyed... So if I set offset to a random value, independent from its previous position, it displays the indicator at the correct position, if marginStart was 0 before...
Otherwise the indicator bar appears at the left side of the screen.
Now I am getting really lost...
I double-checked the calculations but they give the correct offset...
Best wishes,
Agoston

Related

Horizontal RecyclerView with variable item heights not wrapping properly

What I intend to achieve
The item view should occupy the entire height of the item
It could be that the item height is lesser than the height of the tallest item in the recyclerview, in which case it should just stick to the top like in the screenshot above.
The bug I'm running into
As in the screenshot above, views are getting truncated.
What I've tried so far
Initially I went with wrap_content on the recyclerview, now that it is supported. It didn't work when none of the views visible on the screen at the time were the tallest. This makes sense in how the view hierarchy is laid out. How can the height of something which hasn't even been bound to any data yet be calculated if the height is dependent on that data?
Workaround time :S
Instead of trying a custom layoutmanager, I first went with what I felt needed to be done - laying out all item views at the beginning to figure out their height.
There's a progressbar and an animation playing in the upper part of the screen to catch the user's attention while all this happens with recyclerview visibility set to invisible. I use two things, one didn't suffice - I've attached an observer in the adapter's onViewAttached() call and I've used a scroll change listener as well. There's a LinearSnapHelper attached to the recycler view to snap to adjacent (next or previous, depending on the scroll direction) position on scroll.
In this setup,
I'm going to each position in the recyclerview using layoutManager.smoothScrollToPosition()
Getting the child view height using
View currentChildView = binding.nextRv.getChildAt(layoutManager.findFirstCompletelyVisibleItemPosition());
if (currentChildView != null) {
currentChildHeight = currentChildView.getHeight();
}
in scroll change listener on RecyclerView.SCROLL_STATE_IDLE or by passing the height to the view attached observer mentioned above in the adapter's onViewAttachedToWindow()
#Override
public void onViewAttachedToWindow(BindingViewHolder holder) {
if (mObserver != null) {
mObserver.onViewAttached(holder.binding.getRoot().getHeight());
}
}
Storing a maxHeight that changes to the max of maxHeight and new child's height.
As is evident, this is ugly. Plus it doesn't give me the current view's height - onAttached means it's only just attached, not measured and laid out. It is the recycled view, not the view bound to current data item. Which presents problems like the truncation of view illustrated above.
I've also tried wrap_content height on the recycler view and invalidating from recycler's parent till the recycler and the child on scroll coming to SCROLL_STATE_IDLE. Doesn't work.
I'm not sure how a custom layoutmanager can help here.
Can someone guide me in the right direction?
I could not accept #Pradeep Kumar Kushwaha's answer because against one solution, I do not want different font sizes in the list. Consistency is a key element in design. Second alternative he gave couldn't work because with ellipsize I would need to give a "more" button of some sort for user to read the entire content and my text view is already taking a click action. Putting more some place else would again not be good design.
Changing the design with the simple compromise of resizing the recyclerview when the tallest, truncated item comes into focus, it turns into the simple use case of notifyItemChanged(). Even for the attempt I made using the view attached observer and scroll state listener, notifyItemChanged could be used but that approach is just too hacky. This I can live with in both code and design. Here goes the code required.
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
int position = ((LinearLayoutManager) binding.nextRv.getLayoutManager())
.findFirstVisibleItemPosition();
if (position != nextSnippetAdapter.getItemCount() - 1) {
binding.nextRv.getAdapter().notifyItemRangeChanged(position, 2);
} else {
binding.nextRv.getAdapter().notifyItemChanged(position);
}
}
}
For my particular setup, calling for just these two elements works. It can further be optimized so as to call for single element at position + 1 in most cases, and checking and calling for the appropriate one in corner (literal) cases.
Inside your adapter where I can find two cards one on top and another on bottom
How I would have defined my layout is like this:
Cardview1
LinearLayout1 --> orientation vertical
cardview2 (Top card where text is written)
Linearlayout2 (where I can see icons such as like etc)-->orientation horizontal
Now fix the height of Linearlayout2 by setting it to wrap content.
And the height of cardview2 should be 0dp and add weight = 1
Now inside cardview2 add a TextView1 to matchparent in height and width.
Better inside textview1 add ellipsize to end and add max lines
If you want to show all lines try to find autoresizetextview library it can be founded here --> AutoResizeTextView
Hope it helps.
I think the recyclerview can be set to height wrap_content. And the items can be make like height to match_parent.
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layput_height="wrap_content"/>
Item as:
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
// your coode
</androidx.constraintlayout.widget.ConstraintLayout>
I had little more requirement than the question. Even my problem solved in the way.
Remember I am using:
androidx.recyclerview:recyclerview:1.0.0-beta01
dependency for the project

Android: PageTransformer applying correct scale but not alpha before first touch

I have a PageTransformer that correctly displays my ViewPager with the second element correctly scaled, but there is no alpha applied to the second element. However, upon dragging the ViewPager a miniscule amount, the next view is properly displayed. The code for both my alpha and scale is identical, I'm not sure what the issue here is.
I have tried calling code from a handler once the ViewPagerAdapter has things added to it (such as viewPager.scrollBy() and pager.beginFakeDrag()) but these didn't help.
viewPager.setOffscreenPageLimit(2) is set. I have tried setting alpha to the views programmatically when instantiated in the Adapter but it has no effect. If the scale is correctly being applied then the alpha should be as well, surely.
#Override
public void transformPage(View page, float position)
{
float scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position));
float alphaFactor = Math.max(MIN_ALPHA, 1 - Math.abs(position));
page.setScaleX(scaleFactor);
page.setScaleY(scaleFactor);
page.setAlpha(alphaFactor);
}
(EDIT) CORRECT SOLUTION:
Replace ViewPager with ViewPager2.
Useful resources:
https://developer.android.com/training/animation/vp2-migration
https://stackoverflow.com/a/57694887/10504386
LAZY (AND NOT SO COOL, ERROR PRONE) SOLUTION:
Unfortunately for me, I did not have the property android:animateLayoutChanges=true set in my layout.
I faced the same problem when I moved the ViewPager from an Activity to a Fragment.
I think the problem is due to the fact I reused the fragment. Upon a FragmentTransaction, the ViewPager technically still has its old value, so its view doesn't need to change, that's why the PageTransformer isn't fired.
Basically, all you need to do is to set the ViewPager's currentItem to 0, and then set it to the value you want to.
This is my snippet (MyViewPager.kt, but you don't need to override ViewPager).
init {
post {
currentItem = 0
}
}
fun setCurrentPage(position: Int) {
post {
setCurrentItem(position, true) // I want smooth scroll
}
}
I had android:animateLayoutChanges=true set on the ViewPager. Once I removed this, it loaded correctly. Don't ask me why it was there in the first place.

Proper Way of Changing View Properties at runtime

So i have a view which is inflated via Xml. That view has a subView, which i need to set a marginTop with a dynamic value like this:
toolbar.getHeight() - 100
For this reason, i cannot set it to xml. I could do this:
?attr/actionBarSize
but i need specifically toolbar.getHeight() - 100
What is the proper way of accomplish this? I am doing it in the onCreate of the activity, i set a viewTreeObserver.addOnGlobalLayoutListener for that view, get the layoutParameters and add a margin.
Is this the right way to do this? The way i see it, the view is drawn, and when i run some code inside viewTreeObserver of that view, the view must be redrawn again. Is there a way of avoid this double rendering without setting a custom view?
Inside of onCreate() you can look up the value for this attribute in the current context:
int[] attrIds = new int[1]{ R.attr.actionBarSize };
TypedArray a = obtainStyledAttributes(attrIds);
// first argument is index in attrIds, second argument is
// a default value to return if not found
int actionBarSize = a.getDimensionPizelSize(0, 0);
a.recycle();
// do something with actionBarSize

Override animation for notifyItemChanged in RecyclerView.Adapter

Well, I have a RecyclerView with an adapter and everything works great. The items in the ArrayList dataset are being updated periodically. So the items and their elements as well as their position in the list change. This is achieved by simple sorting and manually calling these methods, whenever things happen:
// swapping two items
Collections.swap(items, i, j);
itemsAdapter.notifyItemMoved(i, j);
// adding a new one
itemAdapter.notifyItemInserted(items.size());
// when updating valus
itemAdapter.notifyItemChanged(i);
The latter of which, is the cause of my misery. Every time an item is updated, a little "blink" animation is triggered.
I found a couple of solutions for this:
// disabling all animations
recyclerView.getItemAnimator().setSupportsChangeAnimations(false);
// or
// setting the animation duration to zero,
recyclerView.getItemAnimator().setChangeDuration(0);
But both of these kill the animations when items move (being swapped). I just want to override the one animation and keep all of this magic. Is there a way of doing this? And if it's overriding ItemAnimator, does anyone have a simple example?
Thanks in advance!
There is a dedicated method to disable just item changed animations:
((SimpleItemAnimator) myRecyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
Official docs.
Yes, I did.
First, get the source code of DefaultItemAnimator. Take the code and create a class named MyItemAnimator in your project. Then, set the ItemAnimator to a new instance of your modified MyItemAnimator, like so:
recyclerView.setItemAnimator(new MyItemAnimator());
Now, go in the new classes source code and locate the method
animateChangeImpl(final ChangeInfo changeInfo) { ... }
We simply have to locate the method calls changing alpha values. Find the following two lines and remove the .alpha(0) and .alpha(1)
oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() { ... }
newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()).alpha(1).setListener(new VpaListenerAdapter() { ... }
like so
oldViewAnim.setListener(new VpaListenerAdapter() { ... }
newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()).setListener(new VpaListenerAdapter() { ... }
Try setting:
mRecyclerview.setItemAnimator(null);
Kotlin version of #Pablo A. Martínez answer:
(recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
If you are only modifying the data of Recyclerview means no addition or deletion of item then you should add this line.
mRecyclerview.setHasFixedSize(true);
So that recyclerview will get to know there is no change in size after that you can apply
((SimpleItemAnimator) mRecyclerview.getItemAnimator()).setSupportsChangeAnimations(false);
So that animation will be disappear and code will work like charm :)
Slight variation on Alex's answer.
Instead of removing the alpha change altogether, I just reduced it. the only changes needed were:
//changed alpha from 0
ViewCompat.setAlpha(newHolder.itemView, 0.5f);
and
//changed alpha from 0
oldViewAnim.alpha(0.5f).setListener(new VpaListenerAdapter() {
In both cases, change the 0 to 0.5
This has the effect of removing the flicker associated with the alpha going completely to 0, but keeps the morphing qualities of the item change animation.
In my case the recyclerview took all the space below toolbar. All I did was changing the layout_height of recyclerview from wrap_content to match_parent and blinking disappeared.

Dynamically Setting RelativeLayout.LayoutParams.addRule

I have this issue where I have a relative layout that has two child relative layouts (leftpanel and rightpanel). They're inside a custom layout for listview items and each item is updated from a json response from the server. So the size depends on what the server provides.
Issue: I want to have each panel's height to match each other, but it seems that setting layout_height to match_parent doesn't work (actually, if this can be resolved, then no more problems).
What I did: I programmatically set the align top and bottom of each panel to each other -- if the other's bigger, adjust the other one and vice versa. So what I did was to have a view (rightpanel) to listen to rightPanel.getViewTreeObserver().addOnScrollChangedListener(), and call the method below everytime there's a scroll change:
private void updateLayoutAlignmentParams(ViewHolder viewHolder) {
RelativeLayout.LayoutParams leftPanelLayoutParams = (RelativeLayout.LayoutParams)viewHolder.leftPanel.getLayoutParams();
RelativeLayout.LayoutParams rightPanelLayoutParams = (RelativeLayout.LayoutParams)viewHolder.rightPanel.getLayoutParams();
int leftPanelHeight = viewHolder.leftPanel.getHeight();
int rightPanelHeight = viewHolder.rightPanel.getHeight();
if(leftPanelHeight > rightPanelHeight) {
rightPanelLayoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, 0);
rightPanelLayoutParams.addRule(RelativeLayout.ALIGN_TOP, 0);
leftPanelLayoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, viewHolder.rightPanel.getId());
leftPanelLayoutParams.addRule(RelativeLayout.ALIGN_TOP, viewHolder.rightPanel.getId());
} else {
leftPanelLayoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, 0);
leftPanelLayoutParams.addRule(RelativeLayout.ALIGN_TOP, 0);
rightPanelLayoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, viewHolder.leftPanel.getId());
rightPanelLayoutParams.addRule(RelativeLayout.ALIGN_TOP, viewHolder.leftPanel.getId());
}
}
What happens: not all the views get updated while scrolling; so I get a lop-sided listview item where one is bigger than the other vertically but some do adjust well. Odd thing is, when the item gets out of view, it's lop-sided, then gets corrected consistently.
Note: I also tried
addOnDrawListener() - every item is updated but I get an ArrayList out of bounds index but doesn't point to any line in my code. Wouldn't be the best solution anyway as I need to support devices with API < 16.
setOnGlobalLayoutListener() - Nothing happens.
Please let me know if you know why or have a better solution.
Finally [kindof] fixed it! Replaced the whole method with the code below:
private void updateLayoutAlignmentParams(ViewHolder viewHolder) {
viewHolder.rightPanel.setMinimumHeight(viewHolder.leftPanel.getHeight());
viewHolder.leftPanel.setMinimumHeight(viewHolder.rightPanel.getHeight());
}
Although, I was able to achieve having the left and right panel aligned with each other using the code above. I'm now having issues where the previous view's height and width are retained when I switch views. :(
Edit:
Okay, I ended up using LinearLayout to wrap the whole listview item. Not really sure why RelativeLayout isn't complying with match_parent, though.

Categories

Resources