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