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 android.support.v17.leanback.transition;
17
18import static android.support.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.support.annotation.RequiresApi;
27import android.support.annotation.RestrictTo;
28import android.support.v17.leanback.R;
29import android.transition.Fade;
30import android.transition.Transition;
31import android.transition.TransitionValues;
32import android.transition.Visibility;
33import android.util.AttributeSet;
34import android.view.Gravity;
35import android.view.View;
36import android.view.ViewGroup;
37import android.view.animation.DecelerateInterpolator;
38
39/**
40 * Execute horizontal slide of 1/4 width and fade (to workaround bug 23718734)
41 * @hide
42 */
43@RequiresApi(21)
44@RestrictTo(LIBRARY_GROUP)
45public 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
58        CalculateSlide() {
59        }
60
61        /** Returns the translation X value for view when it goes out of the scene */
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 */
67        float getGoneY(FadeAndShortSlide t, ViewGroup sceneRoot, View view, int[] position) {
68            return view.getTranslationY();
69        }
70    }
71
72    float getHorizontalDistance(ViewGroup sceneRoot) {
73        return mDistance >= 0 ? mDistance : (sceneRoot.getWidth() / 4);
74    }
75
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
154    public FadeAndShortSlide() {
155        this(Gravity.START);
156    }
157
158    public FadeAndShortSlide(int slideEdge) {
159        setSlideEdge(slideEdge);
160    }
161
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
171    public void setEpicenterCallback(EpicenterCallback epicenterCallback) {
172        mFade.setEpicenterCallback(epicenterCallback);
173        super.setEpicenterCallback(epicenterCallback);
174    }
175
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
184    public void captureStartValues(TransitionValues transitionValues) {
185        mFade.captureStartValues(transitionValues);
186        super.captureStartValues(transitionValues);
187        captureValues(transitionValues);
188    }
189
190    @Override
191    public void captureEndValues(TransitionValues transitionValues) {
192        mFade.captureEndValues(transitionValues);
193        super.captureEndValues(transitionValues);
194        captureValues(transitionValues);
195    }
196
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
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
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
286    public Transition addListener(TransitionListener listener) {
287        mFade.addListener(listener);
288        return super.addListener(listener);
289    }
290
291    @Override
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     */
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     */
310    public void setDistance(float distance) {
311        mDistance = distance;
312    }
313
314    @Override
315    public Transition clone() {
316        FadeAndShortSlide clone = null;
317        clone = (FadeAndShortSlide) super.clone();
318        clone.mFade = (Visibility) mFade.clone();
319        return clone;
320    }
321}
322