blob: 0d25724af5bf2b70b1175ab30e92c513f86e9ace [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* 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 com.android.tv.settings.util;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.graphics.RectF;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* Class used by an activity to perform animation of multiple TransitionImageViews
* Usage:
* - on activity create:
* TransitionImageAnimation animation = new TransitionImageAnimation(rootView);
* for_each TransitionImage of source
* animation.addTransitionSource(source);
* animation.startCancelTimer();
* - When the activity loads all target images
* for_each TransitionImage of target
* animation.addTransitionTarget(target);
* animation.startTransition();
*/
public class TransitionImageAnimation {
public static class Listener {
public void onRemovedView(TransitionImage src, TransitionImage dst) {
}
public void onCancelled(TransitionImageAnimation animation) {
}
public void onFinished(TransitionImageAnimation animation) {
}
}
private static final long DEFAULT_TRANSITION_TIMEOUT_MS = 2000;
private static final long DEFAULT_CANCEL_TRANSITION_MS = 250;
private static final long DEFAULT_TRANSITION_DURATION_MS = 250;
private static final long DEFAULT_TRANSITION_START_DELAY_MS = 160;
private Interpolator mInterpolator = new DecelerateInterpolator();
private final ViewGroup mRoot;
private long mTransitionTimeoutMs = DEFAULT_TRANSITION_TIMEOUT_MS;
private long mCancelTransitionMs = DEFAULT_CANCEL_TRANSITION_MS;
private long mTransitionDurationMs = DEFAULT_TRANSITION_DURATION_MS;
private long mTransitionStartDelayMs = DEFAULT_TRANSITION_START_DELAY_MS;
private final List<TransitionImageView> mTransitions = new ArrayList<>();
private Listener mListener;
private Comparator<TransitionImage> mComparator = new TransitionImageMatcher();
private static final int STATE_INITIAL = 0;
private static final int STATE_WAIT_DST = 1;
private static final int STATE_TRANSITION = 2;
private static final int STATE_CANCELLED = 3;
private static final int STATE_FINISHED = 4;
private int mState;
private boolean mListeningLayout;
private static final RectF sTmpRect1 = new RectF();
private static final RectF sTmpRect2 = new RectF();
public TransitionImageAnimation(ViewGroup root) {
mRoot = root;
mState = STATE_INITIAL;
}
/**
* Set listener for animation events
*/
public TransitionImageAnimation listener(Listener listener) {
mListener = listener;
return this;
}
public Listener getListener() {
return mListener;
}
/**
* set comparator for matching src and dst ImageTransition
*/
public TransitionImageAnimation comparator(Comparator<TransitionImage> comparator) {
mComparator = comparator;
return this;
}
public Comparator<TransitionImage> getComparator() {
return mComparator;
}
/**
* set interpolator used for animating the Transition
*/
public TransitionImageAnimation interpolator(Interpolator interpolator) {
mInterpolator = interpolator;
return this;
}
public Interpolator getInterpolator() {
return mInterpolator;
}
/**
* set timeout in ms for {@link #startCancelTimer}
*/
public TransitionImageAnimation timeoutMs(long timeoutMs) {
mTransitionTimeoutMs = timeoutMs;
return this;
}
public long getTimeoutMs() {
return mTransitionTimeoutMs;
}
/**
* set duration of fade out animation when cancel the transition
*/
public TransitionImageAnimation cancelDurationMs(long ms) {
mCancelTransitionMs = ms;
return this;
}
public long getCancelDurationMs() {
return mCancelTransitionMs;
}
/**
* set start delay of transition animation
*/
public TransitionImageAnimation transitionStartDelayMs(long delay) {
mTransitionStartDelayMs = delay;
return this;
}
public long getTransitionStartDelayMs() {
return mTransitionStartDelayMs;
}
/**
* set duration of transition animation
*/
public TransitionImageAnimation transitionDurationMs(long duration) {
mTransitionDurationMs = duration;
return this;
}
public long getTransitionDurationMs() {
return mTransitionDurationMs;
}
/**
* add source transition and create initial view in root
*/
public void addTransitionSource(TransitionImage src) {
if (mState != STATE_INITIAL) {
return;
}
TransitionImageView view = new TransitionImageView(mRoot.getContext());
mRoot.addView(view);
view.setSourceTransition(src);
mTransitions.add(view);
if (!mListeningLayout) {
mListeningLayout = true;
mRoot.addOnLayoutChangeListener(mInitializeClip);
}
}
/**
* kick off the timer for cancel transition
*/
public void startCancelTimer() {
if (mState != STATE_INITIAL) {
return;
}
mRoot.postDelayed(mCancelTransitionRunnable, mTransitionTimeoutMs);
mState = STATE_WAIT_DST;
}
private final Runnable mCancelTransitionRunnable = new Runnable() {
@Override
public void run() {
cancelTransition();
}
};
private void setProgress(float progress) {
// draw from last child (top most in z-order)
int lastIndex = mTransitions.size() - 1;
for (int i = lastIndex; i >= 0; i--) {
TransitionImageView view = mTransitions.get(i);
view.setProgress(progress);
sTmpRect2.left = 0;
sTmpRect2.top = 0;
sTmpRect2.right = view.getWidth();
sTmpRect2.bottom = view.getHeight();
WindowLocationUtil.getLocationsInWindow(view, sTmpRect2);
if (i == lastIndex) {
view.clearExcludeClipRect();
sTmpRect1.set(sTmpRect2);
} else {
view.setExcludeClipRect(sTmpRect1);
// FIXME: this assumes 3rd image will be clipped by "1st union 2nd",
// applies to certain situation such as images are stacked in one row
sTmpRect1.union(sTmpRect2);
}
view.invalidate();
}
}
private final View.OnLayoutChangeListener mInitializeClip = new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
v.removeOnLayoutChangeListener(this);
mListeningLayout = false;
// set initial clipping for all views
setProgress(0f);
}
};
/**
* start transition
*/
public void startTransition() {
if (mState != STATE_WAIT_DST && mState != STATE_INITIAL) {
return;
}
for (int i = mTransitions.size() - 1; i >= 0; i--) {
TransitionImageView view = mTransitions.get(i);
if (view.getDestTransition() == null) {
cancelTransition(view);
mTransitions.remove(i);
}
}
if (mTransitions.size() == 0) {
mState = STATE_CANCELLED;
if (mListener != null) {
mListener.onCancelled(this);
}
return;
}
ValueAnimator v = ValueAnimator.ofFloat(0f, 1f);
v.setInterpolator(mInterpolator);
v.setDuration(mTransitionDurationMs);
v.setStartDelay(mTransitionStartDelayMs);
v.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float progress = animation.getAnimatedFraction();
setProgress(progress);
}
});
v.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
for (int i = 0, count = mTransitions.size(); i < count; i++) {
final TransitionImageView view = mTransitions.get(i);
if (mListener != null) {
mListener.onRemovedView(view.getSourceTransition(),
view.getDestTransition());
}
mRoot.removeView(view);
}
mTransitions.clear();
mState = STATE_FINISHED;
if (mListener != null) {
mListener.onFinished(TransitionImageAnimation.this);
}
}
});
v.start();
mState = STATE_TRANSITION;
}
private void cancelTransition(final View iv) {
iv.animate().alpha(0f).setDuration(mCancelTransitionMs).
setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator arg0) {
mRoot.removeView(iv);
}
}).start();
}
/**
* Cancel the transition before it starts, no effect if it already starts
*/
public void cancelTransition() {
if (mState != STATE_WAIT_DST && mState != STATE_INITIAL) {
return;
}
int count = mTransitions.size();
if (count > 0) {
for (int i = 0; i < count; i++) {
cancelTransition(mTransitions.get(i));
}
mTransitions.clear();
}
mState = STATE_CANCELLED;
if (mListener != null) {
mListener.onCancelled(this);
}
}
/**
* find a matching source and relate it with destination
*/
public boolean addTransitionTarget(TransitionImage dst) {
if (mState != STATE_WAIT_DST && mState != STATE_INITIAL) {
return false;
}
for (int i = 0, count = mTransitions.size(); i < count; i++) {
TransitionImageView view = mTransitions.get(i);
if (mComparator.compare(view.getSourceTransition(), dst) == 0) {
view.setDestTransition(dst);
return true;
}
}
return false;
}
}