package com.marshalchen.common.uimodule.simplemodule;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import android.content.Context;
import android.database.DataSetObserver;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnPreDrawListener;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.OvershootInterpolator;
import android.widget.BaseAdapter;
import android.widget.ListAdapter;
import android.widget.ListView;
import com.marshalchen.common.commonUtils.logUtils.Logs;
import com.marshalchen.common.uimodule.nineoldandroids.animation.Animator;
import com.marshalchen.common.uimodule.nineoldandroids.animation.AnimatorListenerAdapter;
import com.marshalchen.common.uimodule.nineoldandroids.animation.AnimatorSet;
import com.marshalchen.common.uimodule.nineoldandroids.animation.ObjectAnimator;
import com.marshalchen.common.uimodule.nineoldandroids.view.ViewHelper;
import com.marshalchen.common.uimodule.nineoldandroids.view.animation.AnimatorProxy;
@SuppressWarnings({ "rawtypes", "unchecked" })
public class FilckerAnimationListView extends ListView {
public static interface Manipulator<T extends ListAdapter> {
void manipulate(T adapter);
}
private static class AdapterWrapper extends BaseAdapter {
private final ListAdapter adapter;
private boolean mayNotify = true;
private final DataSetObserver observer = new DataSetObserver() {
@Override
public void onChanged() {
if (mayNotify) {
notifyDataSetChanged();
}
}
@Override
public void onInvalidated() {
notifyDataSetInvalidated();
};
};
public AdapterWrapper(final ListAdapter adapter) {
this.adapter = adapter;
adapter.registerDataSetObserver(observer);
}
public void setMayNotify(final boolean mayNotify) {
this.mayNotify = mayNotify;
}
@Override
public int getCount() {
return adapter.getCount();
}
@Override
public Object getItem(final int position) {
return adapter.getItem(position);
}
@Override
public long getItemId(final int position) {
return adapter.getItemId(position);
}
@Override
public boolean hasStableIds() {
return adapter.hasStableIds();
}
@Override
public View getView(final int position, final View convertView, final ViewGroup parent) {
return adapter.getView(position, convertView, parent);
}
}
protected static final int MAX_ANIM_DURATION = 900;
protected static final int MIN_ANIM_DURATION = 500;
protected static final int ALPHA_ANIM_DURATION = 300;
protected final Map<Long, Float> yMap = new HashMap<Long, Float>();
protected final Map<Long, Integer> positionMap = new HashMap<Long, Integer>();
protected final Collection<Long> beforeVisible = new HashSet<>();
protected final Collection<Long> afterVisible = new HashSet<>();
private final List<Manipulator> pendingManipulations = new ArrayList<>();
private final List<Manipulator> pendingManipulationsWithoutAnimation = new ArrayList<>();
private boolean animating = false;
private AdapterWrapper adapter;
private float animationDurationFactor = 1f;
private Interpolator translateInterpolater = new OvershootInterpolator(1.1f);
public FilckerAnimationListView(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
init();
}
public FilckerAnimationListView(final Context context, final AttributeSet attrs) {
super(context, attrs);
init();
}
public FilckerAnimationListView(final Context context) {
super(context);
init();
}
private void init() {
}
public void setAnimationDurationFactor(final float animationDurationFactor) {
this.animationDurationFactor = animationDurationFactor;
}
public float getAnimationDurationFactor() {
return animationDurationFactor;
}
public void setInterpolater(final Interpolator translateInterpolater) {
this.translateInterpolater = translateInterpolater;
}
public Interpolator getInterpolater() {
return translateInterpolater;
}
@Override
public void setAdapter(final ListAdapter adapter) {
this.adapter = new AdapterWrapper(adapter);
super.setAdapter(this.adapter);
}
public <T extends ListAdapter> void manipulate(final Manipulator<T> manipulator) {
if (!animating) {
prepareAnimation();
manipulator.manipulate((T) adapter.adapter);
doAnimation();
} else {
pendingManipulations.add(manipulator);
}
}
public <T extends ListAdapter> void manipulateWithoutAnimation(final Manipulator<T> manipulator) {
if (!animating) {
manipulator.manipulate((T) adapter.adapter);
adapter.notifyDataSetChanged();
} else {
pendingManipulationsWithoutAnimation.add(manipulator);
}
}
private void manipulatePending() {
if (!pendingManipulationsWithoutAnimation.isEmpty()) {
animating = true;
for (final Manipulator manipulator : pendingManipulationsWithoutAnimation) {
manipulator.manipulate(adapter.adapter);
}
pendingManipulationsWithoutAnimation.clear();
adapter.notifyDataSetChanged();
post(new Runnable() {
@Override
public void run() {
animating = false;
manipulatePending();
}
});
} else {
if (pendingManipulations.isEmpty()) {
return;
}
prepareAnimation();
for (final Manipulator manipulator : pendingManipulations) {
manipulator.manipulate(adapter.adapter);
}
pendingManipulations.clear();
doAnimation();
}
}
private void prepareAnimation() {
yMap.clear();
positionMap.clear();
beforeVisible.clear();
afterVisible.clear();
adapter.setMayNotify(false);
final int childCount = getChildCount();
final int firstVisiblePosition = getFirstVisiblePosition();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final long id = adapter.getItemId(firstVisiblePosition + i);
yMap.put(id, ViewHelper.getY(child));
positionMap.put(id, firstVisiblePosition + i);
}
for (int i = 0; i < firstVisiblePosition; i++) {
final long id = adapter.getItemId(i);
beforeVisible.add(id);
}
final int count = adapter.getCount();
for (int i = firstVisiblePosition + childCount; i < count; i++) {
final long id = adapter.getItemId(i);
afterVisible.add(id);
}
}
private void doAnimation() {
setEnabled(false);
animating = true;
final float durationUnit = (float) MAX_ANIM_DURATION / getHeight();
animatePreLayout(durationUnit, new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(final Animator animation) {
adapter.notifyDataSetChanged();
getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
@Override
public boolean onPreDraw() {
getViewTreeObserver().removeOnPreDrawListener(this);
animatePostLayout(durationUnit);
return true;
}
});
}
});
}
/**
* Animate items that are deleted entirely and items that move out of
* bounds.
*/
private void animatePreLayout(final float durationUnit, final Animator.AnimatorListener listener) {
final AnimatorSet animatorSet = new AnimatorSet();
final int firstVisiblePosition = getFirstVisiblePosition();
final int childCount = getChildCount();
for (final Iterator<Entry<Long, Float>> iter = yMap.entrySet().iterator(); iter.hasNext();) {
final Entry<Long, Float> entry = iter.next();
final long id = entry.getKey();
final int oldPos = positionMap.get(id);
final View child = getChildAt(oldPos - firstVisiblePosition);
final int newPos = getPositionForId(id);
// fade out items that disappear
if (newPos == -1) {
final ObjectAnimator anim = animateAlpha(child, false);
animatorSet.play(anim);
iter.remove();
positionMap.remove(id);
continue;
}
// translate items that move out of bounds
if (newPos < firstVisiblePosition || newPos > firstVisiblePosition + childCount) {
final float offset;
if (newPos < firstVisiblePosition) {
offset = -getHeight();
} else {
offset = getHeight();
}
final AnimatorProxy proxy = AnimatorProxy.wrap(child);
final ObjectAnimator anim = ObjectAnimator.ofFloat(proxy, "translationY", 0f,
offset);
final int finalDuration = getDuration(0, getHeight() / 2, durationUnit);
anim.setInterpolator(new AccelerateInterpolator());
anim.setDuration((long) (finalDuration * animationDurationFactor));
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(final Animator animation) {
child.post(new Runnable() {
@Override
public void run() {
proxy.setTranslationY(0f);
}
});
}
});
animatorSet.play(anim);
iter.remove();
positionMap.remove(id);
continue;
}
}
if (!animatorSet.getChildAnimations().isEmpty()) {
animatorSet.addListener(listener);
animatorSet.start();
} else {
listener.onAnimationEnd(animatorSet);
}
}
/**
* Animate items that just appeared and items that move within the screen.
*/
private void animatePostLayout(final float durationUnit) {
final AnimatorSet animatorSet = new AnimatorSet();
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
final long id = getItemIdAtPosition(getFirstVisiblePosition() + i);
ObjectAnimator anim = null;
ViewHelper.setAlpha(child, 1f);
if (yMap.containsKey(id)) {
// moved within visible area
// log("Moved within visible area id: " + id);
final float oldY = yMap.remove(id);
final float newY = ViewHelper.getY(child);
if (oldY != newY) {
anim = animateY(child, oldY, newY, durationUnit);
}
} else {
// moved into visible area or new
if (beforeVisible.contains(id)) {
// moved from top
final float newY = ViewHelper.getY(child);
final float oldY = -child.getHeight();
anim = animateY(child, oldY, newY, durationUnit);
} else if (afterVisible.contains(id)) {
// moved from bottom
final float newY = ViewHelper.getY(child);
final float oldY = getHeight();
anim = animateY(child, oldY, newY, durationUnit);
} else {
// entirely new
ViewHelper.setAlpha(child, 0f);
anim = animateAlpha(child, true);
anim.setStartDelay(MIN_ANIM_DURATION);
}
}
if (anim != null) {
animatorSet.play(anim);
}
}
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(final Animator animation) {
finishAnimation();
};
});
animatorSet.start();
}
private void finishAnimation() {
adapter.setMayNotify(true);
animating = false;
setEnabled(true);
manipulatePending();
}
protected int getPositionForId(final long id) {
for (int i = 0; i < adapter.getCount(); i++) {
if (adapter.getItemId(i) == id) {
return i;
}
}
return -1;
}
protected ObjectAnimator animateAlpha(final View view, final boolean fadeIn) {
final ObjectAnimator anim = ObjectAnimator.ofFloat(view, "alpha", fadeIn ? new float[] {
0f, 1f } : new float[] { 1f, 0f });
anim.setDuration((long) (ALPHA_ANIM_DURATION * animationDurationFactor));
return anim;
}
protected ObjectAnimator animateY(final View view, final float oldY, final float newY,
final float durationUnit) {
final int duration = getDuration(oldY, newY, durationUnit);
final ObjectAnimator anim = ObjectAnimator.ofFloat(AnimatorProxy.wrap(view),
"translationY", oldY - newY, 0);
final int finalDuration = Math
.min(Math.max(duration, MIN_ANIM_DURATION), MAX_ANIM_DURATION);
anim.setDuration((long) (finalDuration * animationDurationFactor));
anim.setInterpolator(translateInterpolater);
return anim;
}
protected static int getDuration(final float oldY, final float newY, final float durationUnit) {
final float distance = newY - oldY;
final int duration = (int) (Math.abs(distance) * durationUnit);
return duration;
}
protected boolean isPositionVisible(final int position) {
final int firstVisiblePosition = getFirstVisiblePosition();
if (position < firstVisiblePosition) {
return false;
}
final int childCount = getChildCount();
if (position > firstVisiblePosition + childCount) {
return false;
}
return true;
}
protected void log(final String msg) {
Logs.d(getClass().getSimpleName(), msg);
}
}