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