1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv.settings.util;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ValueAnimator;
22 import android.animation.ValueAnimator.AnimatorUpdateListener;
23 import android.graphics.RectF;
24 import android.view.View;
25 import android.view.ViewGroup;
26 import android.view.animation.DecelerateInterpolator;
27 import android.view.animation.Interpolator;
28 
29 import java.util.ArrayList;
30 import java.util.Comparator;
31 import java.util.List;
32 
33 /**
34  * Class used by an activity to perform animation of multiple TransitionImageViews
35  * Usage:
36  * - on activity create:
37  *   TransitionImageAnimation animation = new TransitionImageAnimation(rootView);
38  *   for_each TransitionImage of source
39  *       animation.addTransitionSource(source);
40  *   animation.startCancelTimer();
41  * - When the activity loads all target images
42  *   for_each TransitionImage of target
43  *       animation.addTransitionTarget(target);
44  *   animation.startTransition();
45  */
46 public class TransitionImageAnimation {
47 
48     public static class Listener {
49 
onRemovedView(TransitionImage src, TransitionImage dst)50         public void onRemovedView(TransitionImage src, TransitionImage dst) {
51         }
52 
onCancelled(TransitionImageAnimation animation)53         public void onCancelled(TransitionImageAnimation animation) {
54         }
55 
onFinished(TransitionImageAnimation animation)56         public void onFinished(TransitionImageAnimation animation) {
57         }
58     }
59 
60     private static long DEFAULT_TRANSITION_TIMEOUT_MS = 2000;
61     private static long DEFAULT_CANCEL_TRANSITION_MS = 250;
62     private static long DEFAULT_TRANSITION_DURATION_MS = 250;
63     private static long DEFAULT_TRANSITION_START_DELAY_MS = 160;
64 
65     private Interpolator mInterpolator = new DecelerateInterpolator();
66     private ViewGroup mRoot;
67     private long mTransitionTimeoutMs = DEFAULT_TRANSITION_TIMEOUT_MS;
68     private long mCancelTransitionMs = DEFAULT_CANCEL_TRANSITION_MS;
69     private long mTransitionDurationMs = DEFAULT_TRANSITION_DURATION_MS;
70     private long mTransitionStartDelayMs = DEFAULT_TRANSITION_START_DELAY_MS;
71     private List<TransitionImageView> mTransitions = new ArrayList<TransitionImageView>();
72     private Listener mListener;
73     private Comparator<TransitionImage> mComparator = new TransitionImageMatcher();
74 
75     private static final int STATE_INITIAL = 0;
76     private static final int STATE_WAIT_DST = 1;
77     private static final int STATE_TRANSITION = 2;
78     private static final int STATE_CANCELLED = 3;
79     private static final int STATE_FINISHED = 4;
80     private int mState;
81 
82     private boolean mListeningLayout;
83     private static RectF sTmpRect1 = new RectF();
84     private static RectF sTmpRect2 = new RectF();
85 
TransitionImageAnimation(ViewGroup root)86     public TransitionImageAnimation(ViewGroup root) {
87         mRoot = root;
88         mState = STATE_INITIAL;
89     }
90 
91     /**
92      * Set listener for animation events
93      */
listener(Listener listener)94     public TransitionImageAnimation listener(Listener listener) {
95         mListener = listener;
96         return this;
97     }
98 
getListener()99     public Listener getListener() {
100         return mListener;
101     }
102 
103     /**
104      * set comparator for matching src and dst ImageTransition
105      */
comparator(Comparator<TransitionImage> comparator)106     public TransitionImageAnimation comparator(Comparator<TransitionImage> comparator) {
107         mComparator = comparator;
108         return this;
109     }
110 
getComparator()111     public Comparator<TransitionImage> getComparator() {
112         return mComparator;
113     }
114 
115     /**
116      * set interpolator used for animating the Transition
117      */
interpolator(Interpolator interpolator)118     public TransitionImageAnimation interpolator(Interpolator interpolator) {
119         mInterpolator = interpolator;
120         return this;
121     }
122 
getInterpolator()123     public Interpolator getInterpolator() {
124         return mInterpolator;
125     }
126 
127     /**
128      * set timeout in ms for {@link #startCancelTimer}
129      */
timeoutMs(long timeoutMs)130     public TransitionImageAnimation timeoutMs(long timeoutMs) {
131         mTransitionTimeoutMs = timeoutMs;
132         return this;
133     }
134 
getTimeoutMs()135     public long getTimeoutMs() {
136         return mTransitionTimeoutMs;
137     }
138 
139     /**
140      * set duration of fade out animation when cancel the transition
141      */
cancelDurationMs(long ms)142     public TransitionImageAnimation cancelDurationMs(long ms) {
143         mCancelTransitionMs = ms;
144         return this;
145     }
146 
getCancelDurationMs()147     public long getCancelDurationMs() {
148         return mCancelTransitionMs;
149     }
150 
151     /**
152      * set start delay of transition animation
153      */
transitionStartDelayMs(long delay)154     public TransitionImageAnimation transitionStartDelayMs(long delay) {
155         mTransitionStartDelayMs = delay;
156         return this;
157     }
158 
getTransitionStartDelayMs()159     public long getTransitionStartDelayMs() {
160         return mTransitionStartDelayMs;
161     }
162 
163     /**
164      * set duration of transition animation
165      */
transitionDurationMs(long duration)166     public TransitionImageAnimation transitionDurationMs(long duration) {
167         mTransitionDurationMs = duration;
168         return this;
169     }
170 
getTransitionDurationMs()171     public long getTransitionDurationMs() {
172         return mTransitionDurationMs;
173     }
174 
175     /**
176      * add source transition and create initial view in root
177      */
addTransitionSource(TransitionImage src)178     public void addTransitionSource(TransitionImage src) {
179         if (mState != STATE_INITIAL) {
180             return;
181         }
182         TransitionImageView view = new TransitionImageView(mRoot.getContext());
183         mRoot.addView(view);
184         view.setSourceTransition(src);
185         mTransitions.add(view);
186         if (!mListeningLayout) {
187             mListeningLayout = true;
188             mRoot.addOnLayoutChangeListener(mInitializeClip);
189         }
190     }
191 
192     /**
193      * kick off the timer for cancel transition
194      */
startCancelTimer()195     public void startCancelTimer() {
196         if (mState != STATE_INITIAL) {
197             return;
198         }
199         mRoot.postDelayed(mCancelTransitionRunnable, mTransitionTimeoutMs);
200         mState = STATE_WAIT_DST;
201     }
202 
203     private Runnable mCancelTransitionRunnable = new Runnable() {
204 
205         @Override
206         public void run() {
207             cancelTransition();
208         }
209 
210     };
211 
setProgress(float progress)212     private void setProgress(float progress) {
213         // draw from last child (top most in z-order)
214         int lastIndex = mTransitions.size() - 1;
215         for (int i = lastIndex; i >= 0; i--) {
216             TransitionImageView view = mTransitions.get(i);
217             view.setProgress(progress);
218             sTmpRect2.left = 0;
219             sTmpRect2.top = 0;
220             sTmpRect2.right = view.getWidth();
221             sTmpRect2.bottom = view.getHeight();
222             WindowLocationUtil.getLocationsInWindow(view, sTmpRect2);
223             if (i == lastIndex) {
224                 view.clearExcludeClipRect();
225                 sTmpRect1.set(sTmpRect2);
226             } else {
227                 view.setExcludeClipRect(sTmpRect1);
228                 // FIXME: this assumes 3rd image will be clipped by "1st union 2nd",
229                 // applies to certain situation such as images are stacked in one row
230                 sTmpRect1.union(sTmpRect2);
231             }
232             view.invalidate();
233         }
234     }
235 
236     private View.OnLayoutChangeListener mInitializeClip = new View.OnLayoutChangeListener() {
237         @Override
238         public void onLayoutChange(View v, int left, int top, int right, int bottom,
239                 int oldLeft, int oldTop, int oldRight, int oldBottom) {
240             v.removeOnLayoutChangeListener(this);
241             mListeningLayout = false;
242             // set initial clipping for all views
243             setProgress(0f);
244         }
245     };
246 
247     /**
248      * start transition
249      */
startTransition()250     public void startTransition() {
251         if (mState != STATE_WAIT_DST && mState != STATE_INITIAL) {
252             return;
253         }
254         for (int i = mTransitions.size() - 1; i >= 0; i--) {
255             TransitionImageView view = mTransitions.get(i);
256             if (view.getDestTransition() == null) {
257                 cancelTransition(view);
258                 mTransitions.remove(i);
259             }
260         }
261         if (mTransitions.size() == 0) {
262             mState = STATE_CANCELLED;
263             if (mListener != null) {
264                 mListener.onCancelled(this);
265             }
266             return;
267         }
268         ValueAnimator v = ValueAnimator.ofFloat(0f, 1f);
269         v.setInterpolator(mInterpolator);
270         v.setDuration(mTransitionDurationMs);
271         v.setStartDelay(mTransitionStartDelayMs);
272         v.addUpdateListener(new AnimatorUpdateListener() {
273             @Override
274             public void onAnimationUpdate(ValueAnimator animation) {
275                 float progress = animation.getAnimatedFraction();
276                 setProgress(progress);
277             }
278         });
279         v.addListener(new AnimatorListenerAdapter() {
280             @Override
281             public void onAnimationEnd(Animator animation) {
282                 for (int i = 0, count = mTransitions.size(); i < count; i++) {
283                     final TransitionImageView view = mTransitions.get(i);
284                     if (mListener != null) {
285                         mListener.onRemovedView(view.getSourceTransition(),
286                                 view.getDestTransition());
287                     }
288                     mRoot.removeView(view);
289                 }
290                 mTransitions.clear();
291                 mState = STATE_FINISHED;
292                 if (mListener != null) {
293                     mListener.onFinished(TransitionImageAnimation.this);
294                 }
295             }
296         });
297         v.start();
298         mState = STATE_TRANSITION;
299     }
300 
cancelTransition(final View iv)301     private void cancelTransition(final View iv) {
302         iv.animate().alpha(0f).setDuration(mCancelTransitionMs).
303             setListener(new AnimatorListenerAdapter() {
304             @Override
305             public void onAnimationEnd(Animator arg0) {
306                 mRoot.removeView(iv);
307             }
308         }).start();
309     }
310 
311     /**
312      * Cancel the transition before it starts, no effect if it already starts
313      */
cancelTransition()314     public void cancelTransition() {
315         if (mState != STATE_WAIT_DST && mState != STATE_INITIAL) {
316             return;
317         }
318         int count = mTransitions.size();
319         if (count > 0) {
320             for (int i = 0; i < count; i++) {
321                 cancelTransition(mTransitions.get(i));
322             }
323             mTransitions.clear();
324         }
325         mState = STATE_CANCELLED;
326         if (mListener != null) {
327             mListener.onCancelled(this);
328         }
329     }
330 
331     /**
332      * find a matching source and relate it with destination
333      */
addTransitionTarget(TransitionImage dst)334     public boolean addTransitionTarget(TransitionImage dst) {
335         if (mState != STATE_WAIT_DST && mState != STATE_INITIAL) {
336             return false;
337         }
338         for (int i = 0, count = mTransitions.size(); i < count; i++) {
339             TransitionImageView view = mTransitions.get(i);
340             if (mComparator.compare(view.getSourceTransition(), dst) == 0) {
341                 view.setDestTransition(dst);
342                 return true;
343             }
344         }
345         return false;
346     }
347 }
348