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 */
16package androidx.leanback.transition;
17
18import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
19
20import android.animation.Animator;
21import android.animation.AnimatorSet;
22import android.animation.TimeInterpolator;
23import android.content.Context;
24import android.content.res.TypedArray;
25import android.graphics.Rect;
26import android.transition.Fade;
27import android.transition.Transition;
28import android.transition.TransitionValues;
29import android.transition.Visibility;
30import android.util.AttributeSet;
31import android.view.Gravity;
32import android.view.View;
33import android.view.ViewGroup;
34import android.view.animation.DecelerateInterpolator;
35
36import androidx.annotation.RequiresApi;
37import androidx.annotation.RestrictTo;
38import 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)
46public 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
59        CalculateSlide() {
60        }
61
62        /** Returns the translation X value for view when it goes out of the scene */
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 */
68        float getGoneY(FadeAndShortSlide t, ViewGroup sceneRoot, View view, int[] position) {
69            return view.getTranslationY();
70        }
71    }
72
73    float getHorizontalDistance(ViewGroup sceneRoot) {
74        return mDistance >= 0 ? mDistance : (sceneRoot.getWidth() / 4);
75    }
76
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
155    public FadeAndShortSlide() {
156        this(Gravity.START);
157    }
158
159    public FadeAndShortSlide(int slideEdge) {
160        setSlideEdge(slideEdge);
161    }
162
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
172    public void setEpicenterCallback(EpicenterCallback epicenterCallback) {
173        mFade.setEpicenterCallback(epicenterCallback);
174        super.setEpicenterCallback(epicenterCallback);
175    }
176
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
185    public void captureStartValues(TransitionValues transitionValues) {
186        mFade.captureStartValues(transitionValues);
187        super.captureStartValues(transitionValues);
188        captureValues(transitionValues);
189    }
190
191    @Override
192    public void captureEndValues(TransitionValues transitionValues) {
193        mFade.captureEndValues(transitionValues);
194        super.captureEndValues(transitionValues);
195        captureValues(transitionValues);
196    }
197
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
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
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
287    public Transition addListener(TransitionListener listener) {
288        mFade.addListener(listener);
289        return super.addListener(listener);
290    }
291
292    @Override
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     */
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     */
311    public void setDistance(float distance) {
312        mDistance = distance;
313    }
314
315    @Override
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