1/*
2 * Copyright (C) 2016 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 org.hamcrest.CoreMatchers.allOf;
20import static org.hamcrest.CoreMatchers.is;
21import static org.hamcrest.CoreMatchers.notNullValue;
22import static org.hamcrest.CoreMatchers.nullValue;
23import static org.hamcrest.Matchers.greaterThan;
24import static org.hamcrest.Matchers.lessThan;
25import static org.junit.Assert.assertEquals;
26import static org.junit.Assert.assertThat;
27import static org.mockito.Matchers.any;
28import static org.mockito.Mockito.mock;
29import static org.mockito.Mockito.timeout;
30import static org.mockito.Mockito.verify;
31
32import android.animation.Animator;
33import android.animation.ObjectAnimator;
34import android.animation.ValueAnimator;
35import android.os.Build;
36import android.support.annotation.NonNull;
37import android.support.annotation.Nullable;
38import android.support.test.InstrumentationRegistry;
39import android.support.test.annotation.UiThreadTest;
40import android.support.test.filters.MediumTest;
41import android.view.View;
42import android.view.ViewGroup;
43
44import org.junit.Before;
45import org.junit.Test;
46
47@MediumTest
48public class FadeTest extends BaseTest {
49
50    private View mView;
51    private ViewGroup mRoot;
52
53    @UiThreadTest
54    @Before
55    public void setUp() {
56        mRoot = rule.getActivity().getRoot();
57        mView = new View(rule.getActivity());
58        mRoot.addView(mView, new ViewGroup.LayoutParams(100, 100));
59    }
60
61    @Test
62    public void testMode() {
63        assertThat(Fade.IN, is(Visibility.MODE_IN));
64        assertThat(Fade.OUT, is(Visibility.MODE_OUT));
65        final Fade fade = new Fade();
66        assertThat(fade.getMode(), is(Visibility.MODE_IN | Visibility.MODE_OUT));
67        fade.setMode(Visibility.MODE_IN);
68        assertThat(fade.getMode(), is(Visibility.MODE_IN));
69    }
70
71    @Test
72    @UiThreadTest
73    public void testDisappear() {
74        final Fade fade = new Fade();
75        final TransitionValues startValues = new TransitionValues();
76        startValues.view = mView;
77        fade.captureStartValues(startValues);
78        mView.setVisibility(View.INVISIBLE);
79        final TransitionValues endValues = new TransitionValues();
80        endValues.view = mView;
81        fade.captureEndValues(endValues);
82        Animator animator = fade.createAnimator(mRoot, startValues, endValues);
83        assertThat(animator, is(notNullValue()));
84    }
85
86    @Test
87    @UiThreadTest
88    public void testAppear() {
89        mView.setVisibility(View.INVISIBLE);
90        final Fade fade = new Fade();
91        final TransitionValues startValues = new TransitionValues();
92        startValues.view = mView;
93        fade.captureStartValues(startValues);
94        mView.setVisibility(View.VISIBLE);
95        final TransitionValues endValues = new TransitionValues();
96        endValues.view = mView;
97        fade.captureEndValues(endValues);
98        Animator animator = fade.createAnimator(mRoot, startValues, endValues);
99        assertThat(animator, is(notNullValue()));
100    }
101
102    @Test
103    @UiThreadTest
104    public void testNoChange() {
105        final Fade fade = new Fade();
106        final TransitionValues startValues = new TransitionValues();
107        startValues.view = mView;
108        fade.captureStartValues(startValues);
109        final TransitionValues endValues = new TransitionValues();
110        endValues.view = mView;
111        fade.captureEndValues(endValues);
112        Animator animator = fade.createAnimator(mRoot, startValues, endValues);
113        // No visibility change; no animation should happen
114        assertThat(animator, is(nullValue()));
115    }
116
117    @Test
118    public void testFadeOutThenIn() throws Throwable {
119        // Fade out
120        final Runnable interrupt = mock(Runnable.class);
121        float[] valuesOut = new float[2];
122        final InterruptibleFade fadeOut = new InterruptibleFade(Fade.MODE_OUT, interrupt,
123                valuesOut);
124        final Transition.TransitionListener listenerOut = mock(Transition.TransitionListener.class);
125        fadeOut.addListener(listenerOut);
126        changeVisibility(fadeOut, mRoot, mView, View.INVISIBLE);
127        verify(listenerOut, timeout(3000)).onTransitionStart(any(Transition.class));
128
129        // The view is in the middle of fading out
130        verify(interrupt, timeout(3000)).run();
131
132        // Fade in
133        float[] valuesIn = new float[2];
134        final InterruptibleFade fadeIn = new InterruptibleFade(Fade.MODE_IN, null, valuesIn);
135        final Transition.TransitionListener listenerIn = mock(Transition.TransitionListener.class);
136        fadeIn.addListener(listenerIn);
137        changeVisibility(fadeIn, mRoot, mView, View.VISIBLE);
138        verify(listenerOut, timeout(3000)).onTransitionPause(any(Transition.class));
139        verify(listenerIn, timeout(3000)).onTransitionStart(any(Transition.class));
140        assertThat(valuesOut[1], allOf(greaterThan(0f), lessThan(1f)));
141        if (Build.VERSION.SDK_INT >= 19) {
142            // These won't match on API levels 18 and below due to lack of Animator pause.
143            assertEquals(valuesOut[1], valuesIn[0], 0.01f);
144        }
145
146        verify(listenerIn, timeout(3000)).onTransitionEnd(any(Transition.class));
147        assertThat(mView.getVisibility(), is(View.VISIBLE));
148        assertEquals(valuesIn[1], 1.f, 0.01f);
149    }
150
151    @Test
152    public void testFadeInThenOut() throws Throwable {
153        changeVisibility(null, mRoot, mView, View.INVISIBLE);
154        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
155
156        // Fade in
157        final Runnable interrupt = mock(Runnable.class);
158        float[] valuesIn = new float[2];
159        final InterruptibleFade fadeIn = new InterruptibleFade(Fade.MODE_IN, interrupt, valuesIn);
160        final Transition.TransitionListener listenerIn = mock(Transition.TransitionListener.class);
161        fadeIn.addListener(listenerIn);
162        changeVisibility(fadeIn, mRoot, mView, View.VISIBLE);
163        verify(listenerIn, timeout(3000)).onTransitionStart(any(Transition.class));
164
165        // The view is in the middle of fading in
166        verify(interrupt, timeout(3000)).run();
167
168        // Fade out
169        float[] valuesOut = new float[2];
170        final InterruptibleFade fadeOut = new InterruptibleFade(Fade.MODE_OUT, null, valuesOut);
171        final Transition.TransitionListener listenerOut = mock(Transition.TransitionListener.class);
172        fadeOut.addListener(listenerOut);
173        changeVisibility(fadeOut, mRoot, mView, View.INVISIBLE);
174        verify(listenerIn, timeout(3000)).onTransitionPause(any(Transition.class));
175        verify(listenerOut, timeout(3000)).onTransitionStart(any(Transition.class));
176        assertThat(valuesIn[1], allOf(greaterThan(0f), lessThan(1f)));
177        if (Build.VERSION.SDK_INT >= 19) {
178            // These won't match on API levels 18 and below due to lack of Animator pause.
179            assertEquals(valuesIn[1], valuesOut[0], 0.01f);
180        }
181
182        verify(listenerOut, timeout(3000)).onTransitionEnd(any(Transition.class));
183        assertThat(mView.getVisibility(), is(View.INVISIBLE));
184    }
185
186    @Test
187    public void testFadeWithAlpha() throws Throwable {
188        // Set the view alpha to 0.5
189        rule.runOnUiThread(new Runnable() {
190            @Override
191            public void run() {
192                mView.setAlpha(0.5f);
193            }
194        });
195        // Fade out
196        final Fade fadeOut = new Fade(Fade.OUT);
197        final Transition.TransitionListener listenerOut = mock(Transition.TransitionListener.class);
198        fadeOut.addListener(listenerOut);
199        changeVisibility(fadeOut, mRoot, mView, View.INVISIBLE);
200        verify(listenerOut, timeout(3000)).onTransitionStart(any(Transition.class));
201        verify(listenerOut, timeout(3000)).onTransitionEnd(any(Transition.class));
202        // Fade in
203        final Fade fadeIn = new Fade(Fade.IN);
204        final Transition.TransitionListener listenerIn = mock(Transition.TransitionListener.class);
205        fadeIn.addListener(listenerIn);
206        changeVisibility(fadeIn, mRoot, mView, View.VISIBLE);
207        verify(listenerIn, timeout(3000)).onTransitionStart(any(Transition.class));
208        verify(listenerIn, timeout(3000)).onTransitionEnd(any(Transition.class));
209        // Confirm that the view still has the original alpha value
210        assertThat(mView.getVisibility(), is(View.VISIBLE));
211        assertEquals(0.5f, mView.getAlpha(), 0.01f);
212    }
213
214    private void changeVisibility(final Fade fade, final ViewGroup container, final View target,
215            final int visibility) throws Throwable {
216        rule.runOnUiThread(new Runnable() {
217            @Override
218            public void run() {
219                if (fade != null) {
220                    TransitionManager.beginDelayedTransition(container, fade);
221                }
222                target.setVisibility(visibility);
223            }
224        });
225    }
226
227    /**
228     * A special version of {@link Fade} that runs a specified {@link Runnable} soon after the
229     * target starts fading in or out.
230     */
231    private static class InterruptibleFade extends Fade {
232
233        static final float ALPHA_THRESHOLD = 0.2f;
234
235        float mInitialAlpha = -1;
236        Runnable mMiddle;
237        final float[] mAlphaValues;
238
239        InterruptibleFade(int mode, Runnable middle, float[] alphaValues) {
240            super(mode);
241            mMiddle = middle;
242            mAlphaValues = alphaValues;
243        }
244
245        @Nullable
246        @Override
247        public Animator createAnimator(@NonNull ViewGroup sceneRoot,
248                @Nullable final TransitionValues startValues,
249                @Nullable final TransitionValues endValues) {
250            final Animator animator = super.createAnimator(sceneRoot, startValues, endValues);
251            if (animator instanceof ObjectAnimator) {
252                ((ObjectAnimator) animator).addUpdateListener(
253                        new ValueAnimator.AnimatorUpdateListener() {
254                            @Override
255                            public void onAnimationUpdate(ValueAnimator animation) {
256                                final float alpha = (float) animation.getAnimatedValue();
257                                mAlphaValues[1] = alpha;
258                                if (mInitialAlpha < 0) {
259                                    mInitialAlpha = alpha;
260                                    mAlphaValues[0] = mInitialAlpha;
261                                } else if (Math.abs(alpha - mInitialAlpha) > ALPHA_THRESHOLD) {
262                                    if (mMiddle != null) {
263                                        mMiddle.run();
264                                        mMiddle = null;
265                                    }
266                                }
267                            }
268                        });
269            }
270            return animator;
271        }
272
273    }
274
275}
276