/* * Copyright (C) 2013 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.camera.ui; import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import com.android.camera.app.CameraAppUI; import com.android.camera.debug.Log; import com.android.camera.util.Gusterpolator; import com.android.camera2.R; /** * This view is designed to handle all the animations during camera mode transition. * It should only be visible during mode switch. */ public class ModeTransitionView extends View { private static final Log.Tag TAG = new Log.Tag("ModeTransView"); private static final int PEEP_HOLE_ANIMATION_DURATION_MS = 300; private static final int ICON_FADE_OUT_DURATION_MS = 850; private static final int FADE_OUT_DURATION_MS = 250; private static final int IDLE = 0; private static final int PULL_UP_SHADE = 1; private static final int PULL_DOWN_SHADE = 2; private static final int PEEP_HOLE_ANIMATION = 3; private static final int FADE_OUT = 4; private static final int SHOW_STATIC_IMAGE = 5; private static final float SCROLL_DISTANCE_MULTIPLY_FACTOR = 2f; private static final int ALPHA_FULLY_TRANSPARENT = 0; private static final int ALPHA_FULLY_OPAQUE = 255; private static final int ALPHA_HALF_TRANSPARENT = 127; private final GestureDetector mGestureDetector; private final Paint mMaskPaint = new Paint(); private final Rect mIconRect = new Rect(); /** An empty drawable to fall back to when mIconDrawable set to null. */ private final Drawable mDefaultDrawable = new ColorDrawable(); private Drawable mIconDrawable; private int mBackgroundColor; private int mWidth = 0; private int mHeight = 0; private int mPeepHoleCenterX = 0; private int mPeepHoleCenterY = 0; private float mRadius = 0f; private int mIconSize; private AnimatorSet mPeepHoleAnimator; private int mAnimationType = PEEP_HOLE_ANIMATION; private float mScrollDistance = 0; private final Path mShadePath = new Path(); private final Paint mShadePaint = new Paint(); private CameraAppUI.AnimationFinishedListener mAnimationFinishedListener; private float mScrollTrend; private Bitmap mBackgroundBitmap; public ModeTransitionView(Context context, AttributeSet attrs) { super(context, attrs); mMaskPaint.setAlpha(0); mMaskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); mBackgroundColor = getResources().getColor(R.color.video_mode_color); mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent ev) { setScrollDistance(0f); mScrollTrend = 0f; return true; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { setScrollDistance(getScrollDistance() + SCROLL_DISTANCE_MULTIPLY_FACTOR * distanceY); mScrollTrend = 0.3f * mScrollTrend + 0.7f * distanceY; return false; } }); mIconSize = getResources().getDimensionPixelSize(R.dimen.mode_transition_view_icon_size); setIconDrawable(mDefaultDrawable); } /** * Updates the size and shape of the shade */ private void updateShade() { if (mAnimationType == PULL_UP_SHADE || mAnimationType == PULL_DOWN_SHADE) { mShadePath.reset(); float shadeHeight; if (mAnimationType == PULL_UP_SHADE) { // Scroll distance > 0. mShadePath.addRect(0, mHeight - getScrollDistance(), mWidth, mHeight, Path.Direction.CW); shadeHeight = getScrollDistance(); } else { // Scroll distance < 0. mShadePath.addRect(0, 0, mWidth, - getScrollDistance(), Path.Direction.CW); shadeHeight = getScrollDistance() * (-1); } if (mIconDrawable != null) { if (shadeHeight < mHeight / 2 || mHeight == 0) { mIconDrawable.setAlpha(ALPHA_FULLY_TRANSPARENT); } else { int alpha = ((int) shadeHeight - mHeight / 2) * ALPHA_FULLY_OPAQUE / (mHeight / 2); mIconDrawable.setAlpha(alpha); } } invalidate(); } } /** * Sets the scroll distance. Note this function gets called in every * frame during animation. It should be very light weight. * * @param scrollDistance the scaled distance that user has scrolled */ public void setScrollDistance(float scrollDistance) { // First make sure scroll distance is clamped to the valid range. if (mAnimationType == PULL_UP_SHADE) { scrollDistance = Math.min(scrollDistance, mHeight); scrollDistance = Math.max(scrollDistance, 0); } else if (mAnimationType == PULL_DOWN_SHADE) { scrollDistance = Math.min(scrollDistance, 0); scrollDistance = Math.max(scrollDistance, -mHeight); } mScrollDistance = scrollDistance; updateShade(); } public float getScrollDistance() { return mScrollDistance; } @Override public void onDraw(Canvas canvas) { if (mAnimationType == PEEP_HOLE_ANIMATION) { canvas.drawColor(mBackgroundColor); if (mPeepHoleAnimator != null) { // Draw a transparent circle using clear mode canvas.drawCircle(mPeepHoleCenterX, mPeepHoleCenterY, mRadius, mMaskPaint); } } else if (mAnimationType == PULL_UP_SHADE || mAnimationType == PULL_DOWN_SHADE) { canvas.drawPath(mShadePath, mShadePaint); } else if (mAnimationType == IDLE || mAnimationType == FADE_OUT) { canvas.drawColor(mBackgroundColor); } else if (mAnimationType == SHOW_STATIC_IMAGE) { // TODO: These different animation types need to be refactored into // different animation effects. canvas.drawBitmap(mBackgroundBitmap, 0, 0, null); super.onDraw(canvas); return; } super.onDraw(canvas); mIconDrawable.draw(canvas); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { mWidth = right - left; mHeight = bottom - top; // Center the icon in the view. mIconRect.set(mWidth / 2 - mIconSize / 2, mHeight / 2 - mIconSize / 2, mWidth / 2 + mIconSize / 2, mHeight / 2 + mIconSize / 2); mIconDrawable.setBounds(mIconRect); } /** * This is an overloaded function. When no position is provided for the animation, * the peep hole will start at the default position (i.e. center of the view). */ public void startPeepHoleAnimation() { float x = mWidth / 2; float y = mHeight / 2; startPeepHoleAnimation(x, y); } /** * Starts the peep hole animation where the circle is centered at position (x, y). */ private void startPeepHoleAnimation(float x, float y) { if (mPeepHoleAnimator != null && mPeepHoleAnimator.isRunning()) { return; } mAnimationType = PEEP_HOLE_ANIMATION; mPeepHoleCenterX = (int) x; mPeepHoleCenterY = (int) y; int horizontalDistanceToFarEdge = Math.max(mPeepHoleCenterX, mWidth - mPeepHoleCenterX); int verticalDistanceToFarEdge = Math.max(mPeepHoleCenterY, mHeight - mPeepHoleCenterY); int endRadius = (int) (Math.sqrt(horizontalDistanceToFarEdge * horizontalDistanceToFarEdge + verticalDistanceToFarEdge * verticalDistanceToFarEdge)); final ValueAnimator radiusAnimator = ValueAnimator.ofFloat(0, endRadius); radiusAnimator.setDuration(PEEP_HOLE_ANIMATION_DURATION_MS); final ValueAnimator iconScaleAnimator = ValueAnimator.ofFloat(1f, 0.5f); iconScaleAnimator.setDuration(ICON_FADE_OUT_DURATION_MS); final ValueAnimator iconAlphaAnimator = ValueAnimator.ofInt(ALPHA_HALF_TRANSPARENT, ALPHA_FULLY_TRANSPARENT); iconAlphaAnimator.setDuration(ICON_FADE_OUT_DURATION_MS); mPeepHoleAnimator = new AnimatorSet(); mPeepHoleAnimator.playTogether(radiusAnimator, iconAlphaAnimator, iconScaleAnimator); mPeepHoleAnimator.setInterpolator(Gusterpolator.INSTANCE); iconAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { // Modify mask by enlarging the hole mRadius = (Float) radiusAnimator.getAnimatedValue(); mIconDrawable.setAlpha((Integer) iconAlphaAnimator.getAnimatedValue()); float scale = (Float) iconScaleAnimator.getAnimatedValue(); int size = (int) (scale * (float) mIconSize); mIconDrawable.setBounds(mPeepHoleCenterX - size / 2, mPeepHoleCenterY - size / 2, mPeepHoleCenterX + size / 2, mPeepHoleCenterY + size / 2); invalidate(); } }); mPeepHoleAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { // Sets a HW layer on the view for the animation. setLayerType(LAYER_TYPE_HARDWARE, null); } @Override public void onAnimationEnd(Animator animation) { // Sets the layer type back to NONE as a workaround for b/12594617. setLayerType(LAYER_TYPE_NONE, null); mPeepHoleAnimator = null; mRadius = 0; mIconDrawable.setAlpha(ALPHA_FULLY_OPAQUE); mIconDrawable.setBounds(mIconRect); setVisibility(GONE); mAnimationType = IDLE; if (mAnimationFinishedListener != null) { mAnimationFinishedListener.onAnimationFinished(true); mAnimationFinishedListener = null; } } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); mPeepHoleAnimator.start(); } @Override public boolean onTouchEvent(MotionEvent ev) { boolean touchHandled = mGestureDetector.onTouchEvent(ev); if (ev.getActionMasked() == MotionEvent.ACTION_UP) { // TODO: Take into account fling snap(); } return touchHandled; } /** * Snaps the shade to position at the end of a gesture. */ private void snap() { if (mScrollTrend >= 0 && mAnimationType == PULL_UP_SHADE) { // Snap to full screen. snapShadeTo(mHeight, ALPHA_FULLY_OPAQUE); } else if (mScrollTrend <= 0 && mAnimationType == PULL_DOWN_SHADE) { // Snap to full screen. snapShadeTo(-mHeight, ALPHA_FULLY_OPAQUE); } else if (mScrollTrend < 0 && mAnimationType == PULL_UP_SHADE) { // Snap back. snapShadeTo(0, ALPHA_FULLY_TRANSPARENT, false); } else if (mScrollTrend > 0 && mAnimationType == PULL_DOWN_SHADE) { // Snap back. snapShadeTo(0, ALPHA_FULLY_TRANSPARENT, false); } } private void snapShadeTo(int scrollDistance, int alpha) { snapShadeTo(scrollDistance, alpha, true); } /** * Snaps the shade to a given scroll distance and sets the icon alpha. If the shade * is to snap back out, then hide the view after the animation. * * @param scrollDistance scaled user scroll distance * @param alpha ending alpha of the icon drawable * @param snapToFullScreen whether this snap animation snaps the shade to full screen */ private void snapShadeTo(final int scrollDistance, final int alpha, final boolean snapToFullScreen) { if (mAnimationType == PULL_UP_SHADE || mAnimationType == PULL_DOWN_SHADE) { ObjectAnimator scrollAnimator = ObjectAnimator.ofFloat(this, "scrollDistance", scrollDistance); scrollAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { setScrollDistance(scrollDistance); mIconDrawable.setAlpha(alpha); mAnimationType = IDLE; if (!snapToFullScreen) { setVisibility(GONE); } if (mAnimationFinishedListener != null) { mAnimationFinishedListener.onAnimationFinished(snapToFullScreen); mAnimationFinishedListener = null; } } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); scrollAnimator.setInterpolator(Gusterpolator.INSTANCE); scrollAnimator.start(); } } /** * Set the states for the animation that pulls up a shade with given shade color. * * @param shadeColorId color id of the shade that will be pulled up * @param iconId id of the icon that will appear on top the shade * @param listener a listener that will get notified when the animation * is finished. Could be null. */ public void prepareToPullUpShade(int shadeColorId, int iconId, CameraAppUI.AnimationFinishedListener listener) { prepareShadeAnimation(PULL_UP_SHADE, shadeColorId, iconId, listener); } /** * Set the states for the animation that pulls down a shade with given shade color. * * @param shadeColorId color id of the shade that will be pulled down * @param modeIconResourceId id of the icon that will appear on top the shade * @param listener a listener that will get notified when the animation * is finished. Could be null. */ public void prepareToPullDownShade(int shadeColorId, int modeIconResourceId, CameraAppUI.AnimationFinishedListener listener) {; prepareShadeAnimation(PULL_DOWN_SHADE, shadeColorId, modeIconResourceId, listener); } /** * Set the states for the animation that involves a shade. * * @param animationType type of animation that will happen to the shade * @param shadeColorId color id of the shade that will be animated * @param iconResId id of the icon that will appear on top the shade * @param listener a listener that will get notified when the animation * is finished. Could be null. */ private void prepareShadeAnimation(int animationType, int shadeColorId, int iconResId, CameraAppUI.AnimationFinishedListener listener) { mAnimationFinishedListener = listener; if (mPeepHoleAnimator != null && mPeepHoleAnimator.isRunning()) { mPeepHoleAnimator.end(); } mAnimationType = animationType; resetShade(shadeColorId, iconResId); } /** * Reset the shade with the given shade color and icon drawable. * * @param shadeColorId id of the shade color * @param modeIconResourceId resource id of the icon drawable */ private void resetShade(int shadeColorId, int modeIconResourceId) { // Sets color for the shade. int shadeColor = getResources().getColor(shadeColorId); mBackgroundColor = shadeColor; mShadePaint.setColor(shadeColor); // Reset scroll distance. setScrollDistance(0f); // Sets new drawable. updateIconDrawableByResourceId(modeIconResourceId); mIconDrawable.setAlpha(0); setVisibility(VISIBLE); } /** * By default, all drawables instances loaded from the same resource share a * common state; if you modify the state of one instance, all the other * instances will receive the same modification. So here we need to make sure * we mutate the drawable loaded from resource. * * @param modeIconResourceId resource id of the icon drawable */ private void updateIconDrawableByResourceId(int modeIconResourceId) { Drawable iconDrawable = getResources().getDrawable(modeIconResourceId); if (iconDrawable == null) { // Resource id not found Log.e(TAG, "Invalid resource id for icon drawable. Setting icon drawable to null."); setIconDrawable(null); return; } // Mutate the drawable loaded from resource so modifying its states does // not affect other drawable instances loaded from the same resource. setIconDrawable(iconDrawable.mutate()); } /** * In order to make sure icon drawable is never set to null. Fall back to an * empty drawable when icon needs to get reset. * * @param iconDrawable new drawable for icon. A value of null sets * the icon drawable to the default drawable. */ private void setIconDrawable(Drawable iconDrawable) { if (iconDrawable == null) { mIconDrawable = mDefaultDrawable; } else { mIconDrawable = iconDrawable; } } /** * Initialize the mode cover with a mode theme color and a mode icon. * * @param colorId resource id of the mode theme color * @param modeIconResourceId resource id of the icon drawable */ public void setupModeCover(int colorId, int modeIconResourceId) { mBackgroundBitmap = null; // Stop ongoing animation. if (mPeepHoleAnimator != null && mPeepHoleAnimator.isRunning()) { mPeepHoleAnimator.cancel(); } mAnimationType = IDLE; mBackgroundColor = getResources().getColor(colorId); // Sets new drawable. updateIconDrawableByResourceId(modeIconResourceId); mIconDrawable.setAlpha(ALPHA_FULLY_OPAQUE); setVisibility(VISIBLE); } /** * Hides the cover view and notifies the * {@link com.android.camera.app.CameraAppUI.AnimationFinishedListener} of whether * the hide animation is successfully finished. * * @param animationFinishedListener a listener that will get notified when the * animation is finished. Could be null. */ public void hideModeCover( final CameraAppUI.AnimationFinishedListener animationFinishedListener) { if (mAnimationType != IDLE) { // Nothing to hide. if (animationFinishedListener != null) { // Animation not successful. animationFinishedListener.onAnimationFinished(false); } } else { // Start fade out animation. mAnimationType = FADE_OUT; ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(this, "alpha", 1f, 0f); alphaAnimator.setDuration(FADE_OUT_DURATION_MS); // Linear interpolation. alphaAnimator.setInterpolator(null); alphaAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { setVisibility(GONE); setAlpha(1f); if (animationFinishedListener != null) { animationFinishedListener.onAnimationFinished(true); mAnimationType = IDLE; } } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); alphaAnimator.start(); } } @Override public void setAlpha(float alpha) { super.setAlpha(alpha); int alphaScaled = (int) (255f * getAlpha()); mBackgroundColor = (mBackgroundColor & 0xFFFFFF) | (alphaScaled << 24); mIconDrawable.setAlpha(alphaScaled); } /** * Setup the mode cover with a screenshot. */ public void setupModeCover(Bitmap screenShot) { mBackgroundBitmap = screenShot; setVisibility(VISIBLE); mAnimationType = SHOW_STATIC_IMAGE; } /** * Hide the mode cover without animation. */ // TODO: Refactor this and define how cover should be hidden during cover setup public void hideImageCover() { mBackgroundBitmap = null; setVisibility(GONE); mAnimationType = IDLE; } }