Override animation for notifyItemChanged in RecyclerView.Adapter - android

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.

Related

How to restore translationX of custom view on 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

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.

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.

animateLayoutChanges does not work well with nested layout?

I have a nested layout like the following:
<LinearLayout> <!----Parent layout--->
<LinearLayout> <!-----child 1--->
...
</LinearLayout> <!----child 1 ended--->
<LinearLayout> <!-----child 2--->
...
</LinearLayout> <!----child 2 ended--->
</LinearLayout> <!----Parent endded--->
The problem I am having now is that since all my data items are within child 1 or child 2 Linearlayout, if I add or delete a item the child linearlayout will animated with the effect of animateLayoutChanges but the parent layout will not do any animation. (I have android:animateLayoutChanges set to true for all linear layouts). Especially when I delete an item within child 1 the animation effect becomes weird (basically child 2 will jump up while child 1 is still doing its animation).
Does anybody have any idea how to solve this?
Thanks
UPDATE
Shortly after I posted this question, I found this on android developer's site in the LayoutTransition API.
Using LayoutTransition at multiple levels of a nested view hierarchy may not work due to the interrelationship of the various levels of layout.
So does anyone have any work around suggestions for this issue?
The animateLayoutChanges property makes use of LayoutTransitions, which animate both the layout's children and, from Android 4.0 onward, ancestors in the layout hierarchy all the way to the top of the tree. In Honeycomb, only the layout's children will be animated. See this Android Developers Blog post for details.
Unfortunately, it seems that there's currently no simple way to have the siblings of a layout react to its LayoutTransitions. You could try using a TransitionListener to get notified when the layout's bounds are being changed, and move the sibling views accordingly using Animators. See Chet Haase's second answer in this Google+ post.
EDIT - Turns out there is a way. In Android 4.1+ (API level 16+) you can use a layout transition type CHANGING, which is disabled by default. To enable it in code:
ViewGroup layout = (ViewGroup) findViewById(R.id.yourLayout);
LayoutTransition layoutTransition = layout.getLayoutTransition();
layoutTransition.enableTransitionType(LayoutTransition.CHANGING);
So, in your example, to have child 2 layout animated, you'd need to enable the CHANGING layout transformation for it. The transformation would then be applied when there is a change in the bounds of its parent.
See this DevBytes video for more details.
Ok, after digesting the first answer, I make it simple here, for those who don't get proper animation result when using android:animateLayoutChanges="true" in NESTED layout:
Make sure you add android:animateLayoutChanges="true" to the will-be-resized ViewGroup (LinearLayout/RelativeLayout/FrameLayout/CoordinatorLayout).
Use setVisibility() to control the visibility of your target View.
Listen carefully from here, add android:animateLayoutChanges="true" to the outer ViewGroup of your will-be-resized ViewGroup, this outer ViewGroup must be the one who wraps all the position-changing View affected by the animation.
Add following code in your Activity before the setVisibility(), here the rootLinearLayout is the outer ViewGroup I mentioned above:
LayoutTransition layoutTransition = rootLinearLayout.getLayoutTransition();
layoutTransition.enableTransitionType(LayoutTransition.CHANGING);
Before:
After:
Reminder: If you miss the 3rd step, you will get null pointer exception.
Good luck!
As a Kotlin Extension
fun ViewGroup.forceLayoutChanges() {
layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
}
Usage
someContainer.forceLayoutChanges()
Notes:
In my experience, this happens when the container is a deep nested layout. For some reason android:animateLayoutChanges="true" just doesn't work, but using this function will force it to work.
We had added the android:animateLayoutChanges attribute to our LinearLayout but the change didn’t trigger an animation. To fix that, use this code:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
((ViewGroup) findViewById(R.id.llRoot)).getLayoutTransition()
.enableTransitionType(LayoutTransition.CHANGING);
}
More details.
It seems that a delayed transition on the parent also works for animating. At least for me the following code gives a proper expand/collapse animation.
expandTrigger.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
TransitionManager.beginDelayedTransition(parentLayout);
expanded = !expanded;
child1.setVisibility(expanded ? View.VISIBLE : View.GONE);
}
});
For deeply nested layouts you sometimes might need to use a parent higher up in the hierarchy in the call to the TransitionManager.
I had a similar issue:
I was using a animatelayoutchanges in one of my activity with a recycler view, I also added some custom layout transition because I wanted to increase speed of the animation while an item disappears in the list. It was working fine when it was not in a nested layout.
I had used the same adapter for another recyclerview which was in a nested layout. It was not working and I tried all the above solutions, None worked for me.
The real reason was, I forgot to set
mTicketsListAdapter.setHasStableIds(true);
in the nested layout activity. And after setting setHasStableIds to true, the animations was working perfectly in the nested layout.

How to scroll ListView to the bottom?

I though this would be a simple task, but apparently there is no way to scroll listview to the bottom. All solutions that I found are using variations of setSelection(lastItem) method which only sets selection to last item, but does not scrolls to the bottom of it.
In my case I have a empty listview (with a long empty view header set) and I want to scroll to bottom of it.
So, is there a way to do it?
Edit:
So for those who are interested the working solution is:
getListView().setSelectionFromTop(0, -mHeader.getHeight());
and
getListView().scrollTo(mOffset)
This also works, with right offset (calculated based on current scroll position), but might give you some unexpected results.
If you want the list view to be scrolled always at the bottom even when you are updating the list view dynamically then you can add these attributes in list view itself.
android:stackFromBottom="true"
android:transcriptMode="alwaysScroll"
use the following
lv.setSelection(adapter.getCount() - 1);
If you would like to have a scroll animation programmatically you could easily scroll to the bottom of the list using:
listView.smoothScrollToPosition(adapter.getCount());
scrolling to the top most of the list can be achieved using:
listView.smoothScrollToPosition(0);
Try this one.. it will solve your problem, i tried it and it works great.
listView.post(new Runnable(){
public void run() {
listView.setSelection(listView.getCount() - 1);
}});
private void scrollMyListViewToBottom() {
myListView.post(new Runnable() {
#Override
public void run() {
// Select the last row so it will scroll into view...
myListView.setSelection(myListAdapter.getCount() - 1);
}
});
}
Its working for me
Use following and try.
listview.setSelection(adapter.getCount()-1);
You might want to try myListView.smoothScrollToPosition(myListView.getCount() - 1). This way, you're not forced into selecting the last row. Plus, you get some smooth, beautiful scrolling! (:
I Had the same problem, This works best for me
listView.setSelection(listView.getAdapter().getCount()-1);
if there is no adapter set to your ListView then it has no children at all, empty view is not a child of your ListView
Try using list.scrollTo(0, list.getHeight());

Categories

Resources