package com.kaichunlin.transition; import android.support.annotation.CheckResult; import android.support.annotation.FloatRange; import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.util.SparseArray; import android.view.View; import android.view.animation.Interpolator; import com.kaichunlin.transition.animation.Animation; import com.kaichunlin.transition.animation.AnimationManager; import com.kaichunlin.transition.animation.TransitionAnimation; import com.kaichunlin.transition.internal.TransitionController; import com.nineoldandroids.animation.PropertyValuesHolder; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; /** * Provides foundation to build classes that allows easy creation of {@link Transition}. */ public abstract class AbstractTransitionBuilder<T extends AbstractTransitionBuilder, S extends Transition> implements Cloneable { static final int SCALE_FACTOR = 10_000; protected static final int ALPHA = 0; protected static final int ROTATION = 1; protected static final int ROTATION_X = 2; protected static final int ROTATION_Y = 3; protected static final int SCALE = 4; protected static final int SCALE_X = 5; protected static final int SCALE_Y = 6; protected static final int TRANSLATION_X = 7; protected static final int TRANSLATION_Y = 8; protected static final int X = 9; protected static final int Y = 10; protected static final int VISIBILITY = 11; protected static final int TRANSLATION_X_AS_FRACTION_OF_WIDTH = 12; protected static final int TRANSLATION_Y_AS_FRACTION_OF_HEIGHT = 13; protected static final int TOTAL = 14; protected boolean mAutoClone; public static String getPropertyName(int propertyId) { switch (propertyId) { case ALPHA: return "alpha"; case ROTATION: return "rotation"; case ROTATION_X: return "rotationX"; case ROTATION_Y: return "rotationY"; case SCALE_X: return "scaleX"; case SCALE_Y: return "scaleY"; case TRANSLATION_X: return "translationX"; case TRANSLATION_Y: return "translationY"; case X: return "x"; case Y: return "y"; case TRANSLATION_X_AS_FRACTION_OF_WIDTH: return "translationXAsFractionOfWidth"; case TRANSLATION_Y_AS_FRACTION_OF_HEIGHT: return "translationYAsFractionOfHeight"; } throw new IllegalArgumentException(); } SparseArray<PropertyValuesHolder> mHolders = new SparseArray<>(4); SparseArray<ShadowValuesHolder> mShadowHolders = new SparseArray<>(4); List<DelayedEvaluator<T>> mDelayed; float mStart = TransitionController.DEFAULT_START; float mEnd = TransitionController.DEFAULT_END; String mId; boolean mReverse; boolean mApplyInitialConditions; Interpolator mInterpolator; int mDuration; DelayedProcessor mDelayedProcessor; transient WeakReference<Object> mOwnerRef; AbstractTransitionBuilder() { } /** * Sets the ID of the transition for debug purpose, same as {@link Transition#setId(String)}. * * @param id * @return */ public T id(String id) { mId = id; return self(); } /** * Sets the start and end range of the transition, this affects where the end the transition is * reached and no further modification will be performed on the target view. * <p> * For most {@link TransitionManager} the range will be [0..1]. * * @param start * @param end * @return */ public T range(float start, float end) { mStart = start; mEnd = end; return self(); } /** * Sets the start range of the transition, this affects where the end the transition is reached * and no further modification will be performed on the target view. * <p> * For most {@link TransitionManager} the range will be [0..1]. * * @param start * @return */ public T startRange(float start) { mStart = start; return self(); } /** * Sets the end range of the transition, this affects where the end the transition is reached * and no further modification will be performed on the target view. * <p> * For most {@link TransitionManager} the range will be [0..1]. * * @param end * @return */ public T endRange(float end) { mEnd = end; return self(); } public float getStartRange() { return mStart; } public float getEndRange() { return mEnd; } public float getRange() { return mEnd - mStart; } /** * Changes the alpha to the specified values. * * @param vals * @return self */ public T alpha(float... vals) { transitFloat(ALPHA, vals); return self(); } /** * Changes the alpha value from the target view's current value to the end value. * * @param end * @return self */ public abstract T alpha(@FloatRange(from = 0.0, to = 1.0) float end); /** * Similar to alpha(float...), but wait until the transition is about to start to perform the evaluation. * * @param transitions * @return self */ public T delayAlpha(float... transitions) { getDelayedProcessor().addProcess(ALPHA, transitions); return self(); } /** * Similar to alpha(float), but wait until the transition is about to start to perform the evaluation. * * @param end * @return self */ public T delayAlpha(@FloatRange(from = 0.0, to = 1.0) float end) { getDelayedProcessor().addProcess(ALPHA, end); return self(); } /** * Changes the rotation (rotationX and rotationY) value to the specified values. * * @param vals * @return self */ public T rotation(float... vals) { transitFloat(ROTATION, vals); return self(); } /** * Changes the rotation value from the target view's current value to the end value. * * @param end * @return self */ public abstract T rotation(float end); /** * Similar to rotation(float...), but wait until the transition is about to start to perform the evaluation. * * @param transitions * @return self */ public T delayRotation(float... transitions) { getDelayedProcessor().addProcess(ROTATION, transitions); return self(); } /** * Similar to rotation(float), but wait until the transition is about to start to perform the evaluation. * * @param end * @return self */ public T delayRotation(float end) { getDelayedProcessor().addProcess(ROTATION, end); return self(); } /** * Changes the rotationX value to the specified values. * * @param vals * @return self */ public T rotationX(float... vals) { transitFloat(ROTATION_X, vals); return self(); } /** * Changes the rotationX value from the target view's current value to the end value. * * @param end * @return self */ public abstract T rotationX(float end); /** * Similar to rotationX(float...), but wait until the transition is about to start to perform the evaluation. * * @param transitions * @return self */ public T delayRotationX(float... transitions) { getDelayedProcessor().addProcess(ROTATION_X, transitions); return self(); } /** * Similar to rotationX(float), but wait until the transition is about to start to perform the evaluation. * * @param end * @return self */ public T delayRotationX(float end) { getDelayedProcessor().addProcess(ROTATION_X, end); return self(); } /** * Changes the rotationY to the specified values. * * @param vals * @return self */ public T rotationY(float... vals) { transitFloat(ROTATION_Y, vals); return self(); } /** * Changes the rotationY value from the target view's current value to the end value. * * @param end * @return self */ public abstract T rotationY(float end); /** * Similar to rotationY(float...), but wait until the transition is about to start to perform the evaluation. * * @param transitions * @return self */ public T delayRotationY(float... transitions) { getDelayedProcessor().addProcess(ROTATION_Y, transitions); return self(); } /** * Similar to rotationY(float), but wait until the transition is about to start to perform the evaluation. * * @param end * @return self */ public T delayRotationY(float end) { getDelayedProcessor().addProcess(ROTATION_Y, end); return self(); } /** * Changes the scaleX value to the specified values. * * @param vals * @return self */ public T scaleX(float... vals) { transitFloat(SCALE_X, vals); return self(); } /** * Changes the scaleX value from the target view's current value to the end value. * * @param end * @return self */ public abstract T scaleX(@FloatRange(from = 0.0) float end); /** * Similar to scaleX(float...), but wait until the transition is about to start to perform the evaluation. * * @param transition * @return self */ public T delayScaleX(float... transition) { getDelayedProcessor().addProcess(SCALE_X, transition); return self(); } /** * Similar to scaleX(float), but wait until the transition is about to start to perform the evaluation. * * @param end * @return self */ public T delayScaleX(@FloatRange(from = 0.0) float end) { getDelayedProcessor().addProcess(SCALE_X, end); return self(); } /** * Changes the scaleY value to the specified values. * * @param vals * @return self */ public T scaleY(float... vals) { transitFloat(SCALE_Y, vals); return self(); } /** * Changes the scaleY value from the target view's current value to the end value. * * @param end * @return self */ public abstract T scaleY(@FloatRange(from = 0.0) float end); /** * Similar to scaleX(float...), but wait until the transition is about to start to perform the evaluation. * * @param transitions * @return self */ public T delayScaleY(@FloatRange(from = 0.0) float... transitions) { getDelayedProcessor().addProcess(SCALE_Y, transitions); return self(); } /** * Similar to scaleX(float), but wait until the transition is about to start to perform the evaluation. * * @param end * @return self */ public T delayScaleY(@FloatRange(from = 0.0) float end) { getDelayedProcessor().addProcess(SCALE_Y, end); return self(); } /** * Changes the scale (scaleX and scaleY) value to the specified values. * * @param vals * @return self */ public T scale(float... vals) { transitFloat(SCALE_X, vals); transitFloat(SCALE_Y, vals); return self(); } /** * Changes the scale value from the target view's current value to the end value. * * @param end * @return self */ public abstract T scale(@FloatRange(from = 0.0) float end); /** * Similar to scale(float...), but wait until the transition is about to start to perform the evaluation. * * @param transitions * @return self */ public T delayScale(float... transitions) { getDelayedProcessor().addProcess(SCALE, transitions); return self(); } /** * Similar to scale(float), but wait until the transition is about to start to perform the evaluation. * * @param end * @return self */ public T delayScale(@FloatRange(from = 0.0) float end) { getDelayedProcessor().addProcess(SCALE, end); return self(); } /** * Changes the translationX value from the target view's current value to the end value. * * @param vals * @return self */ public T translationX(float... vals) { transitFloat(TRANSLATION_X, vals); return self(); } /** * Changes the translationX value from the target view's current value to the end value. * * @param end * @return self */ public abstract T translationX(float end); /** * Similar to delayTranslationX(float...), but wait until the transition is about to start to perform * the evaluation. * * @param transitions * @return self */ public T delayTranslationX(float... transitions) { getDelayedProcessor().addProcess(TRANSLATION_X, transitions); return self(); } /** * Similar to delayTranslationX(float), but wait until the transition is about to start to perform the * evaluation. * * @param end * @return self */ public T delayTranslationX(float end) { getDelayedProcessor().addProcess(TRANSLATION_X, end); return self(); } /** * Changes the translationY value from the target view's current value to the end value. * * @param vals * @return self */ public T translationY(float... vals) { transitFloat(TRANSLATION_Y, vals); return self(); } /** * Changes the translationY value from the target view's current value to the end value. * * @param end * @return self */ public abstract T translationY(float end); /** * Similar to delayTranslationY(float...), but wait until the transition is about to start to perform * the evaluation. * * @param transitions * @return self */ public T delayTranslationY(float... transitions) { getDelayedProcessor().addProcess(TRANSLATION_Y, transitions); return self(); } /** * Similar to delayTranslationY(float), but wait until the transition is about to start to perform the * evaluation. * * @param end * @return self */ public T delayTranslationY(float end) { getDelayedProcessor().addProcess(TRANSLATION_Y, end); return self(); } /** * Changes the x value from the target view's current value to the end value. * * @param vals * @return self */ public T x(float... vals) { transitFloat(X, vals); return self(); } /** * Changes the x value from the target view's current value to the end value. * * @param end * @return self */ public abstract T x(float end); /** * Similar to x(float), but wait until the transition is about to start to perform the evaluation. * * @param transitions * @return self */ public T delayX(final float... transitions) { getDelayedProcessor().addProcess(X, transitions); return self(); } /** * Similar to x(float), but wait until the transition is about to start to perform the evaluation. * * @param end * @return self */ public T delayX(final float end) { getDelayedProcessor().addProcess(X, end); return self(); } /** * Changes the y value from the target view's current value to the end value. * * @param vals * @return self */ public T y(float... vals) { transitFloat(Y, vals); return self(); } /** * Changes the y value from the target view's current value to the end value. * * @param end * @return self */ public abstract T y(float end); /** * Similar to y(float), but wait until the transition is about to start to perform the evaluation. * * @param transitions * @return self */ public T delayY(final float... transitions) { getDelayedProcessor().addProcess(Y, transitions); return self(); } /** * Similar to y(float), but wait until the transition is about to start to perform the evaluation. * * @param end * @return self */ public T delayY(final float end) { getDelayedProcessor().addProcess(Y, end); return self(); } /** * Only applies when building {@link Animation}. * * @param duration * @return */ public T duration(@IntRange(from = 0) int duration) { mDuration = duration; return self(); } protected void checkModifiability() { if (mOwnerRef == null || mOwnerRef.get() == null) { mOwnerRef = null; } else { throw new IllegalStateException("clone() should be called when building multiple animations/transitions from the same builder, i.e. after build*() is called."); } } /** * Transits a float propertyId from the start value to the end value. * * @param propertyId * @param vals * @return self */ public T transitFloat(int propertyId, float... vals) { String property = getPropertyName(propertyId); mHolders.put(propertyId, PropertyValuesHolder.ofFloat(property, vals)); mShadowHolders.put(propertyId, ShadowValuesHolder.ofFloat(property, vals)); return self(); } /** * Transits a float property from the start value to the end value. * * @param propertyId * @param vals * @return self */ public T transitInt(int propertyId, int... vals) { String property = getPropertyName(propertyId); mHolders.put(propertyId, PropertyValuesHolder.ofInt(property, vals)); mShadowHolders.put(propertyId, ShadowValuesHolder.ofInt(property, vals)); return self(); } /** * Reverse the transition effect. * * @return self */ public T reverse() { mReverse = !mReverse; return self(); } public T interpolator(Interpolator interpolator) { mInterpolator = interpolator; return self(); } @CheckResult /** * Asks the subclass to create and set a ITransition. * * @return {@link Transition} that would perform the desired transition. */ protected abstract S createTransition(); /** * Builds a {@link Transition}, the created object will not be modified when the builder's modifier * methods are called. * * @return */ public final S build() { S vt = createTransition(); markObjectAsModifiabilityFlag(vt); vt.setId(mId); if (mInterpolator != null) { vt.setInterpolator(mInterpolator); } if (mReverse) { vt.reverse(); } return vt; } public TransitionAnimation buildAnimation() { TransitionAnimation animation = new TransitionAnimation(mStart < mEnd ? build() : build().reverse()); markObjectAsModifiabilityFlag(animation); if (mDuration != 0) { animation.setDuration(mDuration); } if(mApplyInitialConditions) { animation.startAnimation(); animation.pauseAnimation(); } return animation; } public TransitionAnimation buildAnimationFor(AnimationManager animationManager) { TransitionAnimation animation = buildAnimation(); markObjectAsModifiabilityFlag(animation); animationManager.addAnimation(animation); if(mApplyInitialConditions) { animation.startAnimation(); animation.pauseAnimation(); } return animation; } /** * Sets the {@link TransitionManager}, once set calling {@link #build()} would automatically add * the created {@link ViewTransition} to the adapter. * * @param transitionManager * @return */ public S buildFor(@NonNull TransitionManager transitionManager) { markObjectAsModifiabilityFlag(transitionManager); S transition = build(); transition.setProgress(0); transitionManager.addTransition(transition); return transition; } protected void markObjectAsModifiabilityFlag(Object owner) { mOwnerRef = new WeakReference<>(owner); } /** * Adds a DelayedEvaluator whose evaluate(View, BaseTransitionBuilder) method will only be called * when the transition is about to start. * * @param delayed * @return */ public T addDelayedEvaluator(@NonNull DelayedEvaluator delayed) { if (mDelayed == null) { mDelayed = new ArrayList<>(4); } mDelayed.add(delayed); return self(); } protected abstract T self(); @CheckResult @Override public AbstractTransitionBuilder clone() { AbstractTransitionBuilder newCopy = null; try { newCopy = (AbstractTransitionBuilder) super.clone(); newCopy.mOwnerRef = null; newCopy.mHolders = new SparseArray<>(mHolders.size()); for (int i = 0, size = mHolders.size(); i < size; i++) { newCopy.mHolders.put(mHolders.keyAt(i), mHolders.valueAt(i).clone()); } newCopy.mShadowHolders = new SparseArray<>(mShadowHolders.size()); for (int i = 0, size = mShadowHolders.size(); i < size; i++) { newCopy.mShadowHolders.put(mShadowHolders.keyAt(i), mShadowHolders.valueAt(i).clone()); } if (mDelayed != null) { newCopy.mDelayed = new ArrayList<>(mDelayed.size()); newCopy.mDelayed.addAll(mDelayed); } if (mDelayedProcessor != null) { newCopy.mDelayedProcessor = mDelayedProcessor.clone(); newCopy.mDelayed.remove(mDelayedProcessor); newCopy.mDelayed.add(newCopy.mDelayedProcessor); } } catch (CloneNotSupportedException e) { e.printStackTrace(); } return newCopy; } public T visible() { getDelayedProcessor().addProcess(VISIBILITY, View.VISIBLE); return self(); } public T invisible() { getDelayedProcessor().addProcess(VISIBILITY, View.INVISIBLE); return self(); } public T applyInitialConditions() { mApplyInitialConditions = true; return self(); } public T gone() { getDelayedProcessor().addProcess(VISIBILITY, View.GONE); return self(); } protected boolean hasDelayedProcessor() { return mDelayedProcessor != null; } protected DelayedProcessor getDelayedProcessor() { if (mDelayedProcessor == null) { mDelayedProcessor = new DelayedProcessor(); addDelayedEvaluator(mDelayedProcessor); } return mDelayedProcessor; } protected PropertyValuesHolder[] getValuesHolders() { final int size = mHolders.size(); PropertyValuesHolder[] holders = new PropertyValuesHolder[size]; for (int i = 0; i < size; i++) { holders[i] = mHolders.valueAt(i); } return holders; } //a shared static DelayedEvaluator class to reduce object creation protected static class DelayedProcessor implements DelayedEvaluator, Cloneable { float[] process; float[][] multiValueProcess; int cachedViewState = -1; DelayedProcessor() { process = new float[TOTAL]; for (int i = 0; i < TOTAL; i++) { process[i] = Float.MIN_VALUE; } multiValueProcess = new float[TOTAL][]; } private float[] createNewValues(float[] values, float firstVal) { float[] newValues = new float[values.length + 1]; newValues[0] = firstVal; System.arraycopy(values, 0, newValues, 1, values.length); return newValues; } private View getView(AbstractTransitionBuilder builder) { return ((ViewTransitionBuilder) builder).getTargetView(); } @Override public void evaluate(View view, AbstractTransitionBuilder builder) { float value; float[] values; value = process[ALPHA]; if (value != Float.MIN_VALUE) { builder.alpha(value); } values = multiValueProcess[ALPHA]; if (values != null) { builder.alpha(createNewValues(values, getView(builder).getAlpha())); } value = process[ROTATION]; if (value != Float.MIN_VALUE) { builder.rotation(value); } values = multiValueProcess[ROTATION]; if (values != null) { builder.rotation(createNewValues(values, getView(builder).getRotation())); } value = process[ROTATION_X]; if (value != Float.MIN_VALUE) { builder.rotationX(value); } values = multiValueProcess[ROTATION_X]; if (values != null) { builder.rotationX(createNewValues(values, getView(builder).getRotationX())); } value = process[ROTATION_Y]; if (value != Float.MIN_VALUE) { builder.rotationY(value); } values = multiValueProcess[ROTATION_Y]; if (values != null) { builder.rotationY(createNewValues(values, getView(builder).getRotationY())); } value = process[SCALE]; if (value != Float.MIN_VALUE) { builder.scale(value, value); } values = multiValueProcess[SCALE]; if (values != null) { //TODO assumes scaleX & scaleY are equal builder.scale(createNewValues(values, getView(builder).getScaleX())); } value = process[SCALE_X]; if (value != Float.MIN_VALUE) { builder.scaleX(value); } values = multiValueProcess[SCALE_X]; if (values != null) { builder.scaleX(createNewValues(values, getView(builder).getScaleX())); } value = process[SCALE_Y]; if (value != Float.MIN_VALUE) { builder.scaleY(value); } values = multiValueProcess[SCALE_Y]; if (values != null) { builder.scaleY(createNewValues(values, getView(builder).getScaleY())); } value = process[TRANSLATION_X]; if (value != Float.MIN_VALUE) { builder.translationX(value); } values = multiValueProcess[TRANSLATION_X]; if (values != null) { builder.translationX(createNewValues(values, getView(builder).getTranslationX())); } value = process[TRANSLATION_Y]; if (value != Float.MIN_VALUE) { builder.translationY(value); } values = multiValueProcess[TRANSLATION_Y]; if (values != null) { builder.translationY(createNewValues(values, getView(builder).getTranslationY())); } value = process[X]; if (value != Float.MIN_VALUE) { builder.x(value); } values = multiValueProcess[X]; if (values != null) { builder.x(createNewValues(values, getView(builder).getX())); } value = process[Y]; if (value != Float.MIN_VALUE) { builder.y(value); } values = multiValueProcess[Y]; if (values != null) { builder.y(createNewValues(values, getView(builder).getY())); } value = process[VISIBILITY]; if (value != Float.MIN_VALUE) { if (cachedViewState == -1) { cachedViewState = view.getVisibility(); view.setVisibility((int) value); } else { view.setVisibility(cachedViewState); } } value = process[TRANSLATION_X_AS_FRACTION_OF_WIDTH]; if (value != Float.MIN_VALUE) { builder.translationX(getView(builder).getWidth() * value); } values = multiValueProcess[TRANSLATION_X_AS_FRACTION_OF_WIDTH]; if (values != null) { float[] widths = new float[values.length + 1]; int width = getView(builder).getWidth(); widths[0] = width; for (int i = 0; i < values.length; i++) { widths[i + 1] = width * values[i]; } builder.translationX(widths); } value = process[TRANSLATION_Y_AS_FRACTION_OF_HEIGHT]; if (value != Float.MIN_VALUE) { builder.translationY(getView(builder).getHeight() * value); } values = multiValueProcess[TRANSLATION_Y_AS_FRACTION_OF_HEIGHT]; if (values != null) { float[] heights = new float[values.length + 1]; int height = getView(builder).getHeight(); heights[0] = height; for (int i = 0; i < values.length; i++) { heights[i + 1] = height * values[i]; } builder.translationY(heights); } } void addProcess(int type, int value) { process[type] = value; } void addProcess(int type, float value) { process[type] = value; } public void addProcess(int type, float[] values) { multiValueProcess[type] = values; } @CheckResult protected DelayedProcessor clone() { try { DelayedProcessor dp = (DelayedProcessor) super.clone(); dp.process = new float[TOTAL]; System.arraycopy(process, 0, dp.process, 0, process.length); //dp.process[VISIBILITY] = Float.MIN_VALUE; return dp; } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } } /** * Holds values so the reversed version of PropertyValuesHolder can be created. */ static class ShadowValuesHolder implements Cloneable { String property; int[] iVals; float[] fVals; boolean isFloat; ShadowValuesHolder(@NonNull String property, float... vals) { this.property = property; fVals = vals; isFloat = true; } ShadowValuesHolder(@NonNull String property, int... vals) { this.property = property; iVals = vals; } static ShadowValuesHolder ofFloat(@NonNull String property, float... vals) { return new ShadowValuesHolder(property, vals); } static ShadowValuesHolder ofInt(@NonNull String property, int... vals) { return new ShadowValuesHolder(property, vals); } @CheckResult PropertyValuesHolder createReverse() { final int max; if (isFloat) { float[] newfVals = new float[fVals.length]; max = fVals.length; for (int i = 0; i < max; i++) { newfVals[i] = fVals[max - i - 1]; } return PropertyValuesHolder.ofFloat(property, newfVals); } else { int[] newiVals = new int[iVals.length]; max = iVals.length; for (int i = 0; i < max; i++) { newiVals[i] = iVals[max - i - 1]; } return PropertyValuesHolder.ofInt(property, newiVals); } } @CheckResult protected ShadowValuesHolder clone() { try { return (ShadowValuesHolder) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } } /** * Used to delay the evaluation to the time when transition is about to start, so the current state * of the target view can be used in the evaluation. */ public interface DelayedEvaluator<T extends AbstractTransitionBuilder> { /** * Evaluate builder with the given View's states. * * @param view * @param builder */ void evaluate(View view, T builder); } }