diff options
author | Doris Liu <tianliu@google.com> | 2013-12-11 18:39:14 -0800 |
---|---|---|
committer | Doris Liu <tianliu@google.com> | 2013-12-13 16:28:32 -0800 |
commit | b6eaa8c70da19fb3233c3c4b1a2625ce90fc35af (patch) | |
tree | 7ce04801fff55e7c4654b87d3db01c43b276136e | |
parent | 83ebad2e83e9b0917b263d17623f43764665ad7b (diff) | |
download | android_packages_apps_Camera2-b6eaa8c70da19fb3233c3c4b1a2625ce90fc35af.tar.gz android_packages_apps_Camera2-b6eaa8c70da19fb3233c3c4b1a2625ce90fc35af.tar.bz2 android_packages_apps_Camera2-b6eaa8c70da19fb3233c3c4b1a2625ce90fc35af.zip |
Mode selection animation
Change-Id: I3741f71add7e506a974602cefa6edfdbeb442fa0
-rw-r--r-- | res/layout/mode_list_layout.xml | 2 | ||||
-rw-r--r-- | res/values/colors.xml | 1 | ||||
-rw-r--r-- | src/com/android/camera/app/CameraAppUI.java | 56 | ||||
-rw-r--r-- | src/com/android/camera/ui/ModeListView.java | 205 | ||||
-rw-r--r-- | src/com/android/camera/ui/ModeSelectorItem.java | 25 | ||||
-rw-r--r-- | src/com/android/camera/widget/AnimationEffects.java | 51 |
6 files changed, 322 insertions, 18 deletions
diff --git a/res/layout/mode_list_layout.xml b/res/layout/mode_list_layout.xml index 9b2172e6d..24ddf95fe 100644 --- a/res/layout/mode_list_layout.xml +++ b/res/layout/mode_list_layout.xml @@ -20,6 +20,8 @@ android:fillViewport="true" android:layout_width="match_parent" android:layout_height="match_parent" + android:layerType="hardware" + android:background="@null" android:visibility="invisible" > <LinearLayout android:id="@+id/mode_list" diff --git a/res/values/colors.xml b/res/values/colors.xml index 81c64c598..73f78bd07 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -74,6 +74,7 @@ <color name="mode_selector_text_color">#6d6d6d</color> <color name="mode_selector_background_light">#f5f5f5</color> <color name="mode_selector_background_dark">#e7e7e7</color> + <color name="mode_selector_text_highlight_color">#ffffffff</color> <color name="mode_list_background">#00000000</color> <color name="camera_mode_color">#76a7fa</color> diff --git a/src/com/android/camera/app/CameraAppUI.java b/src/com/android/camera/app/CameraAppUI.java index 46377810a..eeac0cada 100644 --- a/src/com/android/camera/app/CameraAppUI.java +++ b/src/com/android/camera/app/CameraAppUI.java @@ -194,6 +194,7 @@ public class CameraAppUI implements ModeListView.ModeSwitchListener, private int mModeCoverState = COVER_HIDDEN; private FilmstripBottomControls mFilmstripBottomControls; private FilmstripContentPanel mFilmstripPanel; + private Runnable mHideCoverRunnable; // TODO this isn't used by all modules universally, should be part of a util class or something /** @@ -294,7 +295,7 @@ public class CameraAppUI implements ModeListView.ModeSwitchListener, public boolean onScroll(MotionEvent e1, MotionEvent ev, float distanceX, float distanceY) { if (ev.getEventTime() - ev.getDownTime() > SWIPE_TIME_OUT_MS || mSwipeState != IDLE) { - return true; + return false; } int deltaX = (int) (ev.getX() - mDown.getX()); @@ -429,19 +430,28 @@ public class CameraAppUI implements ModeListView.ModeSwitchListener, int colorId = ModeListView.getModeThemeColor(modeId); int iconId = ModeListView.getModeIconResourceId(modeId); mModeTransitionView.setupModeCover(colorId, iconId); + mHideCoverRunnable = new Runnable() { + @Override + public void run() { + mModeTransitionView.hideModeCover(new AnimationFinishedListener() { + @Override + public void onAnimationFinished(boolean success) { + if (success) { + // Show shimmy in SHIMMY_DELAY_MS + mModeListView.startAccordionAnimationWithDelay(SHIMMY_DELAY_MS); + } + } + }); + } + }; mModeCoverState = COVER_SHOWN; } private void hideModeCover() { - mModeTransitionView.hideModeCover(new AnimationFinishedListener() { - @Override - public void onAnimationFinished(boolean success) { - if (success) { - // Show shimmy in SHIMMY_DELAY_MS - mModeListView.startAccordionAnimationWithDelay(SHIMMY_DELAY_MS); - } - } - }); + if (mHideCoverRunnable != null) { + mAppRootView.post(mHideCoverRunnable); + mHideCoverRunnable = null; + } } /** @@ -542,9 +552,35 @@ public class CameraAppUI implements ModeListView.ModeSwitchListener, } } + /** + * Gets called when a mode is selected from {@link com.android.camera.ui.ModeListView} + * + * @param modeIndex mode index of the selected mode + */ @Override public void onModeSelected(int modeIndex) { mController.onModeSelected(modeIndex); + mHideCoverRunnable = new Runnable() { + @Override + public void run() { + mModeListView.startModeSelectionAnimation(); + } + }; + + if (mTextureView == null) { + // TODO: Remove this when all the modules use TextureView + int temporaryDelay = 600; // ms + mModeListView.postDelayed(new Runnable() { + @Override + public void run() { + hideModeCover(); + } + }, temporaryDelay); + } else if (mTextureView.getSurfaceTexture() != null) { + hideModeCover(); + } else { + mModeCoverState = COVER_SHOWN; + } } /** diff --git a/src/com/android/camera/ui/ModeListView.java b/src/com/android/camera/ui/ModeListView.java index 7f4671fcc..00baacea4 100644 --- a/src/com/android/camera/ui/ModeListView.java +++ b/src/com/android/camera/ui/ModeListView.java @@ -23,6 +23,10 @@ import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.Configuration; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; import android.os.SystemClock; import android.util.AttributeSet; import android.util.Log; @@ -33,6 +37,7 @@ import android.widget.LinearLayout; import android.widget.ScrollView; import com.android.camera.util.Gusterpolator; +import com.android.camera.widget.AnimationEffects; import com.android.camera2.R; import java.util.ArrayList; @@ -76,6 +81,7 @@ public class ModeListView extends ScrollView { private static final int FULLY_SHOWN = 1; private static final int ACCORDION_ANIMATION = 2; private static final int SCROLLING = 3; + private static final int MODE_SELECTED = 4; // Scrolling delay between non-focused item and focused item private static final int DELAY_MS = 25; @@ -104,6 +110,7 @@ public class ModeListView extends ScrollView { private ModeSelectorItem[] mModeSelectorItems; private AnimatorSet mAnimatorSet; private int mFocusItem = NO_ITEM_SELECTED; + private AnimationEffects mCurrentEffect; // Width and height of this view. They get updated in onLayout() // Unit for width and height are pixels. @@ -224,6 +231,21 @@ public class ModeListView extends ScrollView { // Validate the selection if (index != NO_ITEM_SELECTED) { int modeId = getModeIndex(index); + mModeSelectorItems[index].highlight(); + mState = MODE_SELECTED; + PeepholeAnimationEffect effect = new PeepholeAnimationEffect(); + effect.setSize(mWidth, mHeight); + effect.setAnimationEndAction(new Runnable() { + @Override + public void run() { + setVisibility(INVISIBLE); + mCurrentEffect = null; + snapBack(false); + } + }); + effect.setAnimationStartingPosition((int) ev.getX(), (int) ev.getY()); + mCurrentEffect = effect; + onModeSelected(modeId); } return true; @@ -305,10 +327,10 @@ public class ModeListView extends ScrollView { mListView.addView(selectorItem); // Set alternating background color for each mode selector in the list if (i % 2 == 0) { - selectorItem.setBackgroundColor(getResources() + selectorItem.setDefaultBackgroundColor(getResources() .getColor(R.color.mode_selector_background_light)); } else { - selectorItem.setBackgroundColor(getResources() + selectorItem.setDefaultBackgroundColor(getResources() .getColor(R.color.mode_selector_background_dark)); } int modeId = getModeIndex(i); @@ -347,9 +369,6 @@ public class ModeListView extends ScrollView { if (mListener != null) { mListener.onModeSelected(modeIndex); } - // TODO: There will be another animation indicating selection - // for now, just snap back. - snapBack(); } /** @@ -363,6 +382,10 @@ public class ModeListView extends ScrollView { @Override public boolean onTouchEvent(MotionEvent ev) { + if (mCurrentEffect != null) { + return mCurrentEffect.onTouchEvent(ev); + } + super.onTouchEvent(ev); if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { getParent().requestDisallowInterceptTouchEvent(true); @@ -401,6 +424,9 @@ public class ModeListView extends ScrollView { super.onLayout(changed, left, top, right, bottom); mWidth = right - left; mHeight = bottom - top - getPaddingTop() - getPaddingBottom(); + if (mCurrentEffect != null) { + mCurrentEffect.setSize(mWidth, mHeight); + } } /** @@ -439,11 +465,25 @@ public class ModeListView extends ScrollView { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } + @Override + public void draw(Canvas canvas) { + if (mCurrentEffect != null) { + mCurrentEffect.drawBackground(canvas); + super.draw(canvas); + mCurrentEffect.drawForeground(canvas); + } else { + super.draw(canvas); + } + } + /** * This starts the accordion animation, unless it's already running, in which * case the start animation call will be ignored. */ public void startAccordionAnimation() { + if (mState != IDLE) { + return; + } if (mAnimatorSet != null && mAnimatorSet.isRunning()) { return; } @@ -473,6 +513,7 @@ public class ModeListView extends ScrollView { private void resetModeSelectors() { for (int i = 0; i < mModeSelectorItems.length; i++) { mModeSelectorItems[i].setVisibleWidth(0); + mModeSelectorItems[i].unHighlight(); } } @@ -611,6 +652,7 @@ public class ModeListView extends ScrollView { resetModeSelectors(); mScrollTrendX = 0f; mScrollTrendY = 0f; + mCurrentEffect = null; setVisibility(INVISIBLE); } @@ -632,6 +674,16 @@ public class ModeListView extends ScrollView { } } + @Override + public void onWindowVisibilityChanged(int visibility) { + super.onWindowVisibilityChanged(visibility); + if (visibility != VISIBLE) { + // Reset mode list if the window is no longer visible. + reset(); + mState = IDLE; + } + } + /** * The list view should either snap back or snap to full screen after a gesture. * This function is called when an up or cancel event is received, and then based @@ -651,9 +703,27 @@ public class ModeListView extends ScrollView { } } + /** + * Snaps back out of the screen. + * + * @param withAnimation whether snapping back should be animated + */ + public void snapBack(boolean withAnimation) { + if (withAnimation) { + animateListToWidth(0); + mState = IDLE; + } else { + setVisibility(INVISIBLE); + resetModeSelectors(); + mState = IDLE; + } + } + + /** + * Snaps the mode list back out with animation. + */ private void snapBack() { - animateListToWidth(0); - mState = IDLE; + snapBack(true); } private void snapToFullScreen() { @@ -771,4 +841,125 @@ public class ModeListView extends ScrollView { return mIconResId[modeIndex]; } } + + public void startModeSelectionAnimation() { + if (mState != MODE_SELECTED || mCurrentEffect == null) { + setVisibility(INVISIBLE); + snapBack(false); + mCurrentEffect = null; + } else { + mCurrentEffect.startAnimation(); + } + + } + + private class PeepholeAnimationEffect extends AnimationEffects { + + private final static int UNSET = -1; + private final static int PEEP_HOLE_ANIMATION_DURATION_MS = 650; + + private int mWidth; + private int mHeight; + + private int mPeepHoleCenterX = UNSET; + private int mPeepHoleCenterY = UNSET; + private float mRadius = 0f; + private ValueAnimator mPeepHoleAnimator; + private Runnable mEndAction; + private final Paint mMaskPaint = new Paint(); + + public PeepholeAnimationEffect() { + mMaskPaint.setAlpha(0); + mMaskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + } + + public void setSize(int width, int height) { + mWidth = width; + mHeight = height; + } + + public void drawForeground(Canvas canvas) { + // Draw the circle in clear mode + if (mPeepHoleAnimator != null) { + // Draw a transparent circle using clear mode + canvas.drawCircle(mPeepHoleCenterX, mPeepHoleCenterY, mRadius, mMaskPaint); + } + } + + public void setAnimationStartingPosition(int x, int y) { + mPeepHoleCenterX = x; + mPeepHoleCenterY = y; + } + + public void startAnimation() { + if (mPeepHoleAnimator != null && mPeepHoleAnimator.isRunning()) { + return; + } + if (mPeepHoleCenterY == UNSET || mPeepHoleCenterX == UNSET) { + mPeepHoleCenterX = mWidth / 2; + mPeepHoleCenterY = mHeight / 2; + } + + int horizontalDistanceToFarEdge = Math.max(mPeepHoleCenterX, mWidth - mPeepHoleCenterX); + int verticalDistanceToFarEdge = Math.max(mPeepHoleCenterY, mHeight - mPeepHoleCenterY); + int endRadius = (int) (Math.sqrt(horizontalDistanceToFarEdge * horizontalDistanceToFarEdge + + verticalDistanceToFarEdge * verticalDistanceToFarEdge)); + + mPeepHoleAnimator = ValueAnimator.ofFloat(0, endRadius); + mPeepHoleAnimator.setDuration(PEEP_HOLE_ANIMATION_DURATION_MS); + mPeepHoleAnimator.setInterpolator(Gusterpolator.INSTANCE); + mPeepHoleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + // Modify mask by enlarging the hole + mRadius = (Float) mPeepHoleAnimator.getAnimatedValue(); + invalidate(); + } + }); + + mPeepHoleAnimator.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + + } + + @Override + public void onAnimationEnd(Animator animation) { + if (mEndAction != null) { + post(mEndAction); + mEndAction = null; + post(new Runnable() { + @Override + public void run() { + mPeepHoleAnimator = null; + mRadius = 0; + mPeepHoleCenterX = UNSET; + mPeepHoleCenterY = UNSET; + } + }); + } else { + mPeepHoleAnimator = null; + mRadius = 0; + mPeepHoleCenterX = UNSET; + mPeepHoleCenterY = UNSET; + } + } + + @Override + public void onAnimationCancel(Animator animation) { + + } + + @Override + public void onAnimationRepeat(Animator animation) { + + } + }); + mPeepHoleAnimator.start(); + } + + public void setAnimationEndAction(Runnable runnable) { + mEndAction = runnable; + } + } } diff --git a/src/com/android/camera/ui/ModeSelectorItem.java b/src/com/android/camera/ui/ModeSelectorItem.java index eb7433e5a..bf89f2b09 100644 --- a/src/com/android/camera/ui/ModeSelectorItem.java +++ b/src/com/android/camera/ui/ModeSelectorItem.java @@ -59,9 +59,15 @@ class ModeSelectorItem extends FrameLayout { private int mDrawingMode = TRUNCATE_TEXT_END; private int mHeight; private int mWidth; + private int mDefaultBackgroundColor; + private int mDefaultTextColor; + private int mIconBlockColor; + private final int mHighlightTextColor; public ModeSelectorItem(Context context, AttributeSet attrs) { super(context, attrs); + mHighlightTextColor = context.getResources() + .getColor(R.color.mode_selector_text_highlight_color); } @Override @@ -79,6 +85,12 @@ class ModeSelectorItem extends FrameLayout { mText.setTypeface(typeface); mMinVisibleWidth = getResources() .getDimensionPixelSize(R.dimen.mode_selector_icon_block_width); + mDefaultTextColor = mText.getCurrentTextColor(); + } + + public void setDefaultBackgroundColor(int color) { + mDefaultBackgroundColor = color; + setBackgroundColor(color); } @Override @@ -90,6 +102,16 @@ class ModeSelectorItem extends FrameLayout { new int[] {startColor, color}); } + public void highlight() { + mText.setTextColor(mHighlightTextColor); + setBackgroundColor(mIconBlockColor); + } + + public void unHighlight() { + setBackgroundColor(mDefaultBackgroundColor); + mText.setTextColor(mDefaultTextColor); + } + /** * When swiping in, we truncate the end of the item if the visible width * is not enough to show the whole item. When swiping out, we truncate the @@ -104,6 +126,7 @@ class ModeSelectorItem extends FrameLayout { } public void setIconBackgroundColor(int color) { + mIconBlockColor = color; mIcon.setBackgroundColor(color); } @@ -189,7 +212,7 @@ class ModeSelectorItem extends FrameLayout { canvas.clipRect(0, 0, width, height); } super.draw(canvas); - if (shadeStart > 0) { + if (shadeStart > 0 && width < mWidth) { mGradientShade.setBounds(shadeStart, 0, width, height); mGradientShade.draw(canvas); } diff --git a/src/com/android/camera/widget/AnimationEffects.java b/src/com/android/camera/widget/AnimationEffects.java new file mode 100644 index 000000000..8ed7e23b0 --- /dev/null +++ b/src/com/android/camera/widget/AnimationEffects.java @@ -0,0 +1,51 @@ +/* + * 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.widget; + +import android.graphics.Canvas; +import android.view.MotionEvent; + +/** + * This class aims to encapsulate animation internal states, so that different + * animations running on the same view are more independent. + */ +public abstract class AnimationEffects { + + public boolean onTouchEvent(MotionEvent ev) { + return false; + } + + public abstract void setSize(int width, int height); + + /** + * Draws what is needed for the animation in the background, before view calls + * super.draw(). + * + * @param canvas canvas that the animation effects will draw on + */ + public void drawBackground(Canvas canvas) {} + + /** + * Draws what is needed for the animation in the foreground, after view calls + * super.draw(). + * + * @param canvas canvas that the animation effects will draw on + */ + public abstract void drawForeground(Canvas canvas); + + public abstract void startAnimation(); +} |