1/*
2 * Copyright (C) 2017 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
17package android.support.transition;
18
19import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20
21import android.animation.Animator;
22import android.animation.TimeInterpolator;
23import android.content.Context;
24import android.content.res.TypedArray;
25import android.support.annotation.IntDef;
26import android.support.annotation.NonNull;
27import android.support.annotation.RestrictTo;
28import android.support.v4.content.res.TypedArrayUtils;
29import android.support.v4.view.ViewCompat;
30import android.util.AttributeSet;
31import android.view.Gravity;
32import android.view.View;
33import android.view.ViewGroup;
34import android.view.animation.AccelerateInterpolator;
35import android.view.animation.DecelerateInterpolator;
36
37import org.xmlpull.v1.XmlPullParser;
38
39import java.lang.annotation.Retention;
40import java.lang.annotation.RetentionPolicy;
41
42/**
43 * This transition tracks changes to the visibility of target views in the
44 * start and end scenes and moves views in or out from one of the edges of the
45 * scene. Visibility is determined by both the
46 * {@link View#setVisibility(int)} state of the view as well as whether it
47 * is parented in the current view hierarchy. Disappearing Views are
48 * limited as described in {@link Visibility#onDisappear(android.view.ViewGroup,
49 * TransitionValues, int, TransitionValues, int)}.
50 */
51public class Slide extends Visibility {
52
53    private static final TimeInterpolator sDecelerate = new DecelerateInterpolator();
54    private static final TimeInterpolator sAccelerate = new AccelerateInterpolator();
55    private static final String PROPNAME_SCREEN_POSITION = "android:slide:screenPosition";
56    private CalculateSlide mSlideCalculator = sCalculateBottom;
57    private int mSlideEdge = Gravity.BOTTOM;
58
59    /** @hide */
60    @RestrictTo(LIBRARY_GROUP)
61    @Retention(RetentionPolicy.SOURCE)
62    @IntDef({Gravity.LEFT, Gravity.TOP, Gravity.RIGHT, Gravity.BOTTOM, Gravity.START, Gravity.END})
63    public @interface GravityFlag {
64    }
65
66    private interface CalculateSlide {
67
68        /** Returns the translation value for view when it goes out of the scene */
69        float getGoneX(ViewGroup sceneRoot, View view);
70
71        /** Returns the translation value for view when it goes out of the scene */
72        float getGoneY(ViewGroup sceneRoot, View view);
73    }
74
75    private abstract static class CalculateSlideHorizontal implements CalculateSlide {
76
77        @Override
78        public float getGoneY(ViewGroup sceneRoot, View view) {
79            return view.getTranslationY();
80        }
81    }
82
83    private abstract static class CalculateSlideVertical implements CalculateSlide {
84
85        @Override
86        public float getGoneX(ViewGroup sceneRoot, View view) {
87            return view.getTranslationX();
88        }
89    }
90
91    private static final CalculateSlide sCalculateLeft = new CalculateSlideHorizontal() {
92        @Override
93        public float getGoneX(ViewGroup sceneRoot, View view) {
94            return view.getTranslationX() - sceneRoot.getWidth();
95        }
96    };
97
98    private static final CalculateSlide sCalculateStart = new CalculateSlideHorizontal() {
99        @Override
100        public float getGoneX(ViewGroup sceneRoot, View view) {
101            final boolean isRtl = ViewCompat.getLayoutDirection(sceneRoot)
102                    == ViewCompat.LAYOUT_DIRECTION_RTL;
103            final float x;
104            if (isRtl) {
105                x = view.getTranslationX() + sceneRoot.getWidth();
106            } else {
107                x = view.getTranslationX() - sceneRoot.getWidth();
108            }
109            return x;
110        }
111    };
112
113    private static final CalculateSlide sCalculateTop = new CalculateSlideVertical() {
114        @Override
115        public float getGoneY(ViewGroup sceneRoot, View view) {
116            return view.getTranslationY() - sceneRoot.getHeight();
117        }
118    };
119
120    private static final CalculateSlide sCalculateRight = new CalculateSlideHorizontal() {
121        @Override
122        public float getGoneX(ViewGroup sceneRoot, View view) {
123            return view.getTranslationX() + sceneRoot.getWidth();
124        }
125    };
126
127    private static final CalculateSlide sCalculateEnd = new CalculateSlideHorizontal() {
128        @Override
129        public float getGoneX(ViewGroup sceneRoot, View view) {
130            final boolean isRtl = ViewCompat.getLayoutDirection(sceneRoot)
131                    == ViewCompat.LAYOUT_DIRECTION_RTL;
132            final float x;
133            if (isRtl) {
134                x = view.getTranslationX() - sceneRoot.getWidth();
135            } else {
136                x = view.getTranslationX() + sceneRoot.getWidth();
137            }
138            return x;
139        }
140    };
141
142    private static final CalculateSlide sCalculateBottom = new CalculateSlideVertical() {
143        @Override
144        public float getGoneY(ViewGroup sceneRoot, View view) {
145            return view.getTranslationY() + sceneRoot.getHeight();
146        }
147    };
148
149    /**
150     * Constructor using the default {@link Gravity#BOTTOM}
151     * slide edge direction.
152     */
153    public Slide() {
154        setSlideEdge(Gravity.BOTTOM);
155    }
156
157    /**
158     * Constructor using the provided slide edge direction.
159     */
160    public Slide(int slideEdge) {
161        setSlideEdge(slideEdge);
162    }
163
164    public Slide(Context context, AttributeSet attrs) {
165        super(context, attrs);
166        TypedArray a = context.obtainStyledAttributes(attrs, Styleable.SLIDE);
167        int edge = TypedArrayUtils.getNamedInt(a, (XmlPullParser) attrs, "slideEdge",
168                Styleable.Slide.SLIDE_EDGE, Gravity.BOTTOM);
169        a.recycle();
170        //noinspection WrongConstant
171        setSlideEdge(edge);
172    }
173
174    private void captureValues(TransitionValues transitionValues) {
175        View view = transitionValues.view;
176        int[] position = new int[2];
177        view.getLocationOnScreen(position);
178        transitionValues.values.put(PROPNAME_SCREEN_POSITION, position);
179    }
180
181    @Override
182    public void captureStartValues(@NonNull TransitionValues transitionValues) {
183        super.captureStartValues(transitionValues);
184        captureValues(transitionValues);
185    }
186
187    @Override
188    public void captureEndValues(@NonNull TransitionValues transitionValues) {
189        super.captureEndValues(transitionValues);
190        captureValues(transitionValues);
191    }
192
193    /**
194     * Change the edge that Views appear and disappear from.
195     *
196     * @param slideEdge The edge of the scene to use for Views appearing and disappearing. One of
197     *                  {@link android.view.Gravity#LEFT}, {@link android.view.Gravity#TOP},
198     *                  {@link android.view.Gravity#RIGHT}, {@link android.view.Gravity#BOTTOM},
199     *                  {@link android.view.Gravity#START}, {@link android.view.Gravity#END}.
200     */
201    public void setSlideEdge(@GravityFlag int slideEdge) {
202        switch (slideEdge) {
203            case Gravity.LEFT:
204                mSlideCalculator = sCalculateLeft;
205                break;
206            case Gravity.TOP:
207                mSlideCalculator = sCalculateTop;
208                break;
209            case Gravity.RIGHT:
210                mSlideCalculator = sCalculateRight;
211                break;
212            case Gravity.BOTTOM:
213                mSlideCalculator = sCalculateBottom;
214                break;
215            case Gravity.START:
216                mSlideCalculator = sCalculateStart;
217                break;
218            case Gravity.END:
219                mSlideCalculator = sCalculateEnd;
220                break;
221            default:
222                throw new IllegalArgumentException("Invalid slide direction");
223        }
224        mSlideEdge = slideEdge;
225        SidePropagation propagation = new SidePropagation();
226        propagation.setSide(slideEdge);
227        setPropagation(propagation);
228    }
229
230    /**
231     * Returns the edge that Views appear and disappear from.
232     *
233     * @return the edge of the scene to use for Views appearing and disappearing. One of
234     * {@link android.view.Gravity#LEFT}, {@link android.view.Gravity#TOP},
235     * {@link android.view.Gravity#RIGHT}, {@link android.view.Gravity#BOTTOM},
236     * {@link android.view.Gravity#START}, {@link android.view.Gravity#END}.
237     */
238    @GravityFlag
239    public int getSlideEdge() {
240        return mSlideEdge;
241    }
242
243    @Override
244    public Animator onAppear(ViewGroup sceneRoot, View view,
245            TransitionValues startValues, TransitionValues endValues) {
246        if (endValues == null) {
247            return null;
248        }
249        int[] position = (int[]) endValues.values.get(PROPNAME_SCREEN_POSITION);
250        float endX = view.getTranslationX();
251        float endY = view.getTranslationY();
252        float startX = mSlideCalculator.getGoneX(sceneRoot, view);
253        float startY = mSlideCalculator.getGoneY(sceneRoot, view);
254        return TranslationAnimationCreator
255                .createAnimation(view, endValues, position[0], position[1],
256                        startX, startY, endX, endY, sDecelerate);
257    }
258
259    @Override
260    public Animator onDisappear(ViewGroup sceneRoot, View view,
261            TransitionValues startValues, TransitionValues endValues) {
262        if (startValues == null) {
263            return null;
264        }
265        int[] position = (int[]) startValues.values.get(PROPNAME_SCREEN_POSITION);
266        float startX = view.getTranslationX();
267        float startY = view.getTranslationY();
268        float endX = mSlideCalculator.getGoneX(sceneRoot, view);
269        float endY = mSlideCalculator.getGoneY(sceneRoot, view);
270        return TranslationAnimationCreator
271                .createAnimation(view, startValues, position[0], position[1],
272                        startX, startY, endX, endY, sAccelerate);
273    }
274
275}
276