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