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