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