Is there any way to animate the elements of a RecyclerView when I scroll it?
I took a look at DefaultItemAnimator and RecyclerView.ItemAnimator, but that animations seems to be only called if the dataset has changed, please correct me if I am wrong.
I'm a little confused about RecyclerView.ItemAnimator.animateMove() when is it called? I put some breakpoints into that class but none of them stops my app.
However back to my question how can I animate the RecyclerView? I want that some elements have another opacity, depended on some custom rules.
I did some more reaseach it seems that animation move is exactly that what I'm looking for. That methods are called from dispatchLayout(). Here is the javadoc of that method:
Wrapper around layoutChildren() that handles animating changes caused by layout.
Animations work on the assumption that there are five different kinds of items
in play:
PERSISTENT: items are visible before and after layout
REMOVED: items were visible before layout and were removed by the app
ADDED: items did not exist before layout and were added by the app
DISAPPEARING: items exist in the data set before/after, but changed from
visible to non-visible in the process of layout (they were moved off
screen as a side-effect of other changes)
APPEARING: items exist in the data set before/after, but changed from
non-visible to visible in the process of layout (they were moved on
screen as a side-effect of other changes)
The overall approach figures out what items exist before/after layout and
infers one of the five above states for each of the items. Then the animations
are set up accordingly:
PERSISTENT views are moved ({#link ItemAnimator#animateMove(ViewHolder, int, int, int, int)})
REMOVED views are removed ({#link ItemAnimator#animateRemove(ViewHolder)})
ADDED views are added ({#link ItemAnimator#animateAdd(ViewHolder)})
DISAPPEARING views are moved off screen
APPEARING views are moved on screen
So far I'm looking for PERSISTENT, DISAPPEARING and APPEARING, but that methods are never called because of this line here:
boolean animateChangesSimple = mItemAnimator != null && mItemsAddedOrRemoved
&& !mItemsChanged;
mItemsAddedOrRemoved is simply always false so none of that callback are ever reached. Any idea how to set set flag correctly?
I ended in using an OnScrollListener and animating it in a custom animate() method. In my case that code takes just 2ms so that is no problem for the 60fps.
recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(int newState) {
if(newState == RecyclerView.SCROLL_STATE_IDLE) {
// special handler to avoid displaying half elements
scrollToNext();
}
animate();
}
#Override
public void onScrolled(int dx, int dy) {
animate();
}
});
I did it this way. Might help someone. I don't know whether it's the best way to do it but works fine for me.
UPDATE:
To fix fast scrolling behaviour, override onViewDetachedFromWindow method of the adapter and call clearAnimation on the animated view (in this case, holder.itemView.clearAnimation() ).
up_from_bottom.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="#android:anim/decelerate_interpolator">
<translate
android:fromXDelta="0%" android:toXDelta="0%"
android:fromYDelta="100%" android:toYDelta="0%"
android:duration="400" />
</set>
down_from_top.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="#android:anim/decelerate_interpolator">
<translate
android:fromXDelta="0%" android:toXDelta="0%"
android:fromYDelta="-100%" android:toYDelta="0%"
android:duration="400" />
</set>
And finally put this code in onBindViewHolder of recyclerView. Create a field called lastPosition and initialize it to -1.
Animation animation = AnimationUtils.loadAnimation(context,
(position > lastPosition) ? R.anim.up_from_bottom
: R.anim.down_from_top);
holder.itemView.startAnimation(animation);
lastPosition = position;
The good solution is to animate holder in adapter in onBindViewHolder method.
Snippet taken from Material Test project Slidenerd:
#Override
public void onBindViewHolder(ViewHolderBoxOffice holder, int position) {
Movie currentMovie = mListMovies.get(position);
//one or more fields of the Movie object may be null since they are fetched from the web
holder.movieTitle.setText(currentMovie.getTitle());
//retrieved date may be null
Date movieReleaseDate = currentMovie.getReleaseDateTheater();
if (movieReleaseDate != null) {
String formattedDate = mFormatter.format(movieReleaseDate);
holder.movieReleaseDate.setText(formattedDate);
} else {
holder.movieReleaseDate.setText(Constants.NA);
}
int audienceScore = currentMovie.getAudienceScore();
if (audienceScore == -1) {
holder.movieAudienceScore.setRating(0.0F);
holder.movieAudienceScore.setAlpha(0.5F);
} else {
holder.movieAudienceScore.setRating(audienceScore / 20.0F);
holder.movieAudienceScore.setAlpha(1.0F);
}
if (position > mPreviousPosition) {
AnimationUtils.animateSunblind(holder, true);
} else {
AnimationUtils.animateSunblind(holder, false);
}
mPreviousPosition = position;
here is a link
Related
In my recyclerview, I want all items to jiggle/wobble/wiggle when a user holds an item and moves it. The problem I'm facing is that when a user holds the item and moves it in the same viewtype the wiggle is okay but when someone drags it to the top of the recyclerview (which is a header created as a viewtype in the recyclerview) the wiggle increases a lot.
On playing around with the values I realised that this was because even though the rotation angle the same, the farther it moved away from the item's center the rotation used to increase.
I tried doing this with an object animator also but it didn't help as that too had the same issue of the rotation angle.
Here is my wiggle code
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="100"
android:fromDegrees="-5"
android:pivotX="50%"
android:pivotY="50%"
android:repeatCount="infinite"
android:repeatMode="reverse"
android:toDegrees="5" />
Here is a video of how it looks -
link
On bindView logic to start animating
((VHItem) holder).rlContainer.setOnLongClickListener(new View.OnLongClickListener()
{
#Override
public boolean onLongClick(View view)
{
if (buPostModelList != null)
{
startAnimationItem = true;
isDragCover = true;
isEditCoverImage = false;
for (int i = 0; i <= buPostModelList.size(); i++)
{
if (recyclerView.getChildAt(i) != null && recyclerView.getChildViewHolder(recyclerView.getChildAt(i)).getItemViewType() != TYPE_HEADER)
{
recyclerView.getChildAt(i).startAnimation(AnimationUtils.loadAnimation(context, R.anim.jiggle));
}
}
touchHelper.startDrag(holder);
}
return true;
}
});
EDIT
A Sample project - link
As you move the view, the center of the rotation remains at the starting location but the view still moves five degrees back and forth, so it is like moving from the center of a merry-go-round to the periphery where the five degree movement covers a greater distance in the same amount of time.
I suggest that you move to an ObjectAnimator which does not have this problem.
ObjectAnimator
This subclass of ValueAnimator provides support for animating properties on target objects. The constructors of this class take parameters to define the target object that will be animated as well as the name of the property that will be animated. Appropriate set/get functions are then determined internally and the animation will call these functions as necessary to animate the property.
jiggle.xml
This is a new ObjectAnimator xml for the jiggle effect. It is very similar to your jiggle.xml.
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="100"
android:propertyName="rotation"
android:repeatCount="-1"
android:repeatMode="reverse"
android:valueFrom="-5"
android:valueTo="5"
android:valueType="floatType" />
VHItem
Updated view holder with animator support.
class VHItem extends RecyclerView.ViewHolder {
private ImageView ivCollectionImage, ivRemoveIcon;
private RelativeLayout rlContainer;
private Animator mAnimator;
public VHItem(View itemView) {
super(itemView);
ivCollectionImage = itemView.findViewById(R.id.ivCollectionImage);
ivRemoveIcon = itemView.findViewById(R.id.ivRemoveIcon);
rlContainer = itemView.findViewById(R.id.rlContainer);
}
// Start animation. Inflate the animator lazily.
public void startAnimator() {
if (mAnimator == null) {
mAnimator = AnimatorInflater.loadAnimator(context, R.animator.jiggle);
}
mAnimator.setTarget(itemView);
mAnimator.start();
}
// Stop the animation. Set the rotation back to zero.
public void stopAnimator() {
if (mAnimator != null) {
itemView.setRotation(0);
mAnimator.cancel();
}
}
}
You will need to update the rest of the adapter to work with the new animation.
I am currently working on adding a income animation for RecyclerView items. I want it just likes the Google+ Android App. Here is my code snippets:
anim/up_from_bottom.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="#android:anim/decelerate_interpolator">
<translate
android:fromXDelta="0%" android:toXDelta="0%"
android:fromYDelta="60%" android:toYDelta="0%"
android:duration="500" />
</set>
and in the Adapter class I add the function startAnimation(itemHolder.itemView, position); in the funcitononBindViewHolder
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
...
startAnimation(holder.rootView, position);
}
the startAnimation() is like this:
protected void startAnimation(View view, int position) {
if (position > lastPosition) {
Animation animation = AnimationUtils.loadAnimation(mContext, R.anim.up_from_bottom);
view.startAnimation(animation);
lastPosition = position;
}
}
It seems looks like the Google+ Android App's list animation, but it has a bug, when I scroll fast, it animate not perfect, such as it seems that the first position and the current postion perform the animation at the same time.
I just want the animation like Google+, how can I fix it, or there has any other way.
I hava try recyclerview-animators, it's a great lib, but I still do what I want.
Any help please! Thanks!
Your "bug" occurs when you scroll fast because the animation on the viewholder is still running when it is scrolled out of the display and you are binding a new animation to the viewholder when the recyclerview recycles it.
You should override onViewDetachedFromWindow and clear the animation on the view in the method.
This will ensure that any animation that is running on the view will be cleared before it is sent to the recycler for resuse, and you can safely run a new animation on the view.
recently I wrote a function.it's about a refresh button in each list item. what I want is click the button or List Item, the refresh button starts rotate. stops when the request finished.
I use animation as follows:
<?xml version="1.0" encoding="UTF-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:fillAfter="true"
android:fromDegrees="0"
android:interpolator="#android:anim/linear_interpolator"
android:pivotX="50%"
android:pivotY="50%"
android:repeatCount="infinite"
android:toDegrees="358" />
and some source code are here:
public void refresh(View v) {
Animation rotation = AnimationUtils.loadAnimation(mContext,
R.anim.rotate);
rotation.setFillAfter(true);
v.startAnimation(rotation);
}
public void completeRefresh(View v) {
v.clearAnimation();
}
when the request finished, I call notifyDataSetChanged() to refresh the LiseView.
the problem is that the button was indeed rotating. but when I click it the second time. it's rotating but a little fuzzy. just like this:
any suggestions? thanks a lot.
What is most likely happening on your second (and subsequent clicks) is that the animation is running on it again.
In your current implementation, try using setTag(...) like this:
public void refresh(View v) {
if(v.getTag() != null && (boolean)v.getTag()) {
//do nothing, since we are setting the tag to be true once pressed
} else {
//this view hasn't been clicked yet, show animation and set tag
v.setTag(true);
Animation rotation = AnimationUtils.loadAnimation(mContext, R.anim.rotate);
rotation.setFillAfter(true);
v.startAnimation(rotation);
}
}
In your Adapter, you should keep track of which list items are being animated (I'm assuming multiple items can be clicked at the same time). Once you know that the 'request' is finished, you can update the adapter with the correct items and then call notifyDataSetChanged()
I want to set a background drawable to 1st element of list view, also to other elements of the list view I want to set the same drawable but it should be little faded. I dont want to use two images for this.
I tried using Alpha Animation for this:
In getView of my List View i used This:
Animation animation = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.alpha_anim);
if (position == 0) {
list_row_layout.setBackgroundResource(R.drawable.bg_image);
} else {
list_row_layout.setBackgroundResource(R.drawable.bg_image);
animation.setDuration(0);
animation.setFillAfter( true );
if (list_row_layout != null){
list_row_layout.startAnimation(animation);
}
}
and here is my alpha_anim.xml
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:fromAlpha="0.0"
android:toAlpha="0.5"
android:duration="1000" />
The problem is it is working fine, but the animation gets applied to 0th element also.
I tried cancelling the animation if(position == 0) but its not working.
I need to know can we apply Alpha animation to each ListRow element separately?
Check out the ListViewAnimation which contains the AlphaAnimation for the list of items in ListView.
User AlphaAnimation class as below to apply alpha effects on your ListView
private void setAlphaAdapter() {
AnimationAdapter animAdapter = new AlphaInAnimationAdapter(mAdapter);
animAdapter.setAbsListView(getListView());
getListView().setAdapter(animAdapter);
}
Why animate it? Have you tried setAlpha ?
Oh, and about the first element, add setAlpha(1f) for pos == 0. I think it might have something to do with reusing the listelements...
I have a menu option in my ActionBarSherlock Actionbar that kicks off an AsyncTask. I'm trying to get the icon to animate while the background task is running. I have it working, except when I click on the icon, the icon itself will "jump" a couple pixels over to the right, and it's very noticeable. Not exactly sure what is causing that to happen.
This is what I have so far:
private MenuItem refreshItem;
private void DoRefresh()
{
final LayoutInflater inflater = (LayoutInflater)activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final ImageView ivRefresh = (ImageView)inflater.inflate(R.layout.refresh_view, null);
final Animation rotation = AnimationUtils.loadAnimation(activity, R.anim.refresh);
ImageView ivRefresh.startAnimation(rotation);
refreshItem.setActionView(ivRefresh);
//AsyncTask is kicked off here
}
#Override
public boolean onOptionsItemSelected(final MenuItem item)
{
if (item.getItemId() == R.id.refresh) {
refreshItem = item;
this.DoRefresh();
return true;
} else {
return super.onOptionsItemSelected(item);
}
}
refresh_view.xml
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:src="#drawable/refresh"
/>
refresh.xml
<rotate
xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="0"
android:toDegrees="360"
android:pivotX="50%"
android:pivotY="50%"
android:repeatCount="infinite"
android:interpolator="#android:anim/linear_interpolator"
android:duration="1100"
/>
UPDATE:
Been fooling around with this, with no luck. It has nothing to do with the animation because if I disable the animation, the icon still jumps over to the right. If I comment out setActionView, the icon does not jump, so it has something to do with that. Have no clue, driving me nuts.
I don't know if this will have any effect but if you play around with the pivotvalues you might sort out this problem. Perhaps only pivoting on one axis?
Alex Fu, your article was very helpful, but there are some improvements that can be made.
Restarting the animation in onAnimationEnd each time is inefficient. Instead, set the animation repeatcount to infinite before you start it, then when the action finishes, set the repeatcount to 0. The animation will finish the current loop, and then call onAnimationEnd, where you can call setActionView(null).
I've also encountered a problem (in the emulator at least, haven't tested on a real device, as I don't have one) with both ActionBarSherlock and the native ICS ActionBar, that the last few frames of the animation overlap with the static button after calling setActionView(null). My solution was to wrap the ImageView that is being animated in a LinearLayout, and then call setVisibility(View.GONE) on the LinearLayout. If you animate a view, using setVisibility to hide it won't work, but hiding its parent will.
Here's all the relevant code:
layout/actionbar_progress.xml (note that I've added ids, though only the ImageView needs one):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/actionbar_progress_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/actionbar_progress_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:src="#drawable/ic_action_refresh"
style="#style/Widget.Sherlock.ActionButton"
/>
</LinearLayout>
anim/refresh_rotate.xml:
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="0"
android:toDegrees="360"
android:pivotX="50%"
android:pivotY="50%"
android:duration="1000"
android:interpolator="#android:anim/linear_interpolator"
android:repeatCount="infinite">
</rotate>
repeatCount is set to infinite, but this isn't necessary, as the java code has to set it to infinite anyway.
In onCreateOptionsMenu I have
...
menuInflater.inflate(R.menu.main, menu);
mRefreshItem = menu.findItem(R.id.menu_refresh);
...
this is just to avoid doing the findItem each time the refresh button is clicked.
In onOptionsItemSelected:
#Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
...
case R.id.menu_refresh:
refresh();
break;
...
}
}
the refresh method is just a dummy 5 second delayed post; it calls refreshAnim to start the animation, then refreshAnimEnd when it's done to stop the animation:
private void refresh() {
refreshAnim();
getWindow().getDecorView().postDelayed(
new Runnable() {
#Override
public void run() {
refreshAnimEnd();
}
}, 5000);
}
And here are the most important parts, with comments:
private void refreshAnim() {
LayoutInflater inflater = (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
//The inflated layout and loaded animation are put into members to avoid reloading them every time.
//For convenience, the ImageView is also extracted into a member
if (mRefreshIndeterminateProgressView == null || mRefreshRotateClockwise == null) {
mRefreshIndeterminateProgressView = inflater.inflate(R.layout.actionbar_progress, null);
mRefreshRotateClockwise = AnimationUtils.loadAnimation(this, R.anim.refresh_rotate);
mRefreshImage = mRefreshIndeterminateProgressView.findViewById(R.id.actionbar_progress_image);
}
//reset some stuff - make the animation infinite again,
//and make the containing view visible
mRefreshRotateClockwise.setRepeatCount(Animation.INFINITE);
mRefreshIndeterminateProgressView.setVisibility(View.VISIBLE);
mRefreshRotateClockwise.setAnimationListener(new AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {}
#Override
public void onAnimationRepeat(Animation animation) {}
#Override
public void onAnimationEnd(Animation animation) {
//This is important to avoid the overlapping problem.
//First hide the animated icon
//setActionView(null) does NOT hide it!
//as long as the animation is running, it will be visible
//hiding an animated view also doesn't work
//as long as the animation is running
//but hiding the parent does.
mRefreshIndeterminateProgressView.setVisibility(View.GONE);
//make the static button appear again
mRefreshItem.setActionView(null);
}
});
mRefreshItem.setActionView(mRefreshIndeterminateProgressView);
//everything is set up, start animating.
mRefreshImage.startAnimation(mRefreshRotateClockwise);
}
private void refreshAnimEnd() {
//sanity
if ( mRefreshImage == null || mRefreshItem == null ) {
return;
}
Animation anim = mRefreshImage.getAnimation();
//more sanity
if (anim != null) {
//let the animation finish nicely
anim.setRepeatCount(0);
} else {
//onAnimationEnd won't run in this case, so restore the static button
mRefreshItem.setActionView(null);
}
}
Just add this to your img_layout.xml if you are using the normal ActionBar:
style="?android:attr/actionButtonStyle"
Or this with the SherLock ActionBar:
style="#style/Widget.Sherlock.ActionButton"
If the menu option is an action item, you should apply the style Widget.Sherlock.ActionButton to the ImageView to keep consistency. Read this article I wrote on how to animate action items properly. http://alexfu.tumblr.com/post/19414506327/android-dev-animate-action-items