📜 ⬆️ ⬇️

Adding animation to the ListView

Greetings, colleagues,

Today I came to you with a short article on adding animations to the ListView when scrolling. Not long ago, I wanted to add to my list an animation similar to the one that can be seen in the G + client, but a bit different.

And I wanted to make it so that the new elements do not just appear at the bottom, but come up below and a little to the right. In general, I did it, but later, I looked at the report of Roman Guy and Chet Haas on Google IO 2013 and got an idea to add distortion while adding realism. This required a slightly different approach, but, in general, the concept remained the same.
')
Let us now, in order, I will talk about what was, how it has changed, and, actually, how it all works.

To make it clear, in general, what I mean, below is the link to the video with the final animation. Notice how the elements are deformed when they appear. Since the video was recorded from the emulator, there is a slight tugging, everything is perfectly smooth on the device. Also for clarity, I increased the duration of the animation to 900ms. Usually you want it to last 300 ms.





Simple movement



Since we want the elements to “pop up” when they appear at the bottom or at the top of the list, the most logical place is to add code to the getView of our adapter.

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { animatePostHc(position, v); } else { animatePreHc(position, v); } 


Immediately, I note that I have the main focus on ICS +, so I will continue to talk mainly about it.

Let's take a look at the animatePostHc method;

 @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) private void animatePostHc(int position, View v) { if (prevPosition < position) { v.setTranslationX(animX); v.setTranslationY(animY); } else { v.setTranslationX(-animX); v.setTranslationY(-animY); } v.animate().translationY(0).translationX(0).setDuration(300) .setListener(new InnerAnimatorListener(v)).start(); } 


Step by step. We determine the direction in which our list is moving and make an appropriate shift. Next, using the new animation API, we say that we want to move to (0, 0) in 300 ms.

We also hang a handler that does the following:

 static class InnerAnimatorListener implements AnimatorListener { private View v; private int layerType; public InnerAnimatorListener(View v) { this.v = v; } @Override public void onAnimationStart(Animator animation) { layerType = v.getLayerType(); v.setLayerType(View.LAYER_TYPE_HARDWARE, null); } @Override public void onAnimationRepeat(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { v.setLayerType(layerType, null); } @Override public void onAnimationCancel(Animator animation) { } } 


Since we want our animation to be smooth and good, it's best to set our element to HARDWARE LAYER while it is animating. In this case, we create a whole layer in which our component is rendered as a single texture (this can be seen by turning on, for example, the hardwarew overdraw debug mode), which greatly accelerates rendering.

In fact, starting with Jelly Bean, exactly the same thing can be done much easier, namely, by calling the animator's method withLayer ():

 v.animate().withLayer().translationY(0).translationX(0).setDuration(300).setListener(new InnerAnimatorListener(v)).start(); 


But we do not live in a perfect world.

We check - yes, it works. But ... views are always animated, even when just opening the activation. Let's limit the appearance of animations only to the time when we really scroll our ListView.

For this, I added the animate boolean field to my adapter. Now we just need to hang the handler on the ListView and enable / disable animations:

 listView.setOnScrollListener(new OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { adapter.setAnimate(scrollState == SCROLL_STATE_FLING || SCROLL_STATE_TOUCH_SCROLL == scrollState); } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { } }); 


We try. Now is better. However, there is a defect. If you let the list scroll quickly, the animation looks ugly, because the animated components simply do not have time. It is logical in this case simply to not show the animation when the list scrolls faster than what is permitted. So I did, but I'll write about it a little later, because first I want to tell you about adding deformations to the animation, and then about the elimination of defects.

Distortion


As I said, the addition of this was inspired by this lecture on Google I / O 2013. In general, I believe that every material (video, blog post, etc.) from Roman Guy is absolutely priceless.

In order to add a slight distortion of the list item, we need to create a custom layout. Do not worry, I'm not saying that we need to create it from scratch, we just need to expand the existing one. In my example, each element of the list is RelativeLayout, so I expanded it by creating the SkewingRelativeLayout class:

 public class SkewingRelativeLayout extends RelativeLayout { private float skewX = 0; public SkewingRelativeLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public SkewingRelativeLayout(Context context, AttributeSet attrs) { super(context, attrs); } public SkewingRelativeLayout(Context context) { super(context); } @Override public void draw(Canvas canvas) { if (skewX != 0) { canvas.skew(skewX, 0); } super.draw(canvas); } public void setSkewX(float skewX) { this.skewX = skewX; ViewCompat.postInvalidateOnLayout(this); } } 


We have added a skew - distortion field. Now, we have redefined our draw method, and in it, before drawing our component, we distort the canvas.

Replace list items with SkewingRelativeLayout.

Now for the animation ... In order to make a synchronous distortion and movement of our element, I had to slightly change the approach to its animation:

 @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) private void animatePostHc(int position, View v) { float startSkewX = 0.15f; float translationX; float translationY; if (prevPosition < position) { translationX = animX; translationY = animY; } else { translationX = -animX; translationY = -animY; } ObjectAnimator skewAnimator = ObjectAnimator.ofFloat(v, "skewX", startSkewX, 0f); ObjectAnimator translationXAnimator = ObjectAnimator.ofFloat(v, View.TRANSLATION_X, translationX, 0.0f); ObjectAnimator translationYAnimator = ObjectAnimator.ofFloat(v, View.TRANSLATION_Y, translationY, 0.0f); AnimatorSet set = new AnimatorSet(); set.playTogether(skewAnimator, translationXAnimator, translationYAnimator); set.setDuration(300); set.setInterpolator(decelerator); set.addListener(new AnimatorWithLayerListener(v)); set.start(); } 


