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 android.support.v4.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 */ 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 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 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 */ 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