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  */
16 package com.android.messaging.ui.animation;
17 
18 import android.annotation.TargetApi;
19 import android.app.Activity;
20 import android.content.Context;
21 import android.content.res.Resources;
22 import android.graphics.Bitmap;
23 import android.graphics.Canvas;
24 import android.graphics.Rect;
25 import android.graphics.drawable.BitmapDrawable;
26 import android.graphics.drawable.ColorDrawable;
27 import android.graphics.drawable.Drawable;
28 import androidx.core.view.ViewCompat;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.view.ViewGroupOverlay;
32 import android.view.ViewOverlay;
33 import android.widget.FrameLayout;
34 
35 import com.android.messaging.R;
36 import com.android.messaging.util.ImageUtils;
37 import com.android.messaging.util.OsUtil;
38 import com.android.messaging.util.UiUtils;
39 
40 /**
41  * <p>
42  * Shows a vertical "explode" animation for any view inside a view group (e.g. views inside a
43  * ListView). During the animation, a snapshot is taken for the view to the animated and
44  * presented in a popup window or view overlay on top of the original view group. The background
45  * of the view (a highlight) vertically expands (explodes) during the animation.
46  * </p>
47  * <p>
48  * The exact implementation of the animation depends on platform API level. For JB_MR2 and later,
49  * the implementation utilizes ViewOverlay to perform highly performant overlay animations; for
50  * older API levels, the implementation falls back to using a full screen popup window to stage
51  * the animation.
52  * </p>
53  * <p>
54  * To start this animation, call {@link #startAnimationForView(ViewGroup, View, View, boolean, int)}
55  * </p>
56  */
57 public class ViewGroupItemVerticalExplodeAnimation {
58     /**
59      * Starts a vertical explode animation for a given view situated in a given container.
60      *
61      * @param container the container of the view which determines the explode animation's final
62      *        size
63      * @param viewToAnimate the view to be animated. The view will be highlighted by the explode
64      *        highlight, which expands from the size of the view to the size of the container.
65      * @param animationStagingView the view that stages the animation. Since viewToAnimate may be
66      *        removed from the view tree during the animation, we need a view that'll be alive
67      *        for the duration of the animation so that the animation won't get cancelled.
68      * @param snapshotView whether a snapshot of the view to animate is needed.
69      */
startAnimationForView(final ViewGroup container, final View viewToAnimate, final View animationStagingView, final boolean snapshotView, final int duration)70     public static void startAnimationForView(final ViewGroup container, final View viewToAnimate,
71             final View animationStagingView, final boolean snapshotView, final int duration) {
72         if (OsUtil.isAtLeastJB_MR2() && (viewToAnimate.getContext() instanceof Activity)) {
73             new ViewExplodeAnimationJellyBeanMR2(viewToAnimate, container, snapshotView, duration)
74                 .startAnimation();
75         } else {
76             // Pre JB_MR2, this animation can cause rendering failures which causes the framework
77             // to fall back to software rendering where camera preview isn't supported (b/18264647)
78             // just skip the animation to avoid this case.
79         }
80     }
81 
82     /**
83      * Implementation class for API level >= 18.
84      */
85     @TargetApi(18)
86     private static class ViewExplodeAnimationJellyBeanMR2 {
87         private final View mViewToAnimate;
88         private final ViewGroup mContainer;
89         private final View mSnapshot;
90         private final Bitmap mViewBitmap;
91         private final int mDuration;
92 
ViewExplodeAnimationJellyBeanMR2(final View viewToAnimate, final ViewGroup container, final boolean snapshotView, final int duration)93         public ViewExplodeAnimationJellyBeanMR2(final View viewToAnimate, final ViewGroup container,
94                 final boolean snapshotView, final int duration) {
95             mViewToAnimate = viewToAnimate;
96             mContainer = container;
97             mDuration = duration;
98             if (snapshotView) {
99                 mViewBitmap = snapshotView(viewToAnimate);
100                 mSnapshot = new View(viewToAnimate.getContext());
101             } else {
102                 mSnapshot = null;
103                 mViewBitmap = null;
104             }
105         }
106 
startAnimation()107         public void startAnimation() {
108             final Context context = mViewToAnimate.getContext();
109             final Resources resources = context.getResources();
110             final View decorView = ((Activity) context).getWindow().getDecorView();
111             final ViewOverlay viewOverlay = decorView.getOverlay();
112             if (viewOverlay instanceof ViewGroupOverlay) {
113                 final ViewGroupOverlay overlay = (ViewGroupOverlay) viewOverlay;
114 
115                 // Add a shadow layer to the overlay.
116                 final FrameLayout shadowContainerLayer = new FrameLayout(context);
117                 final Drawable oldBackground = mViewToAnimate.getBackground();
118                 final Rect containerRect = UiUtils.getMeasuredBoundsOnScreen(mContainer);
119                 final Rect decorRect = UiUtils.getMeasuredBoundsOnScreen(decorView);
120                 // Position the container rect relative to the decor rect since the decor rect
121                 // defines whether the view overlay will be positioned.
122                 containerRect.offset(-decorRect.left, -decorRect.top);
123                 shadowContainerLayer.setLeft(containerRect.left);
124                 shadowContainerLayer.setTop(containerRect.top);
125                 shadowContainerLayer.setBottom(containerRect.bottom);
126                 shadowContainerLayer.setRight(containerRect.right);
127                 shadowContainerLayer.setBackgroundColor(resources.getColor(
128                         R.color.open_conversation_animation_background_shadow));
129                 // Per design request, temporarily clear out the background of the item content
130                 // to not show any ripple effects during animation.
131                 if (!(oldBackground instanceof ColorDrawable)) {
132                     mViewToAnimate.setBackground(null);
133                 }
134                 overlay.add(shadowContainerLayer);
135 
136                 // Add a expand layer and position it with in the shadow background, so it can
137                 // be properly clipped to the container bounds during the animation.
138                 final View expandLayer = new View(context);
139                 final int elevation = resources.getDimensionPixelSize(
140                         R.dimen.explode_animation_highlight_elevation);
141                 final Rect viewRect = UiUtils.getMeasuredBoundsOnScreen(mViewToAnimate);
142                 // Frame viewRect from screen space to containerRect space.
143                 viewRect.offset(-containerRect.left - decorRect.left,
144                         -containerRect.top - decorRect.top);
145                 // Since the expand layer expands at the same rate above and below, we need to
146                 // compute the expand scale using the bigger of the top/bottom distances.
147                 final int expandLayerHalfHeight = viewRect.height() / 2;
148                 final int topDist = viewRect.top;
149                 final int bottomDist = containerRect.height() - viewRect.bottom;
150                 final float scale = expandLayerHalfHeight == 0 ? 1 :
151                         ((float) Math.max(topDist, bottomDist) + expandLayerHalfHeight) /
152                         expandLayerHalfHeight;
153                 // Position the expand layer initially to exactly match the animated item.
154                 shadowContainerLayer.addView(expandLayer);
155                 expandLayer.setLeft(viewRect.left);
156                 expandLayer.setTop(viewRect.top);
157                 expandLayer.setBottom(viewRect.bottom);
158                 expandLayer.setRight(viewRect.right);
159                 expandLayer.setBackgroundColor(resources.getColor(
160                         R.color.conversation_background));
161                 ViewCompat.setElevation(expandLayer, elevation);
162 
163                 // Conditionally stage the snapshot in the overlay.
164                 if (mSnapshot != null) {
165                     shadowContainerLayer.addView(mSnapshot);
166                     mSnapshot.setLeft(viewRect.left);
167                     mSnapshot.setTop(viewRect.top);
168                     mSnapshot.setBottom(viewRect.bottom);
169                     mSnapshot.setRight(viewRect.right);
170                     mSnapshot.setBackground(new BitmapDrawable(resources, mViewBitmap));
171                     ViewCompat.setElevation(mSnapshot, elevation);
172                 }
173 
174                 // Apply a scale animation to scale to full screen.
175                 expandLayer.animate().scaleY(scale)
176                     .setDuration(mDuration)
177                     .setInterpolator(UiUtils.EASE_IN_INTERPOLATOR)
178                     .withEndAction(new Runnable() {
179                         @Override
180                         public void run() {
181                             // Clean up the views added to overlay on animation finish.
182                             overlay.remove(shadowContainerLayer);
183                             mViewToAnimate.setBackground(oldBackground);
184                             if (mViewBitmap != null) {
185                                 mViewBitmap.recycle();
186                             }
187                         }
188                 });
189             }
190         }
191     }
192 
193     /**
194      * Take a snapshot of the given review, return a Bitmap object that's owned by the caller.
195      */
snapshotView(final View view)196     static Bitmap snapshotView(final View view) {
197         // Save the content of the view into a bitmap.
198         final Bitmap viewBitmap = Bitmap.createBitmap(view.getWidth(),
199                 view.getHeight(), Bitmap.Config.ARGB_8888);
200         // Strip the view of its background when taking a snapshot so that things like touch
201         // feedback don't get accidentally snapshotted.
202         final Drawable viewBackground = view.getBackground();
203         ImageUtils.setBackgroundDrawableOnView(view, null);
204         view.draw(new Canvas(viewBitmap));
205         ImageUtils.setBackgroundDrawableOnView(view, viewBackground);
206         return viewBitmap;
207     }
208 }
209