/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.plaidapp.ui.transitions;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.transition.Transition;
import android.transition.TransitionValues;
import android.util.AttributeSet;
import android.util.Property;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import io.plaidapp.R;
import io.plaidapp.util.AnimUtils;
/**
* A transition for animating a move/resize of a solid color rectangle. The target view's background
* is hidden and a fake drawable is placed in the view's overlay for the duration of the transition.
* The leading and trailing edges animate with different durations and interpolators,
* creating a stretch effect.
*/
public class StretchyChangeBounds extends Transition {
private static final String PROPNAME_BOUNDS = "plaid:stretchychangebounds:bounds";
private static final String[] PROPERTIES = { PROPNAME_BOUNDS };
private @ColorInt int color = Color.MAGENTA;
private float trailingSpeed = 0.7f; // DPs per ms
private long minTrailingDuration = 200; // ms
private long maxTrailingDuration = 400; // ms
private long leadingDuration = 200; // ms
public StretchyChangeBounds(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StretchyChangeBounds);
color = a.getColor(R.styleable.StretchyChangeBounds_android_color, color);
trailingSpeed = a.getFloat(R.styleable.StretchyChangeBounds_trailingSpeed, trailingSpeed);
minTrailingDuration = a.getInt(R.styleable.StretchyChangeBounds_minTrailingDuration,
(int) minTrailingDuration);
maxTrailingDuration = a.getInt(R.styleable.StretchyChangeBounds_maxTrailingDuration,
(int) maxTrailingDuration);
leadingDuration = a.getInt(R.styleable.StretchyChangeBounds_leadingDuration,
(int) leadingDuration);
a.recycle();
}
@Override
public String[] getTransitionProperties() {
return PROPERTIES;
}
@Override
public void captureStartValues(TransitionValues transitionValues) {
captureValues(transitionValues);
}
@Override
public void captureEndValues(TransitionValues transitionValues) {
captureValues(transitionValues);
}
private void captureValues(TransitionValues transitionValues) {
transitionValues.values.put(PROPNAME_BOUNDS, getBoundsInWindow(transitionValues.view));
}
/**
* Because we use this transition on a view whose parent is also transitioning, we capture
* bounds in window co-ordinates, so that they are not relative to a shifting point.
*/
@NonNull
private Rect getBoundsInWindow(View view) {
int[] loc = new int[2];
view.getLocationInWindow(loc);
int y = Math.max(loc[1], 0);
return new Rect(loc[0], y,
loc[0] + view.getWidth(),
y + view.getHeight() + (int) view.getTranslationY());
}
@Override
public Animator createAnimator(ViewGroup sceneRoot,
TransitionValues startValues,
TransitionValues endValues) {
final View view = endValues.view;
final ViewGroup parent = ((ViewGroup) view.getParent());
final Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS);
final Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS);
// as the captured bounds are in window-space, adjust them to local bounds
int dx = Math.max(view.getLeft(), 0) - endBounds.left;
int dy = Math.max(view.getTop(), 0) - endBounds.top;
startBounds.offset(dx, dy);
endBounds.offset(dx, dy);
// hide the view during the transition and let us draw outside of our bounds
final Drawable background = view.getBackground();
view.setBackground(null);
final ViewOutlineProvider outlineProvider = view.getOutlineProvider();
view.setOutlineProvider(null);
final boolean clipChildren = parent.getClipChildren();
parent.setClipChildren(false);
// use our own drawable in the overlay which we can reposition without thrashing layout
StretchColorDrawable drawable = new StretchColorDrawable(color);
drawable.setBounds(startBounds);
view.getOverlay().add(drawable);
// work out the direction and size change,
// use this to determine which edges are leading vs trailing.
boolean upward = startBounds.centerY() > endBounds.centerY();
boolean expanding = startBounds.width() < endBounds.width();
Interpolator fastOutSlowInInterpolator =
AnimUtils.getFastOutSlowInInterpolator(sceneRoot.getContext());
Interpolator slowOutFastInInterpolator = AnimationUtils.loadInterpolator(
sceneRoot.getContext(), R.interpolator.slow_out_fast_in);
AnimatorSet transition = new AnimatorSet();
long trailingDuration =
calculateTrailingDuration(startBounds, endBounds, sceneRoot.getContext());
Animator leadingEdges, trailingEdges;
if (expanding) {
// expanding, left/right move at speed of leading edge
PropertyValuesHolder left = PropertyValuesHolder.ofInt(StretchColorDrawable.LEFT,
startBounds.left, endBounds.left);
PropertyValuesHolder right = PropertyValuesHolder.ofInt(StretchColorDrawable.RIGHT,
startBounds.right, endBounds.right);
PropertyValuesHolder leadingEdge = PropertyValuesHolder.ofInt(
upward ? StretchColorDrawable.TOP : StretchColorDrawable.BOTTOM,
upward ? startBounds.top : startBounds.bottom,
upward ? endBounds.top : endBounds.bottom);
leadingEdges =
ObjectAnimator.ofPropertyValuesHolder(drawable, left, right, leadingEdge);
leadingEdges.setDuration(leadingDuration);
leadingEdges.setInterpolator(fastOutSlowInInterpolator);
trailingEdges = ObjectAnimator.ofInt(drawable,
upward ? StretchColorDrawable.BOTTOM : StretchColorDrawable.TOP,
upward ? startBounds.bottom : startBounds.top,
upward ? endBounds.bottom : endBounds.top);
trailingEdges.setDuration(trailingDuration);
trailingEdges.setInterpolator(slowOutFastInInterpolator);
} else {
// contracting, left/right move at speed of trailing edge
leadingEdges =
ObjectAnimator.ofInt(drawable,
upward ? StretchColorDrawable.TOP : StretchColorDrawable.BOTTOM,
upward ? startBounds.top : startBounds.bottom,
upward ? endBounds.top : endBounds.bottom);
leadingEdges.setDuration(leadingDuration);
leadingEdges.setInterpolator(fastOutSlowInInterpolator);
PropertyValuesHolder left = PropertyValuesHolder.ofInt(StretchColorDrawable.LEFT,
startBounds.left, endBounds.left);
PropertyValuesHolder right = PropertyValuesHolder.ofInt(StretchColorDrawable.RIGHT,
startBounds.right, endBounds.right);
PropertyValuesHolder trailingEdge = PropertyValuesHolder.ofInt(
upward ? StretchColorDrawable.BOTTOM : StretchColorDrawable.TOP,
upward ? startBounds.bottom : startBounds.top,
upward ? endBounds.bottom : endBounds.top);
trailingEdges =
ObjectAnimator.ofPropertyValuesHolder(drawable, left, right, trailingEdge);
trailingEdges.setDuration(trailingDuration);
trailingEdges.setInterpolator(slowOutFastInInterpolator);
}
transition.playTogether(leadingEdges, trailingEdges);
transition.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// clean up
parent.setClipChildren(clipChildren);
view.setBackground(background);
view.setOutlineProvider(outlineProvider);
view.getOverlay().clear();
}
});
return transition;
}
@Override
public Transition setDuration(long duration) {
/* don't call super as we want to handle duration ourselves */
return this;
}
/**
* Calculate the duration for the transition depending upon how far we're moving.
*/
private long calculateTrailingDuration(
@NonNull Rect startPosition,
@NonNull Rect endPosition,
@NonNull Context context) {
if (minTrailingDuration == maxTrailingDuration) return minTrailingDuration;
float pxDistance = (float) Math.hypot(
startPosition.exactCenterX() - endPosition.exactCenterX(),
startPosition.exactCenterY() - endPosition.exactCenterY());
float dpDistance = pxDistance / context.getResources().getDisplayMetrics().density;
long duration = (long) (dpDistance / trailingSpeed);
return Math.max(minTrailingDuration, Math.min(maxTrailingDuration, duration));
}
/**
* An extension to {@link ColorDrawable} with convenience methods and properties for easily
* animating its position and size.
*/
private static class StretchColorDrawable extends ColorDrawable {
static final Property<StretchColorDrawable, Integer> LEFT
= AnimUtils.createIntProperty(new AnimUtils.IntProp<StretchColorDrawable>("left") {
@Override
public void set(StretchColorDrawable drawable, int left) {
drawable.setLeft(left);
}
@Override
public int get(StretchColorDrawable drawable) {
return drawable.getLeft();
}
});
static final Property<StretchColorDrawable, Integer> TOP
= AnimUtils.createIntProperty(new AnimUtils.IntProp<StretchColorDrawable>("top") {
@Override
public void set(StretchColorDrawable drawable, int top) {
drawable.setTop(top);
}
@Override
public int get(StretchColorDrawable drawable) {
return drawable.getTop();
}
});
static final Property<StretchColorDrawable, Integer> RIGHT
= AnimUtils.createIntProperty(new AnimUtils.IntProp<StretchColorDrawable>("right") {
@Override
public void set(StretchColorDrawable drawable, int right) {
drawable.setRight(right);
}
@Override
public int get(StretchColorDrawable drawable) {
return drawable.getRight();
}
});
static final Property<StretchColorDrawable, Integer> BOTTOM
= AnimUtils.createIntProperty(new AnimUtils.IntProp<StretchColorDrawable>("bottom") {
@Override
public void set(StretchColorDrawable drawable, int bottom) {
drawable.setBottom(bottom);
}
@Override
public int get(StretchColorDrawable drawable) {
return drawable.getBottom();
}
});
private int left, top, right, bottom;
StretchColorDrawable(@ColorInt int color) {
super(color);
}
int getLeft() {
return left;
}
void setLeft(int left) {
this.left = left;
updateBounds();
}
int getTop() {
return top;
}
void setTop(int top) {
this.top = top;
updateBounds();
}
int getRight() {
return right;
}
void setRight(int right) {
this.right = right;
updateBounds();
}
int getBottom() {
return bottom;
}
void setBottom(int bottom) {
this.bottom = bottom;
updateBounds();
}
private void updateBounds() {
setBounds(left, top, right, bottom);
invalidateSelf();
}
}
}