Task to do: we have a
ListView
in an Android layout. Whenever an item is removed, the list item flies out from its original position to the top of the
ListView
.
Android has predefined animations for adding and removing items from a
ViewGroup
. Even more, you can define animation for the other elements (that are not being removed), when some sibling element is being removed. See:
http://developer.android.com/training/animation/layout.html . However a
ListView
is a
ViewGroup
, this animation never happens, because the adapter of the
ListView
is immediately updates the view after a removal. You can set animation for the listItems and start them eg. during addition, but not during removal.
There are a couple of ways to make it work:
- Seaching google, many suggest to utilise
onListItemClick
listener, and start animation than (eg. http://stackoverflow.com/questions/3928193/how-to-animate-addition-or-removal-of-android-listview-rows). And at the same time schedule a runnable with postDelayed()
. I don't believe, relying on times schedules is a good idea generally, even though we might know the exact value of the delay (with Animation.getDuration()
in this case). It still can be unprecise.
- A bit improvement is to use setAnimationListener to remove the item exactly at the end of the animation. Problem in my case is, the event of the removal of the item comes from the model (and view should not modify its data, that should be a copy of the model, or else synch issues may raise). So the
onListItemClick
is not involved in this process at all.
- Implement a custom ListView. (but if for each problem I have to implement a custom class, what the Android framework is good for?)
- My approach is to initiate an animation from the Adapter (that receives update messages from the model). Unfortunately it will have a small visual flaw: the other list items will jump to their new space without animation.
- The adapter inflates a new view (
getView()
is visible from there). Actually it is weird the adapter doesn't have reference to the ui element, it controls, so we can't get the view itself by default (however we could set this reference manually).
- places it to the same position where the removable item was,
- starts a custom animation
- and at the same time it can remove the item.
A fairly good post about interpolators is
here, to show how each interpolator works.
The adapter starts the animation when an item is removed.
class MyAdapter extends ArrayAdapter<WhateverListItem> {
// ... removed some code for clarity
/** Animation used when an item is removed from the adapter. Could be set by the constructor */
private RemoveAnimation mRemoveAnimation;
/**
* Sets the remove animation. This could be done in the constructor, but in my project, this method was more appropriate
*/
public void setRemoveAnimation(RemoveAnimation removeAnimation) {
mRemoveAnimation = removeAnimation;
}
@Override
public void remove(SpendingRewardItem object) {
final View view = mVisibleItems.get(object.mOfferId);
if (view != null) { // we only want to play the animation for visible items. This mVisibleItems set is maintained by us
final View removedView = getView(getPosition(object), null, null); // create a copy of the removed item
if (mRemoveAnimation != null) mRemoveAnimation.animate(view, removedView); // play the animation
mVisibleItems.remove(object.mOfferId);
}
super.remove(object);
}
/**
* Tracks which item is visible currently. Used to test if a removed item should be animated.
*/
private WeakHashMap<String, View> mVisibleItems = new WeakHashMap<String, View>();
/**
* ListAdapter override.
* @param position item position in the parents' inner list
* @param convertView reused view if any
* @param parent parent of the view
* @return inflated view
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final View view = super.getView(position, convertView, parent);
// ... customize view, holder pattern, etc.
mVisibleItems.put(getItem(position).mOfferId, view);
return view;
}
}
The animation itself. I use the end of animation to initiate an other one (described in a
previous post), I leave related lines here commented out. There are some secret additions. The animated item can go out of the original container. It plays multiple animations on the same item. It is important, that this translates only vertically. Horizontally the item will stay where it was (in my case it will stay in the middle).
public class RemoveAnimation {
// private static final int GLOW_ANIM_DURATION = 500;
/** This will be the root ViewGroup for the animated item. With this, the view can "fly out" of the original ListView */
private RelativeLayout mAnimationRoot;
/** Used to acquire interpolators */
private Context mContext;
// private final View mTargetTab;
// private TabGlow mGlow;
/** In this implementation, animationRoot has to be RelativeLayout, because animate() uses its typed LayoutParams */
public SpendingRewardsRemoveAnimation(RelativeLayout animationRoot, View targetTab) {
mAnimationRoot = animationRoot;
mContext = animationRoot.getContext();
// mTargetTab = targetTab;
// mGlow = new TabGlow();
}
public void animate(View originalView, View copyView) {
if (originalView != null && copyView != null) {
final View animatingView;
animatingView = copyView;
// place the copyView to where the originalView was.
final RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(originalView.getWidth(), originalView.getHeight());
layoutParams.leftMargin = originalView.getLeft();
layoutParams.topMargin = originalView.getTop() + getDistanceFromRoot(originalView);
animatingView.setLayoutParams(layoutParams);
// you need to set the background colour of the list items, or else they will have transparent background. Which is weird usually.
// animatingView.setBackgroundColor(Color.argb(255, 14, 52, 128));
mAnimationRoot.addView(animatingView);
mAnimationRoot.invalidate(); // ask the framework to redraw this view
// create scale animations. You can play with these parameters. Check reference
final ScaleAnimation scaleUp = new ScaleAnimation(1.0f, 1.1f, 1.0f, 1.1f, Animation.RELATIVE_TO_SELF,0.5f, Animation.RELATIVE_TO_SELF, 0f);
final ScaleAnimation scaleDown = new ScaleAnimation(1.0f, 0.0f, 1.0f, 0.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0f);
scaleUp.setDuration(50);
scaleUp.setInterpolator(mContext, android.R.anim.decelerate_interpolator);
scaleDown.setDuration(250);
scaleDown.setInterpolator(mContext, android.R.anim.accelerate_interpolator);
scaleDown.setStartOffset(50); // second animation starts, when firsts finishes
// create translation animation
final TranslateAnimation trans = new TranslateAnimation(
Animation.RELATIVE_TO_SELF,
0,
Animation.RELATIVE_TO_SELF,
0,
Animation.ABSOLUTE,
0,
Animation.ABSOLUTE,
-1.0f * layoutParams.topMargin // move the item from the current position upwards, to 0 (finally topMargin + (-1 * topMargin) will be equal to 0
);
trans.setDuration(250);
trans.setStartOffset(50);
trans.setInterpolator(mContext, android.R.anim.accelerate_interpolator);
// add new animations to the set
final AnimationSet animationSet = new AnimationSet(false);
animationSet.addAnimation(scaleUp); // view first pops up
animationSet.addAnimation(scaleDown); // a bit later shrinks to nothing
animationSet.addAnimation(trans); // and flies to the top of
animationSet.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
animatingView.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationEnd(Animation animation) {
animatingView.setVisibility(View.GONE);
mAnimationRoot.post(new Runnable() {
@Override
public void run() {
mAnimationRoot.removeView(animatingView); // we don't want to keep the animated View forever
}
});
// mGlow.glow(mTargetTab, GLOW_ANIM_DURATION);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
animatingView.startAnimation(animationSet);
}
}
/** This calculates the absolute position of the view. Which can differ from getTop() when the any parent container also has a vertical displacement. */
private int getDistanceFromRoot(View originalView) {
View current = originalView;
ViewGroup parent = null;
int ret = 0;
do {
if (current.getParent() instanceof ViewGroup) {
parent = (ViewGroup)current.getParent();
ret += parent.getTop();
current = parent;
}
} while (parent != null && parent != mAnimationRoot);
return ret;
}
}
Wiring up is easy:
class MyFragment extends Fragment {
// removed some code for brevity
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View inflated = inflater.inflate(R.layout.spending_rewards_hub, null);
final ListView listView = (ListView) inflated.findViewById(R.id.listView);
mAdapter = new MyAdapter(/* whatever you initialise with */);
mAdapter.setRemoveAnimation(new SpendingRewardsRemoveAnimation((RelativeLayout)inflated, activeTab));
listView.setAdapter(mAdapter);
// ... more UI initialisation as needed
}
}