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 androidx.leanback.transition;
17 
18 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
19 
20 import android.animation.Animator;
21 import android.animation.AnimatorSet;
22 import android.animation.TimeInterpolator;
23 import android.content.Context;
24 import android.content.res.TypedArray;
25 import android.graphics.Rect;
26 import android.transition.Fade;
27 import android.transition.Transition;
28 import android.transition.TransitionValues;
29 import android.transition.Visibility;
30 import android.util.AttributeSet;
31 import android.view.Gravity;
32 import android.view.View;
33 import android.view.ViewGroup;
34 import android.view.animation.DecelerateInterpolator;
35 
36 import androidx.annotation.RequiresApi;
37 import androidx.annotation.RestrictTo;
38 import androidx.leanback.R;
39 
40 /**
41  * Execute horizontal slide of 1/4 width and fade (to workaround bug 23718734)
42  */
43 @RequiresApi(21)
44 @RestrictTo(LIBRARY_GROUP_PREFIX)
45 public class FadeAndShortSlide extends Visibility {
46 
47     private static final TimeInterpolator sDecelerate = new DecelerateInterpolator();
48     // private static final TimeInterpolator sAccelerate = new AccelerateInterpolator();
49     private static final String PROPNAME_SCREEN_POSITION =
50             "android:fadeAndShortSlideTransition:screenPosition";
51 
52     private CalculateSlide mSlideCalculator;
53     private Visibility mFade = new Fade();
54     private float mDistance = -1;
55 
56     private static abstract class CalculateSlide {
57 
CalculateSlide()58         CalculateSlide() {
59         }
60 
61         /** Returns the translation X value for view when it goes out of the scene */
getGoneX(FadeAndShortSlide t, ViewGroup sceneRoot, View view, int[] position)62         float getGoneX(FadeAndShortSlide t, ViewGroup sceneRoot, View view, int[] position) {
63             return view.getTranslationX();
64         }
65 
66         /** Returns the translation Y value for view when it goes out of the scene */
getGoneY(FadeAndShortSlide t, ViewGroup sceneRoot, View view, int[] position)67         float getGoneY(FadeAndShortSlide t, ViewGroup sceneRoot, View view, int[] position) {
68             return view.getTranslationY();
69         }
70     }
71 
getHorizontalDistance(ViewGroup sceneRoot)72     float getHorizontalDistance(ViewGroup sceneRoot) {
73         return mDistance >= 0 ? mDistance : (sceneRoot.getWidth() / 4);
74     }
75 
getVerticalDistance(ViewGroup sceneRoot)76     float getVerticalDistance(ViewGroup sceneRoot) {
77         return mDistance >= 0 ? mDistance : (sceneRoot.getHeight() / 4);
78     }
79 
80     final static CalculateSlide sCalculateStart = new CalculateSlide() {
81         @Override
82         public float getGoneX(FadeAndShortSlide t, ViewGroup sceneRoot, View view, int[] position) {
83             final boolean isRtl = sceneRoot.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
84             final float x;
85             if (isRtl) {
86                 x = view.getTranslationX() + t.getHorizontalDistance(sceneRoot);
87             } else {
88                 x = view.getTranslationX() - t.getHorizontalDistance(sceneRoot);
89             }
90             return x;
91         }
92     };
93 
94     final static CalculateSlide sCalculateEnd = new CalculateSlide() {
95         @Override
96         public float getGoneX(FadeAndShortSlide t, ViewGroup sceneRoot, View view, int[] position) {
97             final boolean isRtl = sceneRoot.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
98             final float x;
99             if (isRtl) {
100                 x = view.getTranslationX() - t.getHorizontalDistance(sceneRoot);
101             } else {
102                 x = view.getTranslationX() + t.getHorizontalDistance(sceneRoot);
103             }
104             return x;
105         }
106     };
107 
108     final static CalculateSlide sCalculateStartEnd = new CalculateSlide() {
109         @Override
110         public float getGoneX(FadeAndShortSlide t, ViewGroup sceneRoot, View view, int[] position) {
111             final int viewCenter = position[0] + view.getWidth() / 2;
112             sceneRoot.getLocationOnScreen(position);
113             Rect center = t.getEpicenter();
114             final int sceneRootCenter = center == null ? (position[0] + sceneRoot.getWidth() / 2)
115                     : center.centerX();
116             if (viewCenter < sceneRootCenter) {
117                 return view.getTranslationX() - t.getHorizontalDistance(sceneRoot);
118             } else {
119                 return view.getTranslationX() + t.getHorizontalDistance(sceneRoot);
120             }
121         }
122     };
123 
124     final static CalculateSlide sCalculateBottom = new CalculateSlide() {
125         @Override
126         public float getGoneY(FadeAndShortSlide t, ViewGroup sceneRoot, View view, int[] position) {
127             return view.getTranslationY() + t.getVerticalDistance(sceneRoot);
128         }
129     };
130 
131     final static CalculateSlide sCalculateTop = new CalculateSlide() {
132         @Override
133         public float getGoneY(FadeAndShortSlide t, ViewGroup sceneRoot, View view, int[] position) {
134             return view.getTranslationY() - t.getVerticalDistance(sceneRoot);
135         }
136     };
137 
138     final CalculateSlide sCalculateTopBottom = new CalculateSlide() {
139         @Override
140         public float getGoneY(FadeAndShortSlide t, ViewGroup sceneRoot, View view, int[] position) {
141             final int viewCenter = position[1] + view.getHeight() / 2;
142             sceneRoot.getLocationOnScreen(position);
143             Rect center = getEpicenter();
144             final int sceneRootCenter = center == null ? (position[1] + sceneRoot.getHeight() / 2)
145                     : center.centerY();
146             if (viewCenter < sceneRootCenter) {
147                 return view.getTranslationY() - t.getVerticalDistance(sceneRoot);
148             } else {
149                 return view.getTranslationY() + t.getVerticalDistance(sceneRoot);
150             }
151         }
152     };
153 
FadeAndShortSlide()154     public FadeAndShortSlide() {
155         this(Gravity.START);
156     }
157 
FadeAndShortSlide(int slideEdge)158     public FadeAndShortSlide(int slideEdge) {
159         setSlideEdge(slideEdge);
160     }
161 
FadeAndShortSlide(Context context, AttributeSet attrs)162     public FadeAndShortSlide(Context context, AttributeSet attrs) {
163         super(context, attrs);
164         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbSlide);
165         int edge = a.getInt(R.styleable.lbSlide_lb_slideEdge, Gravity.START);
166         setSlideEdge(edge);
167         a.recycle();
168     }
169 
170     @Override
setEpicenterCallback(EpicenterCallback epicenterCallback)171     public void setEpicenterCallback(EpicenterCallback epicenterCallback) {
172         mFade.setEpicenterCallback(epicenterCallback);
173         super.setEpicenterCallback(epicenterCallback);
174     }
175 
captureValues(TransitionValues transitionValues)176     private void captureValues(TransitionValues transitionValues) {
177         View view = transitionValues.view;
178         int[] position = new int[2];
179         view.getLocationOnScreen(position);
180         transitionValues.values.put(PROPNAME_SCREEN_POSITION, position);
181     }
182 
183     @Override
captureStartValues(TransitionValues transitionValues)184     public void captureStartValues(TransitionValues transitionValues) {
185         mFade.captureStartValues(transitionValues);
186         super.captureStartValues(transitionValues);
187         captureValues(transitionValues);
188     }
189 
190     @Override
captureEndValues(TransitionValues transitionValues)191     public void captureEndValues(TransitionValues transitionValues) {
192         mFade.captureEndValues(transitionValues);
193         super.captureEndValues(transitionValues);
194         captureValues(transitionValues);
195     }
196 
setSlideEdge(int slideEdge)197     public void setSlideEdge(int slideEdge) {
198         switch (slideEdge) {
199             case Gravity.START:
200                 mSlideCalculator = sCalculateStart;
201                 break;
202             case Gravity.END:
203                 mSlideCalculator = sCalculateEnd;
204                 break;
205             case Gravity.START | Gravity.END:
206                 mSlideCalculator = sCalculateStartEnd;
207                 break;
208             case Gravity.TOP:
209                 mSlideCalculator = sCalculateTop;
210                 break;
211             case Gravity.BOTTOM:
212                 mSlideCalculator = sCalculateBottom;
213                 break;
214             case Gravity.TOP | Gravity.BOTTOM:
215                 mSlideCalculator = sCalculateTopBottom;
216                 break;
217             default:
218                 throw new IllegalArgumentException("Invalid slide direction");
219         }
220     }
221 
222     @Override
onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues)223     public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues,
224             TransitionValues endValues) {
225         if (endValues == null) {
226             return null;
227         }
228         if (sceneRoot == view) {
229             // workaround b/25375640, avoid run animation on sceneRoot
230             return null;
231         }
232         int[] position = (int[]) endValues.values.get(PROPNAME_SCREEN_POSITION);
233         int left = position[0];
234         int top = position[1];
235         float endX = view.getTranslationX();
236         float startX = mSlideCalculator.getGoneX(this, sceneRoot, view, position);
237         float endY = view.getTranslationY();
238         float startY = mSlideCalculator.getGoneY(this, sceneRoot, view, position);
239         final Animator slideAnimator = TranslationAnimationCreator.createAnimation(view, endValues,
240                 left, top, startX, startY, endX, endY, sDecelerate, this);
241         final Animator fadeAnimator = mFade.onAppear(sceneRoot, view, startValues, endValues);
242         if (slideAnimator == null) {
243             return fadeAnimator;
244         } else if (fadeAnimator == null) {
245             return slideAnimator;
246         }
247         final AnimatorSet set = new AnimatorSet();
248         set.play(slideAnimator).with(fadeAnimator);
249 
250         return set;
251     }
252 
253     @Override
onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues, TransitionValues endValues)254     public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues,
255             TransitionValues endValues) {
256         if (startValues == null) {
257             return null;
258         }
259         if (sceneRoot == view) {
260             // workaround b/25375640, avoid run animation on sceneRoot
261             return null;
262         }
263         int[] position = (int[]) startValues.values.get(PROPNAME_SCREEN_POSITION);
264         int left = position[0];
265         int top = position[1];
266         float startX = view.getTranslationX();
267         float endX = mSlideCalculator.getGoneX(this, sceneRoot, view, position);
268         float startY = view.getTranslationY();
269         float endY = mSlideCalculator.getGoneY(this, sceneRoot, view, position);
270         final Animator slideAnimator = TranslationAnimationCreator.createAnimation(view,
271                 startValues, left, top, startX, startY, endX, endY, sDecelerate /* sAccelerate */,
272                 this);
273         final Animator fadeAnimator = mFade.onDisappear(sceneRoot, view, startValues, endValues);
274         if (slideAnimator == null) {
275             return fadeAnimator;
276         } else if (fadeAnimator == null) {
277             return slideAnimator;
278         }
279         final AnimatorSet set = new AnimatorSet();
280         set.play(slideAnimator).with(fadeAnimator);
281 
282         return set;
283     }
284 
285     @Override
addListener(TransitionListener listener)286     public Transition addListener(TransitionListener listener) {
287         mFade.addListener(listener);
288         return super.addListener(listener);
289     }
290 
291     @Override
removeListener(TransitionListener listener)292     public Transition removeListener(TransitionListener listener) {
293         mFade.removeListener(listener);
294         return super.removeListener(listener);
295     }
296 
297     /**
298      * Returns distance to slide.  When negative value is returned, it will use 1/4 of
299      * sceneRoot dimension.
300      */
getDistance()301     public float getDistance() {
302         return mDistance;
303     }
304 
305     /**
306      * Set distance to slide, default value is -1.  when negative value is set, it will use 1/4 of
307      * sceneRoot dimension.
308      * @param distance Pixels to slide.
309      */
setDistance(float distance)310     public void setDistance(float distance) {
311         mDistance = distance;
312     }
313 
314     @Override
clone()315     public Transition clone() {
316         FadeAndShortSlide clone = (FadeAndShortSlide) super.clone();
317         clone.mFade = (Visibility) mFade.clone();
318         return clone;
319     }
320 }
321