1 /*
2  * Copyright (C) 2015 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 package com.android.tv.common.ui.setup.animation;
17 
18 import android.animation.Animator;
19 import android.animation.AnimatorListenerAdapter;
20 import android.animation.AnimatorSet;
21 import android.animation.TimeInterpolator;
22 import android.transition.Fade;
23 import android.transition.Transition;
24 import android.transition.TransitionValues;
25 import android.transition.Visibility;
26 import android.view.Gravity;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import android.view.ViewParent;
30 import android.view.animation.AccelerateInterpolator;
31 import android.view.animation.DecelerateInterpolator;
32 
33 import java.util.ArrayList;
34 import java.util.Collections;
35 import java.util.Comparator;
36 import java.util.List;
37 
38 /**
39  * Execute horizontal slide of 1/4 width and fade (to workaround bug 23718734)
40  */
41 public class FadeAndShortSlide extends Visibility {
42     private static final TimeInterpolator APPEAR_INTERPOLATOR = new DecelerateInterpolator();
43     private static final TimeInterpolator DISAPPEAR_INTERPOLATOR = new AccelerateInterpolator();
44 
45     private static final String PROPNAME_SCREEN_POSITION =
46             "android_fadeAndShortSlideTransition_screenPosition";
47     private static final String PROPNAME_DELAY = "propname_delay";
48 
49     private static final int DEFAULT_DISTANCE = 200;
50 
51     private static abstract class CalculateSlide {
52         /** Returns the translation value for view when it goes out of the scene */
getGoneX(ViewGroup sceneRoot, View view, int[] position, int distance)53         public abstract float getGoneX(ViewGroup sceneRoot, View view, int[] position,
54                 int distance);
55     }
56 
57     private static final CalculateSlide sCalculateStart = new CalculateSlide() {
58         @Override
59         public float getGoneX(ViewGroup sceneRoot, View view, int[] position, int distance) {
60             final boolean isRtl = sceneRoot.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
61             final float x;
62             if (isRtl) {
63                 x = view.getTranslationX() + distance;
64             } else {
65                 x = view.getTranslationX() - distance;
66             }
67             return x;
68         }
69     };
70 
71     private static final CalculateSlide sCalculateEnd = new CalculateSlide() {
72         @Override
73         public float getGoneX(ViewGroup sceneRoot, View view, int[] position, int distance) {
74             final boolean isRtl = sceneRoot.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
75             final float x;
76             if (isRtl) {
77                 x = view.getTranslationX() - distance;
78             } else {
79                 x = view.getTranslationX() + distance;
80             }
81             return x;
82         }
83     };
84 
85     private static final ViewPositionComparator sViewPositionComparator =
86             new ViewPositionComparator();
87 
88     private int mSlideEdge;
89     private CalculateSlide mSlideCalculator = sCalculateEnd;
90     private Visibility mFade = new Fade();
91 
92     // TODO: Consider using TransitionPropagation.
93     private int[] mParentIdsForDelay;
94     private int mDistance = DEFAULT_DISTANCE;
95 
FadeAndShortSlide()96     public FadeAndShortSlide() {
97         this(Gravity.START);
98     }
99 
FadeAndShortSlide(int slideEdge)100     public FadeAndShortSlide(int slideEdge) {
101         this(slideEdge, null);
102     }
103 
FadeAndShortSlide(int slideEdge, int[] parentIdsForDelay)104     public FadeAndShortSlide(int slideEdge, int[] parentIdsForDelay) {
105         setSlideEdge(slideEdge);
106         mParentIdsForDelay = parentIdsForDelay;
107     }
108 
109     @Override
setEpicenterCallback(EpicenterCallback epicenterCallback)110     public void setEpicenterCallback(EpicenterCallback epicenterCallback) {
111         super.setEpicenterCallback(epicenterCallback);
112         mFade.setEpicenterCallback(epicenterCallback);
113     }
114 
captureValues(TransitionValues transitionValues)115     private void captureValues(TransitionValues transitionValues) {
116         View view = transitionValues.view;
117         int[] position = new int[2];
118         view.getLocationOnScreen(position);
119         transitionValues.values.put(PROPNAME_SCREEN_POSITION, position);
120     }
121 
getDelayOrder(View view, boolean appear)122     private int getDelayOrder(View view, boolean appear) {
123         if (mParentIdsForDelay == null) {
124             return -1;
125         }
126         final View parentForDelay = findParentForDelay(view);
127         if (parentForDelay == null || !(parentForDelay instanceof ViewGroup)) {
128             return -1;
129         }
130         List<View> transitionTargets = new ArrayList<>();
131         getTransitionTargets((ViewGroup) parentForDelay, transitionTargets);
132         sViewPositionComparator.mParentForDelay = parentForDelay;
133         sViewPositionComparator.mIsLtr = view.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
134         sViewPositionComparator.mToLeft = sViewPositionComparator.mIsLtr
135                 ? mSlideEdge == (appear ? Gravity.END : Gravity.START)
136                 : mSlideEdge == (appear ? Gravity.START : Gravity.END);
137         Collections.sort(transitionTargets, sViewPositionComparator);
138         return transitionTargets.indexOf(view);
139     }
140 
findParentForDelay(View view)141     private View findParentForDelay(View view) {
142         if (isParentForDelay(view.getId())) {
143             return view;
144         }
145         View parent = view;
146         while (parent.getParent() instanceof View) {
147             parent = (View) parent.getParent();
148             if (isParentForDelay(parent.getId())) {
149                 return parent;
150             }
151         }
152         return null;
153     }
154 
isParentForDelay(int viewId)155     private boolean isParentForDelay(int viewId) {
156         for (int id : mParentIdsForDelay) {
157             if (id == viewId) {
158                 return true;
159             }
160         }
161         return false;
162     }
163 
getTransitionTargets(ViewGroup parent, List<View> transitionTargets)164     private void getTransitionTargets(ViewGroup parent, List<View> transitionTargets) {
165         int count = parent.getChildCount();
166         for (int i = 0; i < count; ++i) {
167             View child = parent.getChildAt(i);
168             if (child instanceof ViewGroup && !((ViewGroup) child).isTransitionGroup()) {
169                 getTransitionTargets((ViewGroup) child, transitionTargets);
170             } else {
171                 transitionTargets.add(child);
172             }
173         }
174     }
175 
176     @Override
captureStartValues(TransitionValues transitionValues)177     public void captureStartValues(TransitionValues transitionValues) {
178         super.captureStartValues(transitionValues);
179         mFade.captureStartValues(transitionValues);
180         captureValues(transitionValues);
181         int delayIndex = getDelayOrder(transitionValues.view, false);
182         if (delayIndex > 0) {
183             transitionValues.values.put(PROPNAME_DELAY,
184                     delayIndex * SetupAnimationHelper.DELAY_BETWEEN_SIBLINGS_MS);
185         }
186     }
187 
188     @Override
captureEndValues(TransitionValues transitionValues)189     public void captureEndValues(TransitionValues transitionValues) {
190         super.captureEndValues(transitionValues);
191         mFade.captureEndValues(transitionValues);
192         captureValues(transitionValues);
193         int delayIndex = getDelayOrder(transitionValues.view, true);
194         if (delayIndex > 0) {
195             transitionValues.values.put(PROPNAME_DELAY,
196                     delayIndex * SetupAnimationHelper.DELAY_BETWEEN_SIBLINGS_MS);
197         }
198     }
199 
setSlideEdge(int slideEdge)200     public void setSlideEdge(int slideEdge) {
201         mSlideEdge = slideEdge;
202         switch (slideEdge) {
203             case Gravity.START:
204                 mSlideCalculator = sCalculateStart;
205                 break;
206             case Gravity.END:
207                 mSlideCalculator = sCalculateEnd;
208                 break;
209             default:
210                 throw new IllegalArgumentException("Invalid slide direction");
211         }
212     }
213 
214     @Override
onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues)215     public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues,
216             TransitionValues endValues) {
217         if (endValues == null) {
218             return null;
219         }
220         int[] position = (int[]) endValues.values.get(PROPNAME_SCREEN_POSITION);
221         int left = position[0];
222         float endX = view.getTranslationX();
223         float startX = mSlideCalculator.getGoneX(sceneRoot, view, position, mDistance);
224         final Animator slideAnimator = TranslationAnimationCreator.createAnimation(view, endValues,
225                 left, startX, endX, APPEAR_INTERPOLATOR, this);
226         if (slideAnimator == null) {
227             return null;
228         }
229         mFade.setInterpolator(APPEAR_INTERPOLATOR);
230         final AnimatorSet set = new AnimatorSet();
231         set.play(slideAnimator).with(mFade.onAppear(sceneRoot, view, startValues, endValues));
232         Long delay = (Long ) endValues.values.get(PROPNAME_DELAY);
233         if (delay != null) {
234             set.setStartDelay(delay);
235         }
236         return set;
237     }
238 
239     @Override
onDisappear(ViewGroup sceneRoot, final View view, TransitionValues startValues, TransitionValues endValues)240     public Animator onDisappear(ViewGroup sceneRoot, final View view, TransitionValues startValues,
241             TransitionValues endValues) {
242         if (startValues == null) {
243             return null;
244         }
245         int[] position = (int[]) startValues.values.get(PROPNAME_SCREEN_POSITION);
246         int left = position[0];
247         float startX = view.getTranslationX();
248         float endX = mSlideCalculator.getGoneX(sceneRoot, view, position, mDistance);
249         final Animator slideAnimator = TranslationAnimationCreator.createAnimation(view,
250                 startValues, left, startX, endX, DISAPPEAR_INTERPOLATOR, this);
251         if (slideAnimator == null) { // slideAnimator is null if startX == endX
252             return null;
253         }
254 
255         mFade.setInterpolator(DISAPPEAR_INTERPOLATOR);
256         final Animator fadeAnimator = mFade.onDisappear(sceneRoot, view, startValues, endValues);
257         if (fadeAnimator == null) {
258             return null;
259         }
260         fadeAnimator.addListener(new AnimatorListenerAdapter() {
261             @Override
262             public void onAnimationEnd(Animator animator) {
263                 fadeAnimator.removeListener(this);
264                 view.setAlpha(0.0f);
265             }
266         });
267 
268         final AnimatorSet set = new AnimatorSet();
269         set.play(slideAnimator).with(fadeAnimator);
270         Long delay = (Long) startValues.values.get(PROPNAME_DELAY);
271         if (delay != null) {
272             set.setStartDelay(delay);
273         }
274         return set;
275     }
276 
277     @Override
addListener(TransitionListener listener)278     public Transition addListener(TransitionListener listener) {
279         mFade.addListener(listener);
280         return super.addListener(listener);
281     }
282 
283     @Override
removeListener(TransitionListener listener)284     public Transition removeListener(TransitionListener listener) {
285         mFade.removeListener(listener);
286         return super.removeListener(listener);
287     }
288 
289     @Override
clone()290     public Transition clone() {
291         FadeAndShortSlide clone = (FadeAndShortSlide) super.clone();
292         clone.mFade = (Visibility) mFade.clone();
293         return clone;
294     }
295 
296     @Override
setDuration(long duration)297     public Transition setDuration(long duration) {
298         long scaledDuration = SetupAnimationHelper.applyAnimationTimeScale(duration);
299         mFade.setDuration(scaledDuration);
300         return super.setDuration(scaledDuration);
301     }
302 
303     /**
304      * Sets the moving distance in pixel.
305      */
setDistance(int distance)306     public void setDistance(int distance) {
307         mDistance = distance;
308     }
309 
310     private static class ViewPositionComparator implements Comparator<View> {
311         View mParentForDelay;
312         boolean mIsLtr;
313         boolean mToLeft;
314 
315         @Override
compare(View lhs, View rhs)316         public int compare(View lhs, View rhs) {
317             int start1;
318             int start2;
319             if (mIsLtr) {
320                 start1 = getRelativeLeft(lhs, mParentForDelay);
321                 start2 = getRelativeLeft(rhs, mParentForDelay);
322             } else {
323                 start1 = getRelativeRight(lhs, mParentForDelay);
324                 start2 = getRelativeRight(rhs, mParentForDelay);
325             }
326             if (mToLeft) {
327                 if (start1 > start2) {
328                     return 1;
329                 } else if (start1 < start2) {
330                     return -1;
331                 }
332             } else {
333                 if (start1 > start2) {
334                     return -1;
335                 } else if (start1 < start2) {
336                     return 1;
337                 }
338             }
339             int top1 = getRelativeTop(lhs, mParentForDelay);
340             int top2 = getRelativeTop(rhs, mParentForDelay);
341             return Integer.compare(top1, top2);
342         }
343 
getRelativeLeft(View child, View ancestor)344         private int getRelativeLeft(View child, View ancestor) {
345             ViewParent parent = child.getParent();
346             int left = child.getLeft();
347             while (parent instanceof View) {
348                 if (parent == ancestor) {
349                     break;
350                 }
351                 left += ((View) parent).getLeft();
352                 parent = parent.getParent();
353             }
354             return left;
355         }
356 
getRelativeRight(View child, View ancestor)357         private int getRelativeRight(View child, View ancestor) {
358             ViewParent parent = child.getParent();
359             int right = child.getRight();
360             while (parent instanceof View) {
361                 if (parent == ancestor) {
362                     break;
363                 }
364                 right += ((View) parent).getLeft();
365                 parent = parent.getParent();
366             }
367             return right;
368         }
369 
getRelativeTop(View child, View ancestor)370         private int getRelativeTop(View child, View ancestor) {
371             ViewParent parent = child.getParent();
372             int top = child.getTop();
373             while (parent instanceof View) {
374                 if (parent == ancestor) {
375                     break;
376                 }
377                 top += ((View) parent).getTop();
378                 parent = parent.getParent();
379             }
380             return top;
381         }
382     }
383 }
384