package gem.kevin.util.ui; import java.lang.ref.WeakReference; import java.util.LinkedHashMap; import android.content.Context; import android.util.Log; import android.view.View; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.Animation.AnimationListener; import android.view.animation.AnimationSet; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.view.animation.ScaleAnimation; import android.view.animation.TranslateAnimation; public class BunchToStackAnimation { public static final class AnimViewInfo { private final WeakReference<View> mView; public int locationX = INT_UNSPECIFIED; public int locationY = INT_UNSPECIFIED; public int customFromX = INT_UNSPECIFIED; public int customFromY = INT_UNSPECIFIED; public int customToX = INT_UNSPECIFIED; public int customToY = INT_UNSPECIFIED; public long duration = INT_UNSPECIFIED; public Interpolator interpolator = null; public AlphaAnimation alphaAnimation = null; public ScaleAnimation scaleAnimation = null; public AnimationSet animSet = null; private int state = ANIM_STATE_NEW; public AnimViewInfo(final View view) { this.locationX = view.getLeft(); this.locationY = view.getTop(); mView = new WeakReference<View>(view); } public AnimViewInfo(final View view, int locationX, int locationY, int fromX, int fromY, int toX, int toY) throws IllegalAnimViewInfoException { this(view); this.locationX = locationX; this.locationY = locationY; this.customFromX = fromX; this.customFromY = fromY; this.customToX = toX; this.customToY = toY; } public AnimViewInfo(final View view, int locationX, int locationY, int fromX, int fromY, int toX, int toY, AlphaAnimation alphaAnimation, ScaleAnimation scaleAnimation, Interpolator interpolator, long duration) throws IllegalAnimViewInfoException { this(view, locationX, locationY, fromX, fromY, toX, toY); if (alphaAnimation != null) { this.alphaAnimation = alphaAnimation; } if (scaleAnimation != null) { this.scaleAnimation = scaleAnimation; } if (interpolator != null) { this.interpolator = interpolator; } this.duration = duration; } public int getState() { return this.state; } public final View getView() { return mView.get(); } @Override public int hashCode() { return mView.get().hashCode(); } public boolean isValid() { return true; } public void setState(int state) { this.state = state; } } public static interface BunchAnimationListener { public void onSpreadAnimationCanceled(View animView); public void onSpreadAnimationFinished(View animView); public void onSpreadAnimationStart(View animView); public void onStackAnimationCanceled(View animView); public void onStackAnimationFinished(View animView); public void onStackAnimationStart(View animView); } public static final class IllegalAnimViewException extends Throwable { private static final long serialVersionUID = 2406163114552149379L; public IllegalAnimViewException(String message) { super(message); } } public static final class IllegalAnimViewInfoException extends Throwable { private static final long serialVersionUID = -7213767899722019704L; public IllegalAnimViewInfoException(String message) { super(message); } } private static final String TAG = "gem-kevin_BunchToStackAnimation"; private static final int INT_UNSPECIFIED = -270; // Default animation duration public static final long ANIMATION_DURATION = 500l; // Translation direction types public static final int ANIM_BUNCH2STACK = 1; public static final int ANIM_SPREAD_OUT = 2; // Animation state public static final int ANIM_STATE_UNKNOWN = -1; public static final int ANIM_STATE_NEW = 0; public static final int ANIM_STATE_RUNNING = 1; public static final int ANIM_STATE_FINISHED = 2; public static final int ANIM_STATE_CANCELED = 3; private Interpolator mDefaultInterpolator; private AlphaAnimation mDefaultAlphaAnimation; private ScaleAnimation mDefaultScaleAnimation; private long mDefaultDuration = ANIMATION_DURATION; private Context mContext; private boolean mShareInterpolator; private boolean mShareDuration; private LinkedHashMap<View, AnimViewInfo> mViewAnimInfoMap; private boolean mActualMoveAfterAnim = false; private BunchAnimationListener mBunchListener; public BunchToStackAnimation(Context context, boolean actualMoveAfterAnim) { this(context, true, true, false); } public BunchToStackAnimation(Context context, boolean shareInterpolator, boolean shareDuration, boolean actualMoveAfterAnim) { mContext = context; mShareInterpolator = shareInterpolator; mShareDuration = shareDuration; mActualMoveAfterAnim = actualMoveAfterAnim; initialize(); } public boolean addAnimView(View view) { AnimViewInfo info; try { info = getDefaultAnimViewInfo(view); } catch (IllegalAnimViewException e) { e.printStackTrace(); return false; } mViewAnimInfoMap.put(view, info); return true; } public boolean addAnimView(View view, AnimViewInfo info) { try { validateAnimView(view); } catch (IllegalAnimViewException e) { e.printStackTrace(); return false; } try { validateAnimViewInfo(info); } catch (IllegalAnimViewInfoException e) { e.printStackTrace(); return false; } mViewAnimInfoMap.put(view, info); return true; } public void animateBunchUpToPoint(int xTarget, int yTarget) { for (View view : mViewAnimInfoMap.keySet()) { AnimViewInfo info = mViewAnimInfoMap.get(view); AnimationSet animSet = getBunchUpAnimationSet(info, xTarget, yTarget); animSet.setFillAfter(mActualMoveAfterAnim); view.startAnimation(animSet); } } public void animateSpreadOutFromPoint(int xCurrent, int yCurrent) { Log.i(TAG, "animateSpreadOutFromPoint"); for (View view : mViewAnimInfoMap.keySet()) { AnimViewInfo info = mViewAnimInfoMap.get(view); AnimationSet animSet = getSreadOutAnimationSet(info, xCurrent, yCurrent); animSet.setFillAfter(mActualMoveAfterAnim); view.startAnimation(animSet); } } public void cancel() { // FIXME: // Work mechanism of android Animation, AnimationListener may have bugs // so Animation.cancel() and View.clearAnimation() is not reliable, // that's why we need AnimViewInfo.state. for (AnimViewInfo info : mViewAnimInfoMap.values()) { if (info.animSet != null) { Log.i(TAG, "cancel animset!!"); info.animSet.cancel(); info.getView().clearAnimation(); info.setState(ANIM_STATE_CANCELED); } } } public void clearAnimViews() { mViewAnimInfoMap.clear(); } public AnimationSet getBunchUpAnimationSet(final AnimViewInfo info, int xTarget, int yTarget) { AnimationSet animSet = getAnimationSet(ANIM_BUNCH2STACK, info, xTarget, yTarget); animSet.setAnimationListener(new AnimationListener() { @Override public void onAnimationEnd(Animation animation) { if (info.getState() == ANIM_STATE_CANCELED) { if (mBunchListener != null) { mBunchListener.onStackAnimationCanceled(info.getView()); } } else if (info.getState() == ANIM_STATE_RUNNING) { info.setState(ANIM_STATE_FINISHED); if (mBunchListener != null) { mBunchListener.onStackAnimationFinished(info.getView()); } } } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationStart(Animation animation) { info.setState(ANIM_STATE_RUNNING); if (mBunchListener != null) { mBunchListener.onStackAnimationStart(info.getView()); } } }); return animSet; } public AnimationSet getSreadOutAnimationSet(final AnimViewInfo info, int xTarget, int yTarget) { AnimationSet animSet; if (mActualMoveAfterAnim) { animSet = getAnimationSet(ANIM_BUNCH2STACK, info, xTarget, yTarget); animSet.setInterpolator(new ReverseInterpolator(info.interpolator)); } else { animSet = getAnimationSet(ANIM_SPREAD_OUT, info, xTarget, yTarget); } animSet.setAnimationListener(new AnimationListener() { @Override public void onAnimationEnd(Animation animation) { if (info.getState() == ANIM_STATE_CANCELED) { if (mBunchListener != null) { mBunchListener.onSpreadAnimationCanceled(info.getView()); } } else if (info.getState() == ANIM_STATE_RUNNING) { info.setState(ANIM_STATE_FINISHED); if (mBunchListener != null) { mBunchListener.onSpreadAnimationFinished(info.getView()); } } } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationStart(Animation animation) { info.setState(ANIM_STATE_RUNNING); if (mBunchListener != null) { mBunchListener.onSpreadAnimationStart(info.getView()); } } }); return animSet; } public boolean isCanceled() { for (AnimViewInfo info : mViewAnimInfoMap.values()) { if ((info.getState() != ANIM_STATE_CANCELED)) { return false; } } return true; } public boolean isFinished() { for (AnimViewInfo info : mViewAnimInfoMap.values()) { if ((info.getState() != ANIM_STATE_FINISHED)) { return false; } } return true; } public void reset() { Log.i(TAG, "BunchToStackAnimation reset."); if (!isFinished() && !isCanceled()) { cancel(); } for (AnimViewInfo info : mViewAnimInfoMap.values()) { info.setState(ANIM_STATE_NEW); } } public void setBunchAnimationListener(BunchAnimationListener listener) { mBunchListener = listener; } public void setShareDuration(long duration) { mDefaultDuration = duration; } public void setShareInterpolator(Interpolator interpolator) { mDefaultInterpolator = interpolator; } private AnimationSet getAnimationSet(int animType, final AnimViewInfo info, int xTarget, int yTarget) { AnimationSet animSet = new AnimationSet(mShareInterpolator); animSet.addAnimation(info.alphaAnimation); animSet.addAnimation(info.scaleAnimation); TranslateAnimation translateAnim; if (animType != ANIM_SPREAD_OUT) { translateAnim = new TranslateAnimation(0, (info.customFromX != INT_UNSPECIFIED) ? xTarget - info.customFromX : xTarget - info.locationX, 0, (info.customFromY != INT_UNSPECIFIED) ? yTarget - info.customFromY : yTarget - info.locationY); } else { translateAnim = new TranslateAnimation( (info.customFromX != INT_UNSPECIFIED) ? xTarget - info.customFromX : xTarget - info.locationX, 0, (info.customFromY != INT_UNSPECIFIED) ? yTarget - info.customFromY : yTarget - info.locationY, 0); } animSet.addAnimation(translateAnim); if (mShareInterpolator) { animSet.setInterpolator(mDefaultInterpolator); } else { animSet.setInterpolator(info.interpolator); } if (mShareDuration) { animSet.setDuration(mDefaultDuration); } else { animSet.setDuration(info.duration); } info.animSet = animSet; return animSet; } private AnimViewInfo getDefaultAnimViewInfo(View view) throws IllegalAnimViewException { validateAnimView(view); AnimViewInfo result = new AnimViewInfo(view); result.interpolator = mDefaultInterpolator; result.alphaAnimation = mDefaultAlphaAnimation; result.scaleAnimation = mDefaultScaleAnimation; result.duration = mDefaultDuration; result.setState(ANIM_STATE_NEW); return result; } private void initialize() { mDefaultInterpolator = AnimationUtils.loadInterpolator(mContext, android.R.anim.decelerate_interpolator); mDefaultAlphaAnimation = new AlphaAnimation(1.0f, 0.8f); mDefaultScaleAnimation = new ScaleAnimation(1.0f, 1.2f, 1.0f, 1.2f); mViewAnimInfoMap = new LinkedHashMap<View, AnimViewInfo>(); } private void validateAnimView(View view) throws IllegalAnimViewException { if (view == null) { throw new IllegalAnimViewException("Null view is not allowed."); } else if (view.getWidth() == 0 && view.getHeight() == 0) { throw new IllegalAnimViewException("View not drawn is not allowed."); } } private void validateAnimViewInfo(AnimViewInfo info) throws IllegalAnimViewInfoException { if (info == null || !info.isValid()) { throw new IllegalAnimViewInfoException( "Null or invalid AnimViewInfo provided."); } } }