1 /*
2  * Copyright 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 package com.android.managedprovisioning.preprovisioning.anim;
17 
18 import static com.android.internal.util.Preconditions.checkNotNull;
19 
20 import android.animation.Animator;
21 import android.animation.AnimatorInflater;
22 import android.animation.AnimatorSet;
23 import android.animation.ObjectAnimator;
24 import android.annotation.AnimRes;
25 import android.annotation.InterpolatorRes;
26 import android.app.Activity;
27 import android.graphics.drawable.Animatable2;
28 import android.graphics.drawable.AnimatedVectorDrawable;
29 import android.graphics.drawable.Drawable;
30 import androidx.annotation.NonNull;
31 import android.view.ContextThemeWrapper;
32 import android.view.View;
33 import android.view.ViewGroup.LayoutParams;
34 import android.view.animation.AnimationUtils;
35 import android.widget.FrameLayout;
36 import android.widget.ImageView;
37 import android.widget.TextView;
38 
39 import com.android.managedprovisioning.R;
40 import com.android.managedprovisioning.model.CustomizationParams;
41 
42 import java.util.ArrayList;
43 import java.util.List;
44 
45 /**
46  * <p>Drives the animation showing benefits of having a Managed Profile.
47  * <p>Tightly coupled with the {@link R.layout#intro_animation} layout.
48  * The animation consists of 4 horizontally scrolling text labels, placed horizontally.
49  * Each label's horizontal translation is * <code>i*screenWidth/2</code>, where i is the
50  * number of the label (0, 1, 2 or 3) and screenWidth is the actual screen width. We need to
51  * calculate the views' widths dynamically because we want them to fit the display width.
52  */
53 public class BenefitsAnimation {
54     /** Array of Id pairs: {{@link ObjectAnimator}, {@link TextView}} */
55     private static final int[][] ID_ANIMATION_TARGET = {
56             {R.anim.text_scene_0_animation, R.id.text_0},
57             {R.anim.text_scene_1_animation, R.id.text_1},
58             {R.anim.text_scene_2_animation, R.id.text_2},
59             {R.anim.text_scene_3_animation, R.id.text_3}};
60 
61     private static final int[] SLIDE_CAPTION_TEXT_VIEWS = {
62             R.id.text_0, R.id.text_1, R.id.text_2, R.id.text_3};
63 
64     /** Id of an {@link ImageView} containing the animated graphic */
65     private static final int ID_ANIMATED_GRAPHIC = R.id.animated_info;
66 
67     private static final int SLIDE_COUNT = 3;
68     private static final int ANIMATION_ORIGINAL_WIDTH_PX = 1080;
69     private static final int TRANSLATION_DURATION_MS = 1001;
70     private static final float MASTER_CONTAINER_DISPLAY_WIDTH_MULTIPLIER = 2.5f;
71 
72     private final AnimatedVectorDrawable mTopAnimation;
73     private Animator mTextAnimation;
74     private final Activity mActivity;
75 
76     private boolean mStopped;
77 
78     /**
79      * @param captions slide captions for the animation
80      * @param contentDescription for accessibility
81      */
BenefitsAnimation(@onNull Activity activity, @NonNull List<Integer> captions, int contentDescription, CustomizationParams customizationParams)82     public BenefitsAnimation(@NonNull Activity activity, @NonNull List<Integer> captions,
83             int contentDescription, CustomizationParams customizationParams) {
84         if (captions.size() != SLIDE_COUNT) {
85             throw new IllegalArgumentException(
86                     "Wrong number of slide captions. Expected: " + SLIDE_COUNT);
87         }
88         mActivity = checkNotNull(activity);
89         applySlideCaptions(captions);
90         applyContentDescription(contentDescription);
91 
92         setTopInfoDrawable(customizationParams);
93 
94         mTopAnimation = checkNotNull(extractAnimationFromImageView(ID_ANIMATED_GRAPHIC));
95 
96         // once the screen is ready, adjust size
97         mActivity.findViewById(android.R.id.content).post(this::adjustToScreenSize);
98         mActivity.findViewById(android.R.id.content).post(this::prepareAnimations);
99     }
100 
prepareAnimations()101     private void prepareAnimations() {
102         mTextAnimation = checkNotNull(assembleTextAnimation());
103         // chain all animations together
104         chainAnimations();
105     }
106 
setTopInfoDrawable(CustomizationParams customizationParams)107     private void setTopInfoDrawable(CustomizationParams customizationParams) {
108         int swiperTheme = new SwiperThemeMatcher(mActivity, new ColorMatcher())
109                 .findTheme(customizationParams.mainColor);
110 
111         ContextThemeWrapper wrapper = new ContextThemeWrapper(mActivity, swiperTheme);
112         Drawable drawable =
113                 mActivity.getResources().getDrawable(
114                         R.drawable.topinfo_animation,
115                         wrapper.getTheme());
116         ImageView imageView = mActivity.findViewById(ID_ANIMATED_GRAPHIC);
117         imageView.setImageDrawable(drawable);
118     }
119 
120     /** Starts playing the animation in a loop. */
start()121     public void start() {
122         mStopped = false;
123         mTopAnimation.start();
124     }
125 
126     /** Stops the animation. */
stop()127     public void stop() {
128         mStopped = true;
129         mTopAnimation.stop();
130     }
131 
132     /**
133      * Adjust animation and text to match actual screen size
134      */
adjustToScreenSize()135     private void adjustToScreenSize() {
136         if (mActivity.isDestroyed()) {
137             return;
138         }
139 
140         setupLabelDimensions();
141 
142         ImageView animatedInfo = mActivity.findViewById(R.id.animated_info);
143         int widthPx = animatedInfo.getWidth();
144         float scaleRatio = (float) widthPx / ANIMATION_ORIGINAL_WIDTH_PX;
145 
146         // adjust animation height; width happens automatically
147         LayoutParams layoutParams = animatedInfo.getLayoutParams();
148         int originalHeight = animatedInfo.getHeight();
149         int adjustedHeight = (int) (originalHeight * scaleRatio);
150         layoutParams.height = adjustedHeight;
151         animatedInfo.setLayoutParams(layoutParams);
152 
153         // if the content is bigger than the screen, try to shrink just the animation
154         int offset = adjustedHeight - originalHeight;
155         int contentHeight = mActivity.findViewById(R.id.intro_po_content).getHeight() + offset;
156         int viewportHeight = mActivity.findViewById(R.id.suc_layout_content).getHeight();
157         if (contentHeight > viewportHeight) {
158             int targetHeight = layoutParams.height - (contentHeight - viewportHeight);
159             int minHeight = mActivity.getResources().getDimensionPixelSize(
160                     R.dimen.intro_animation_min_height);
161 
162             // if the animation becomes too small, leave it as is and the scrollbar will show
163             if (targetHeight >= minHeight) {
164                 layoutParams.height = targetHeight;
165                 animatedInfo.setLayoutParams(layoutParams);
166             }
167         }
168     }
169 
setupLabelDimensions()170     private void setupLabelDimensions() {
171         final ImageView animatedInfo = mActivity.findViewById(R.id.animated_info);
172         final int width = animatedInfo.getWidth();
173         final FrameLayout textAnimationViewport =
174                 mActivity.findViewById(R.id.text_animation_viewport);
175         final LayoutParams viewportParams = textAnimationViewport.getLayoutParams();
176         viewportParams.width = width;
177         textAnimationViewport.setLayoutParams(viewportParams);
178 
179         final FrameLayout textMasterContainer = mActivity.findViewById(R.id.text_master);
180         final LayoutParams masterContainerParams = textMasterContainer.getLayoutParams();
181         masterContainerParams.width = (int) (width * MASTER_CONTAINER_DISPLAY_WIDTH_MULTIPLIER);
182         textMasterContainer.setLayoutParams(masterContainerParams);
183 
184         int translation = 0;
185         final int step = width / 2;
186         for (int textViewId : SLIDE_CAPTION_TEXT_VIEWS) {
187             final View textView = mActivity.findViewById(textViewId);
188             final LayoutParams layoutParams = textView.getLayoutParams();
189             layoutParams.width = width;
190             textView.setLayoutParams(layoutParams);
191             textView.setTranslationX(translation);
192             translation += step;
193         }
194     }
195 
196     /**
197      * <p>Chains all three sub-animations, and configures them to play in sync in a loop.
198      * <p>Looping {@link AnimatedVectorDrawable} and {@link AnimatorSet} currently not possible in
199      * XML.
200      */
chainAnimations()201     private void chainAnimations() {
202         mTopAnimation.registerAnimationCallback(new Animatable2.AnimationCallback() {
203             @Override
204             public void onAnimationStart(Drawable drawable) {
205                 super.onAnimationStart(drawable);
206 
207                 // starting the other animations at the same time
208                 mTextAnimation.start();
209             }
210 
211             @Override
212             public void onAnimationEnd(Drawable drawable) {
213                 super.onAnimationEnd(drawable);
214 
215                 // without explicitly stopping them, sometimes they won't restart
216                 mTextAnimation.cancel();
217 
218                 // repeating the animation in loop
219                 if (!mStopped) {
220                     mTopAnimation.start();
221                 }
222             }
223         });
224     }
225 
226     /**
227      * <p>Inflates animators required to animate text headers' part of the whole animation.
228      * <p>This has to be done through code, as setting a target on {@link
229      * android.animation.ObjectAnimator} is not currently possible in XML.
230      *
231      * @return {@link AnimatorSet} responsible for the animated text
232      */
assembleTextAnimation()233     private AnimatorSet assembleTextAnimation() {
234         Animator[] animators = new Animator[ID_ANIMATION_TARGET.length + 1];
235         for (int i = 0; i < ID_ANIMATION_TARGET.length; i++) {
236             int[] instance = ID_ANIMATION_TARGET[i];
237             animators[i] = AnimatorInflater.loadAnimator(mActivity, instance[0]);
238             animators[i].setTarget(mActivity.findViewById(instance[1]));
239         }
240         animators[ID_ANIMATION_TARGET.length] = getTranslationAnimatorSet();
241 
242         AnimatorSet animatorSet = new AnimatorSet();
243         animatorSet.playTogether(animators);
244         return animatorSet;
245     }
246 
247     /**
248      * Creates and returns a sequential horizontal translation animation of the benefits labels.
249      */
getTranslationAnimatorSet()250     private AnimatorSet getTranslationAnimatorSet() {
251         ImageView animatedInfo = mActivity.findViewById(R.id.animated_info);
252         final int width = animatedInfo.getWidth();
253         AnimatorSet translationSet = new AnimatorSet();
254         View textContainer = mActivity.findViewById(R.id.text_master);
255         List<Animator> animators = new ArrayList<>();
256         animators.add(getTranslationAnimator(textContainer, 0, 0,
257                 android.R.interpolator.linear));
258         animators.add(getTranslationAnimator(textContainer, 0, -width/2,
259                 android.R.interpolator.fast_out_slow_in));
260         animators.add(getTranslationAnimator(textContainer, -width/2, -width/2,
261                 android.R.interpolator.linear));
262         animators.add(getTranslationAnimator(textContainer, -width/2, -width,
263                 android.R.interpolator.fast_out_slow_in));
264         animators.add(getTranslationAnimator(textContainer, -width, -width,
265                 android.R.interpolator.linear));
266         animators.add(getTranslationAnimator(textContainer, -width, (int) (-width * 1.5),
267                 android.R.interpolator.fast_out_slow_in));
268         translationSet.playSequentially(animators);
269         return translationSet;
270     }
271 
getTranslationAnimator(View container, float from, float to, @AnimRes @InterpolatorRes int interpolator)272     private Animator getTranslationAnimator(View container, float from, float to,
273             @AnimRes @InterpolatorRes int interpolator) {
274         ObjectAnimator animator = ObjectAnimator.ofFloat(container, "translationX", from, to);
275         animator.setDuration(TRANSLATION_DURATION_MS);
276         animator.setInterpolator(AnimationUtils.loadInterpolator(mActivity, interpolator));
277         return animator;
278     }
279 
280     /**
281      * @param captions slide titles
282      */
applySlideCaptions(List<Integer> captions)283     private void applySlideCaptions(List<Integer> captions) {
284         int slideIx = 0;
285         for (int viewId : SLIDE_CAPTION_TEXT_VIEWS) {
286             ((TextView) mActivity.findViewById(viewId)).setText(
287                     captions.get(slideIx++ % captions.size()));
288         }
289     }
290 
applyContentDescription(int contentDescription)291     private void applyContentDescription(int contentDescription) {
292         mActivity.findViewById(R.id.animation_top_level_frame).setContentDescription(
293                 mActivity.getString(contentDescription));
294     }
295 
296     /** Extracts an {@link AnimatedVectorDrawable} from a containing {@link ImageView}. */
extractAnimationFromImageView(int id)297     private AnimatedVectorDrawable extractAnimationFromImageView(int id) {
298         ImageView imageView = mActivity.findViewById(id);
299         return (AnimatedVectorDrawable) imageView.getDrawable();
300     }
301 }