using ViewPropertyAnimator was replaced by three separate ObjectAnimator'a, each of which is responsible for its value (distortion, X offset, Y offset). So that they work synchronously and on one interpolator, we use the class AnimatorSet.

If we now try to run it, we will see how beautifully our elements are distorted.

One problem I encountered when working with distortion is that I had to abandon the hardware layers, because when you add distortion at the edges of the distorted component, terrible black holes appear. I could not overcome it and removed the hardware layers. But it seems that even without them, my Galaxy Nexus works very smoothly.

Get rid of the defect by scrolling fast


After a series of experiments, I came to the conclusion that in order to get rid of unwanted defects, I need to perform two points:


The second action is necessary, because the speed limit is very thin and comes unexpectedly, which can lead to the fact that one element went to animate, and the next - no longer. It turns out that the first is superimposed on the second. Ugliness.

To calculate the speed, I modified the code a bit:

 listView.setOnScrollListener(new OnScrollListener() { private int previousFirstVisibleItem = 0; private long previousEventTime = 0; private double speed = 0; private int scrollState; @Override public void onScrollStateChanged(AbsListView view, int scrollState) { this.scrollState = scrollState; adapter.setAnimate(scrollState == SCROLL_STATE_FLING || SCROLL_STATE_TOUCH_SCROLL == scrollState); } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { if (previousFirstVisibleItem != firstVisibleItem) { long currTime = System.currentTimeMillis(); long timeToScrollOneElement = currTime - previousEventTime; speed = ((double) 1 / timeToScrollOneElement) * 1000; previousFirstVisibleItem = firstVisibleItem; previousEventTime = currTime; if (scrollState == SCROLL_STATE_FLING && speed > 16) { adapter.setAnimate(false); adapter.cancelAnimations(); } else { adapter.setAnimate(true); } } } }); 


As you can see, now when exceeding a certain threshold of speed (matched to the eye), we disable animation and cancel all animations. I repeat that the magic number 16 is picked up by me and works in my case, but it depends on the size of the elements of your list, so it’s better not to hardcode it.

In the adapter, I add a method:

  public void cancelAnimations() { for (int i = anims.size() - 1; i >= 0; i--) { anims.get(i).cancel(); } } 


And I modify listener animations. In the final version, it looks like this:

 private class AnimatorWithLayerListener implements AnimatorListener { View view; public AnimatorWithLayerListener(View view) { this.view = view; } @Override public void onAnimationStart(Animator animation) { ViewCompat.setHasTransientState(view, true); } @Override public void onAnimationEnd(Animator animation) { ViewCompat.setHasTransientState(view, false); anims.remove(animation); } @Override public void onAnimationCancel(Animator animation) { view.setTranslationX(0); view.setTranslationY(0); ((SkewingRelativeLayout) view).setSkewX(0); } @Override public void onAnimationRepeat(Animator animation) { } } 


Now, when you cancel the animation, we instantly remove all offsets and distortions. Immediately, it is worth noting that the onAnimationEnd method is always invoked: when canceled, and during normal termination. Therefore, it makes no sense to duplicate what is in it for cancellation.

It is also important that we set the ViewCompat.setHasTransientState (view, false); flag to our element. This flag, starting with ICS, allows you to mark an item in the list as modifiable, and the ListView will take this into account in the internal view reuse. ViewPropertyAnimator does it for us, but in the case of ObjectAnimator we need to do it with our hands.

Backwards Compatibiliy


Since we are good people and do not want to lose 39% of our users, we want to somehow please and Android 2.3 users. I did not ask myself about the task of fully porting the solution, so I just made an alternative method that uses the old animation API.

 private void animatePreHc(int position, View v) { if (prevPosition < position) { v.clearAnimation(); v.startAnimation(AnimationUtils.loadAnimation(context, R.anim.pop_from_bottom)); } else { v.clearAnimation(); v.startAnimation(AnimationUtils.loadAnimation(context, R.anim.pop_from_top)); } } 


And if I had set myself this goal, I most likely would simply use the JakeWharton NineOldAndroids library, which is a quality backport of animation APIs up to version 1.6.

Conclusion


As always, I do not pretend to the absolute universality and impeccability of what I describe, but in my case it works very well, and I just want to share it.
Perhaps if you have a very long list (I have a maximum of 10 items typed there), you will have to take an extra. actions to minimize the creation of objects in getView. Since AnimatorSet can be reused, it seems to me that some intelligent pool of objects can be organized, but this is all beyond what I wanted to tell you, dear colleagues, so let me bow for this.

Z. Y. if someone wants more complete sources, ask in the comments how time will be, I will drink this piece from the project and put it on github, although 95% of what needs to be done is reflected in this article.

Source: https://habr.com/ru/post/181338/


All Articles