/* * 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 long DEFAULT_TRANSITION_TIMEOUT_MS = 2000; private static long DEFAULT_CANCEL_TRANSITION_MS = 250; private static long DEFAULT_TRANSITION_DURATION_MS = 250; private static long DEFAULT_TRANSITION_START_DELAY_MS = 160; private Interpolator mInterpolator = new DecelerateInterpolator(); private 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 List mTransitions = new ArrayList(); private Listener mListener; private Comparator 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 RectF sTmpRect1 = new RectF(); private static 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 comparator) { mComparator = comparator; return this; } public Comparator 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 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 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; } }