package com.kaichunlin.transition;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.support.annotation.CheckResult;
import android.support.annotation.ColorInt;
import android.support.annotation.ColorRes;
import android.support.annotation.FloatRange;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import com.kaichunlin.transition.internal.CustomTransitionController;
import com.kaichunlin.transition.internal.DefaultTransitionController;
import com.kaichunlin.transition.internal.TransitionController;
import com.kaichunlin.transition.internal.TransitionControllerManager;
import com.kaichunlin.transition.transformer.ScaledTransformer;
import com.kaichunlin.transition.transformer.ViewTransformer;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.AnimatorInflater;
import com.nineoldandroids.animation.AnimatorSet;
import com.nineoldandroids.animation.ArgbEvaluator;
import com.nineoldandroids.animation.ObjectAnimator;
import com.nineoldandroids.view.ViewHelper;
import java.util.ArrayList;
import java.util.List;
/**
* Builder for {@link ViewTransition}.
*/
public class ViewTransitionBuilder extends AbstractTransitionBuilder<ViewTransitionBuilder, ViewTransition> implements ViewTransition.Setup {
private static final String TAG = "ViewTransitionBuilder";
protected static final int WIDTH = 0;
protected static final int HEIGHT = 1;
protected static final int TRANSLATION_X_AS_FRACTION_OF_WIDTH_WITH_VIEW = 2;
protected static final int TRANSLATION_X_AS_FRACTIONS_OF_WIDTH_WITH_VIEW = 3;
protected static final int TRANSLATION_Y_AS_FRACTION_OF_HEIGHT_WITH_VIEW = 4;
protected static final int TRANSLATION_Y_AS_FRACTIONS_OF_HEIGHT_WITH_VIEW = 5;
protected static final int DELAYED_TOTAL = 6;
/**
* Creates a {@link ViewTransitionBuilder} instance without any target view, {@link #target(View)}
* should be called before build* is called.
*
* @return
*/
public static ViewTransitionBuilder transit() {
return new ViewTransitionBuilder();
}
/**
* Creates a {@link ViewTransitionBuilder} instance with the target view set.
*
* @param view
* @return
*/
public static ViewTransitionBuilder transit(@Nullable View view) {
return new ViewTransitionBuilder(view);
}
private ViewTransitionDelayedEvaluation mViewDelayedProcessor;
private CustomTransitionController mCustomTransitionController;
private List<ViewTransition.Setup> mSetupList = new ArrayList<>();
private View mView;
private ViewTransitionBuilder() {
}
private ViewTransitionBuilder(@Nullable View view) {
if (view == null) {
Log.w(TAG, "Null view is provided, may cause exception");
}
mView = view;
}
/**
* @param view The view the created {@link ViewTransition} should manipulate.
* @return
*/
public ViewTransitionBuilder target(@Nullable View view) {
if (view == null) {
Log.w(TAG, "Null view is provided, may cause exception");
}
mView = view;
return self();
}
public View getTargetView() {
return mView;
}
/**
* Adds a custom {@link ViewTransformer}, the builder must be cloned if this method is called.
*
* @param viewTransformer
* @return
*/
public ViewTransitionBuilder addViewTransformer(@NonNull ViewTransformer viewTransformer) {
checkModifiability();
if (mCustomTransitionController == null) {
mCustomTransitionController = new CustomTransitionController();
}
mCustomTransitionController.addViewTransformer(viewTransformer);
return self();
}
@Override
public ViewTransitionBuilder alpha(@FloatRange(from = 0.0, to = 1.0) float end) {
return alpha(ViewHelper.getAlpha(mView), end);
}
@Override
public ViewTransitionBuilder rotation(float end) {
return rotation(ViewHelper.getRotation(mView), end);
}
@Override
public ViewTransitionBuilder rotationX(float end) {
return rotationX(ViewHelper.getRotationX(mView), end);
}
@Override
public ViewTransitionBuilder rotationY(float end) {
return rotationY(ViewHelper.getRotationY(mView), end);
}
@Override
public ViewTransitionBuilder scaleX(@FloatRange(from = 0.0) float end) {
return scaleX(ViewHelper.getScaleX(mView), end);
}
@Override
public ViewTransitionBuilder scaleY(@FloatRange(from = 0.0) float end) {
return scaleY(ViewHelper.getScaleY(mView), end);
}
@Override
public ViewTransitionBuilder scale(@FloatRange(from = 0.0) float end) {
return scaleX(ViewHelper.getScaleX(mView), end).scaleY(ViewHelper.getScaleY(mView), end);
}
@Override
public ViewTransitionBuilder translationX(float end) {
return translationX(ViewHelper.getTranslationX(mView), end);
}
/**
* @param fraction
* @return
*/
public ViewTransitionBuilder translationXAsFractionOfWidth(float fraction) {
return translationX(mView.getWidth() * fraction);
}
/**
* @param widthFractions
* @return
*/
public ViewTransitionBuilder translationXAsFractionOfWidth(float... widthFractions) {
int width = mView.getWidth();
for (int i = 0, size = widthFractions.length; i < size; i++) {
widthFractions[i] = width * widthFractions[i];
}
return translationX(widthFractions);
}
/**
* @param fraction
* @return
*/
public ViewTransitionBuilder delayTranslationXAsFractionOfWidth(float fraction) {
getDelayedProcessor().addProcess(TRANSLATION_X_AS_FRACTION_OF_WIDTH, fraction);
return self();
}
/**
* @param fractions
* @return
*/
public ViewTransitionBuilder delayTranslationXAsFractionOfWidth(float... fractions) {
getDelayedProcessor().addProcess(TRANSLATION_X_AS_FRACTION_OF_WIDTH, fractions);
return self();
}
/**
* @param targetView
* @param fraction
* @return
*/
public ViewTransitionBuilder translationXAsFractionOfWidth(@NonNull final View targetView, float fraction) {
return translationX(targetView.getWidth() * fraction);
}
/**
* @param targetView
* @param widthFractions
* @return
*/
public ViewTransitionBuilder translationXAsFractionOfWidth(@NonNull final View targetView, float... widthFractions) {
int width = targetView.getWidth();
for (int i = 0, size = widthFractions.length; i < size; i++) {
widthFractions[i] = width * widthFractions[i];
}
return translationX(widthFractions);
}
/**
* @param targetView
* @param fraction
* @return
*/
public ViewTransitionBuilder delayTranslationXAsFractionOfWidth(@NonNull final View targetView, final float fraction) {
getViewDelayedProcessor().addProcess(TRANSLATION_X_AS_FRACTION_OF_WIDTH_WITH_VIEW, targetView, fraction);
return self();
}
/**
* @param targetView
* @param widthFractions
* @return
*/
public ViewTransitionBuilder delayTranslationXAsFractionOfWidth(@NonNull final View targetView, final float... widthFractions) {
getViewDelayedProcessor().addProcess(TRANSLATION_X_AS_FRACTIONS_OF_WIDTH_WITH_VIEW, targetView, widthFractions);
return self();
}
@Override
public ViewTransitionBuilder translationY(float end) {
return translationY(ViewHelper.getTranslationY(mView), end);
}
/**
* @param fraction
* @return
*/
public ViewTransitionBuilder translationYAsFractionOfHeight(float fraction) {
return translationY(mView.getHeight() * fraction);
}
/**
* @param heightFractions
* @return
*/
public ViewTransitionBuilder translationYAsFractionOfHeight(float... heightFractions) {
int height = mView.getHeight();
for (int i = 0, size = heightFractions.length; i < size; i++) {
heightFractions[i] = height * heightFractions[i];
}
return translationY(heightFractions);
}
/**
* @param fraction
* @return
*/
public ViewTransitionBuilder delayTranslationYAsFractionOfHeight(float fraction) {
getDelayedProcessor().addProcess(TRANSLATION_Y_AS_FRACTION_OF_HEIGHT, fraction);
return self();
}
/**
* @param heightFractions
* @return
*/
public ViewTransitionBuilder delayTranslationYAsFractionOfHeight(final float... heightFractions) {
getDelayedProcessor().addProcess(TRANSLATION_Y_AS_FRACTION_OF_HEIGHT, heightFractions);
return self();
}
/**
* @param targetView
* @param fraction
* @return
*/
public ViewTransitionBuilder translationYAsFractionOfHeight(@NonNull final View targetView, final float fraction) {
return translationY(targetView.getHeight() * fraction);
}
/**
* @param targetView
* @param heightFractions
* @return
*/
public ViewTransitionBuilder translationYAsFractionOfHeight(@NonNull final View targetView, final float... heightFractions) {
int height = targetView.getHeight();
for (int i = 0, size = heightFractions.length; i < size; i++) {
heightFractions[i] = height * heightFractions[i];
}
return translationY(heightFractions);
}
/**
* @param targetView
* @param fraction
* @return
*/
public ViewTransitionBuilder delayTranslationYAsFractionOfHeight(@NonNull final View targetView, final float fraction) {
getViewDelayedProcessor().addProcess(TRANSLATION_Y_AS_FRACTION_OF_HEIGHT_WITH_VIEW, targetView, fraction);
return self();
}
/**
* @param targetView
* @param heightFractions
* @return
*/
public ViewTransitionBuilder delayTranslationYAsFractionOfHeight(@NonNull final View targetView, final float... heightFractions) {
getViewDelayedProcessor().addProcess(TRANSLATION_Y_AS_FRACTIONS_OF_HEIGHT_WITH_VIEW, targetView, heightFractions);
return self();
}
@Override
public ViewTransitionBuilder x(float end) {
return x(ViewHelper.getX(mView), end);
}
@Override
public ViewTransitionBuilder y(float end) {
return y(ViewHelper.getY(mView), end);
}
public ViewTransitionBuilder width(@IntRange(from = 0) final int targetWidth) {
width(mView.getWidth(), targetWidth);
return self();
}
public ViewTransitionBuilder width(@IntRange(from = 0) final int fromWidth, @IntRange(from = 0) final int targetWidth) {
addViewTransformer(new WidthTransformer(fromWidth, targetWidth));
return self();
}
public ViewTransitionBuilder delayWidth(@IntRange(from = 0) final int targetHeight) {
getViewDelayedProcessor().addProcess(WIDTH, targetHeight);
return self();
}
public ViewTransitionBuilder height(@IntRange(from = 0) final int targetHeight) {
height(mView.getHeight(), targetHeight);
return self();
}
public ViewTransitionBuilder height(@IntRange(from = 0) final int fromHeight, @IntRange(from = 0) final int targetHeight) {
addViewTransformer(new HeightTransformer(fromHeight, targetHeight));
return self();
}
public ViewTransitionBuilder delayHeight(@IntRange(from = 0) final int targetHeight) {
getViewDelayedProcessor().addProcess(HEIGHT, targetHeight);
return self();
}
@CheckResult
@Override
public ViewTransitionBuilder clone() {
ViewTransitionBuilder newCopy = (ViewTransitionBuilder) super.clone();
newCopy.mSetupList = new ArrayList<>(mSetupList.size());
newCopy.mSetupList.addAll(mSetupList);
if (mCustomTransitionController != null) {
newCopy.mCustomTransitionController = mCustomTransitionController.clone();
}
if (mDelayedProcessor != null) {
newCopy.mDelayedProcessor = mDelayedProcessor.clone();
newCopy.mDelayed.remove(mDelayedProcessor);
newCopy.mDelayed.add(newCopy.mDelayedProcessor);
}
return newCopy;
}
@Override
public ViewTransitionBuilder reverse() {
// super.reverse();
mHolders.clear();
for (int i = 0, size = mShadowHolders.size(); i < size; i++) {
mHolders.put(mShadowHolders.keyAt(i), mShadowHolders.valueAt(i).createReverse());
}
float oldStart = mStart;
mStart = mEnd;
mEnd = oldStart;
return self();
}
/**
* The builder must be cloned if this method is called.
*
* @param setup
* @return
*/
public ViewTransitionBuilder addSetup(@NonNull ViewTransition.Setup setup) {
checkModifiability();
mSetupList.add(setup);
return self();
}
/**
* @param res
* @param fromColorId
* @param toColorId
* @return
*/
public ViewTransitionBuilder backgroundColorResource(@NonNull Resources res, @ColorRes int fromColorId, @ColorRes int toColorId) {
return backgroundColor(res.getColor(fromColorId), res.getColor(toColorId));
}
/**
* @param fromColor
* @param toColor
* @return
*/
public ViewTransitionBuilder backgroundColor(@ColorInt final int fromColor, @ColorInt final int toColor) {
addSetup(new ViewTransition.Setup() {
@Override
public void setupAnimation(@NonNull final TransitionControllerManager manager) {
ObjectAnimator animator = ObjectAnimator.ofInt(manager.getTarget(), "backgroundColor", fromColor, toColor);
animator.setDuration(10_000);
animator.setEvaluator(new ArgbEvaluator());
manager.addTransitionController(DefaultTransitionController.wrapAnimator(animator));
}
});
return self();
}
/**
* @param res
* @param fromColorId
* @param toColorId
* @return
*/
public ViewTransitionBuilder backgroundColorResourceHSV(@NonNull Resources res, @ColorRes int fromColorId, @ColorRes int toColorId) {
return backgroundColorHSV(res.getColor(fromColorId), res.getColor(toColorId));
}
/**
* @param fromColor
* @param toColor
* @return
*/
public ViewTransitionBuilder backgroundColorHSV(@ColorInt final int fromColor, @ColorInt final int toColor) {
addViewTransformer(new BackgroundColorHsvTransformer(fromColor, toColor));
return self();
}
/**
* TODO Current support is rudimentary, may expand support if there are enough demand for this.
* <p>
* Converts an animator to ITransition when built, note that not all functions of Animator are supported.
* <p>
* Non-working functions: repeatMode, repeatCount, delay, duration (when in a set), Interpolator.
* <p>
* Furthermore, {@link #transitViewGroup(ViewGroupTransition)} does not work with this method.
*
* @param animator
* @return
*/
public ViewTransitionBuilder animator(@NonNull final Animator animator) {
addSetup(new ViewTransition.Setup() {
@Override
public void setupAnimation(@NonNull final TransitionControllerManager manager) {
Animator animator2 = animator.clone();
if (animator2 instanceof AnimatorSet) {
manager.addTransitionController(DefaultTransitionController.wrapAnimatorSet((AnimatorSet) animator2));
} else {
manager.addTransitionController(DefaultTransitionController.wrapAnimator(animator2));
}
}
});
return self();
}
/**
* See {@link #animator(Animator)}.
*
* @param context
* @param animatorId
* @return
*/
public ViewTransitionBuilder animator(@NonNull Context context, int animatorId) {
animator(AnimatorInflater.loadAnimator(context, animatorId));
return this;
}
/**
* The view previously set (through {@link #target(View)}) is casted as a ViewGroup, and the specified
* {@link ViewGroupTransition} will {@link ViewGroupTransition#transit(ViewTransitionBuilder, ViewGroupTransitionConfig)}
* all the children views.
*
* @param viewGroupTransition
* @return
* @throws ClassCastException If the target view is not a ViewGroup.
*/
public ViewTransitionBuilder transitViewGroup(@NonNull ViewGroupTransition viewGroupTransition) {
ViewGroup vg = (ViewGroup) mView;
int total = vg.getChildCount();
View view;
ViewGroupTransitionConfig config = new ViewGroupTransitionConfig(vg, total);
for (int i = 0; i < total; i++) {
view = vg.getChildAt(i);
config.childView = view;
config.index = i;
viewGroupTransition.transit(target(view), config);
}
return self();
}
/**
* Similar to {@link #transitViewGroup(ViewGroupTransition)}, but with builder.range() auto filled for each child View, how
* the start and end value is determined is defined by {@link Cascade} parameter.
*
* @param viewGroupTransition
* @param cascade
* @return
*/
public ViewTransitionBuilder transitViewGroup(@NonNull final ViewGroupTransition viewGroupTransition, @NonNull final Cascade cascade) {
transitViewGroup(new ViewGroupTransition() {
@Override
public void transit(ViewTransitionBuilder builder, ViewGroupTransitionConfig config) {
cascade.transit(builder, config); //sets up builder.range()
viewGroupTransition.transit(builder, config);
}
});
return self();
}
@CheckResult(suggest = "The created ViewTransition should be utilized")
@Override
protected ViewTransition createTransition() {
ViewTransition vt = new ViewTransition();
vt.setTarget(mView);
//TODO clone() is required since the class implements ViewTransition.Setup and passes itself to ViewTransition,
// without clone ViewTransitions made from the same Builder will have their states intertwined
vt.setSetup(clone());
return vt;
}
@Override
protected ViewTransitionBuilder self() {
return this;
}
@Override
public void setupAnimation(@NonNull TransitionControllerManager manager) {
if (mView == null) {
mView = manager.getTarget();
}
if (mDelayed != null) {
for (int i = 0, size = mDelayed.size(); i < size; i++) {
mDelayed.get(i).evaluate(manager.getTarget(), this);
}
}
for (int i = 0, size = mSetupList.size(); i < size; i++) {
mSetupList.get(i).setupAnimation(manager);
}
if (mCustomTransitionController != null) {
mCustomTransitionController.setTarget(manager.getTarget());
mCustomTransitionController.setRange(mStart, mEnd);
manager.addTransitionController(mCustomTransitionController.clone());
}
ObjectAnimator anim = new ObjectAnimator();
anim.setTarget(mView);
anim.setValues(getValuesHolders());
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(anim);
animatorSet.setDuration(SCALE_FACTOR);
manager.addAnimatorSetAsTransition(mView, animatorSet).setRange(mStart, mEnd);
}
protected ViewTransitionDelayedEvaluation getViewDelayedProcessor() {
if (mViewDelayedProcessor == null) {
mViewDelayedProcessor = new ViewTransitionDelayedEvaluation();
addDelayedEvaluator(mViewDelayedProcessor);
}
return mViewDelayedProcessor;
}
/**
* Performs evaluation on delayed operations.
*/
protected static class ViewTransitionDelayedEvaluation implements DelayedEvaluator<ViewTransitionBuilder> {
View[] views;
float[] process;
float[][] process2;
ViewTransitionDelayedEvaluation() {
views = new View[DELAYED_TOTAL];
process = new float[DELAYED_TOTAL];
for (int i = 0; i < DELAYED_TOTAL; i++) {
process[i] = Float.MIN_VALUE;
}
process2 = new float[DELAYED_TOTAL][];
}
@Override
public void evaluate(View view, ViewTransitionBuilder builder) {
View targetView;
float value;
float[] orgValues;
float[] newValues;
value = process[WIDTH];
if (value != Float.MIN_VALUE) {
builder.width((int) value);
}
value = process[HEIGHT];
if (value != Float.MIN_VALUE) {
builder.height((int) value);
}
value = process[TRANSLATION_X_AS_FRACTION_OF_WIDTH_WITH_VIEW];
if (value != Float.MIN_VALUE) {
builder.width((int) value);
builder.translationX(views[TRANSLATION_X_AS_FRACTION_OF_WIDTH_WITH_VIEW].getWidth() * value);
}
if (process2[TRANSLATION_X_AS_FRACTIONS_OF_WIDTH_WITH_VIEW] != null) {
orgValues = process2[TRANSLATION_X_AS_FRACTIONS_OF_WIDTH_WITH_VIEW];
newValues = new float[orgValues.length + 1];
targetView = views[TRANSLATION_X_AS_FRACTIONS_OF_WIDTH_WITH_VIEW];
int width = (targetView != null ? targetView : view).getWidth();
newValues[0] = width;
for (int i = 0; i < orgValues.length; i++) {
newValues[i + 1] = width * orgValues[i];
}
builder.translationX(newValues);
}
value = process[TRANSLATION_Y_AS_FRACTION_OF_HEIGHT_WITH_VIEW];
if (value != Float.MIN_VALUE) {
builder.height((int) value);
builder.translationY(views[TRANSLATION_Y_AS_FRACTION_OF_HEIGHT_WITH_VIEW].getHeight() * value);
}
if (process2[TRANSLATION_Y_AS_FRACTIONS_OF_HEIGHT_WITH_VIEW] != null) {
orgValues = process2[TRANSLATION_Y_AS_FRACTIONS_OF_HEIGHT_WITH_VIEW];
newValues = new float[orgValues.length + 1];
targetView = views[TRANSLATION_Y_AS_FRACTIONS_OF_HEIGHT_WITH_VIEW];
int height = (targetView != null ? targetView : view).getHeight();
newValues[0] = height;
for (int i = 0; i < orgValues.length; i++) {
newValues[i + 1] = height * orgValues[i];
}
builder.translationY(newValues);
}
}
void addProcess(int type, int value) {
process[type] = value;
}
void addProcess(int type, View view, int value) {
process[type] = value;
views[type] = view;
}
void addProcess(int type, float value) {
process[type] = value;
}
void addProcess(int type, View view, float value) {
process[type] = value;
views[type] = view;
}
public void addProcess(int type, View view, float[] values) {
process2[type] = values;
views[type] = view;
}
@CheckResult
protected DelayedProcessor clone() {
try {
DelayedProcessor dp = (DelayedProcessor) super.clone();
dp.process = new float[TOTAL];
System.arraycopy(process, 0, dp.process, 0, process.length);
return dp;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
/**
* Allows customized {@link ViewTransition} to be applied to each child view of a ViewGroup, see
* {@link #transitViewGroup(ViewGroupTransition)} / {@link #transitViewGroup(ViewGroupTransition, Cascade)}
*/
public interface ViewGroupTransition {
/**
* @param builder
*/
void transit(ViewTransitionBuilder builder, ViewGroupTransitionConfig config);
}
/**
* Encapsulates relevant data when transiting a ViewGroup, including total number of children,
* the current View being processed, and its position within the ViewGroup.
*/
public class ViewGroupTransitionConfig {
public final ViewGroup parentViewGroup;
public final int total;
// the child view to be transitioned
private View childView;
private int index;
/**
* @param viewGroup the parent ViewGroup this view belongs to
* @param total total number of children
*/
public ViewGroupTransitionConfig(ViewGroup viewGroup, int total) {
this.parentViewGroup = viewGroup;
this.total = total;
}
public ViewGroup getParentViewGroup() {
return parentViewGroup;
}
public View getChildView() {
return childView;
}
public int getIndex() {
return index;
}
public int getChildrenCount() {
return total;
}
}
private static class WidthTransformer extends ScaledTransformer {
private final int curWidth;
private final int targetWidth;
public WidthTransformer(int curWidth, int targetWidth) {
this.curWidth = curWidth;
this.targetWidth = targetWidth;
}
@Override
public void updateViewScaled(TransitionController controller, View target, float scaledProgress) {
if (controller.isReverse()) {
target.getLayoutParams().width = (int) ((curWidth - targetWidth) * scaledProgress + targetWidth);
} else {
target.getLayoutParams().width = (int) ((targetWidth - curWidth) * scaledProgress + curWidth);
}
target.requestLayout();
}
}
private static class HeightTransformer extends ScaledTransformer {
private final int curHeight;
private final int targetHeight;
public HeightTransformer(int curHeight, int targetHeight) {
this.curHeight = curHeight;
this.targetHeight = targetHeight;
}
@Override
public void updateViewScaled(TransitionController controller, View target, float scaledProgress) {
if (controller.isReverse()) {
target.getLayoutParams().height = (int) ((curHeight - targetHeight) * scaledProgress + targetHeight);
} else {
target.getLayoutParams().height = (int) ((targetHeight - curHeight) * scaledProgress + curHeight);
}
target.requestLayout();
}
}
private static class BackgroundColorHsvTransformer extends ScaledTransformer {
private final int fromColor;
private final int toColor;
public BackgroundColorHsvTransformer(int fromColor, int toColor) {
this.fromColor = fromColor;
this.toColor = toColor;
}
@Override
public void updateViewScaled(TransitionController controller, View target, float scaledProgress) {
//source: http://stackoverflow.com/questions/18216285/android-animate-color-change-from-color-to-color
final float[] from = new float[3],
to = new float[3];
Color.colorToHSV(fromColor, from);
Color.colorToHSV(toColor, to);
final float[] hsv = new float[3];
// Transition along each axis of HSV (hue, saturation, value)
hsv[0] = from[0] + (to[0] - from[0]) * scaledProgress;
hsv[1] = from[1] + (to[1] - from[1]) * scaledProgress;
hsv[2] = from[2] + (to[2] - from[2]) * scaledProgress;
target.setBackgroundColor(Color.HSVToColor(hsv));
}
}
}