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