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