blob: 98c0c2e6d57f052ecd868d4235379ea326d37891 [file] [log] [blame]
/*
* 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.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.SparseBooleanArray;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import com.android.camera.CaptureLayoutHelper;
import com.android.camera.app.CameraAppUI;
import com.android.camera.debug.Log;
import com.android.camera.util.CameraUtil;
import com.android.camera.util.Gusterpolator;
import com.android.camera.util.UsageStatistics;
import com.android.camera.widget.AnimationEffects;
import com.android.camera.widget.SettingsCling;
import com.android.camera2.R;
import com.google.common.logging.eventprotos;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* ModeListView class displays all camera modes and settings in the form
* of a list. A swipe to the right will bring up this list. Then tapping on
* any of the items in the list will take the user to that corresponding mode
* with an animation. To dismiss this list, simply swipe left or select a mode.
*/
public class ModeListView extends FrameLayout
implements ModeSelectorItem.VisibleWidthChangedListener,
PreviewStatusListener.PreviewAreaChangedListener {
private static final Log.Tag TAG = new Log.Tag("ModeListView");
// Animation Durations
private static final int DEFAULT_DURATION_MS = 200;
private static final int FLY_IN_DURATION_MS = 0;
private static final int HOLD_DURATION_MS = 0;
private static final int FLY_OUT_DURATION_MS = 850;
private static final int START_DELAY_MS = 100;
private static final int TOTAL_DURATION_MS = FLY_IN_DURATION_MS + HOLD_DURATION_MS
+ FLY_OUT_DURATION_MS;
private static final int HIDE_SHIMMY_DELAY_MS = 1000;
// Assumption for time since last scroll when no data point for last scroll.
private static final int SCROLL_INTERVAL_MS = 50;
// Last 20% percent of the drawer opening should be slow to ensure soft landing.
private static final float SLOW_ZONE_PERCENTAGE = 0.2f;
private static final int NO_ITEM_SELECTED = -1;
// Scrolling delay between non-focused item and focused item
private static final int DELAY_MS = 30;
// If the fling velocity exceeds this threshold, snap to full screen at a constant
// speed. Unit: pixel/ms.
private static final float VELOCITY_THRESHOLD = 2f;
/**
* A factor to change the UI responsiveness on a scroll.
* e.g. A scroll factor of 0.5 means UI will move half as fast as the finger.
*/
private static final float SCROLL_FACTOR = 0.5f;
// 60% opaque black background.
private static final int BACKGROUND_TRANSPARENTCY = (int) (0.6f * 255);
private static final int PREVIEW_DOWN_SAMPLE_FACTOR = 4;
// Threshold, below which snap back will happen.
private static final float SNAP_BACK_THRESHOLD_RATIO = 0.33f;
private final GestureDetector mGestureDetector;
private final CurrentStateManager mCurrentStateManager = new CurrentStateManager();
private final int mSettingsButtonMargin;
private long mLastScrollTime;
private int mListBackgroundColor;
private LinearLayout mListView;
private View mSettingsButton;
private int mTotalModes;
private ModeSelectorItem[] mModeSelectorItems;
private AnimatorSet mAnimatorSet;
private int mFocusItem = NO_ITEM_SELECTED;
private ModeListOpenListener mModeListOpenListener;
private ModeListVisibilityChangedListener mVisibilityChangedListener;
private CameraAppUI.CameraModuleScreenShotProvider mScreenShotProvider = null;
private int[] mInputPixels;
private int[] mOutputPixels;
private float mModeListOpenFactor = 1f;
private View mChildViewTouched = null;
private MotionEvent mLastChildTouchEvent = null;
private int mVisibleWidth = 0;
// Width and height of this view. They get updated in onLayout()
// Unit for width and height are pixels.
private int mWidth;
private int mHeight;
private float mScrollTrendX = 0f;
private float mScrollTrendY = 0f;
private ModeSwitchListener mModeSwitchListener = null;
private ArrayList<Integer> mSupportedModes;
private final LinkedList<TimeBasedPosition> mPositionHistory
= new LinkedList<TimeBasedPosition>();
private long mCurrentTime;
private float mVelocityX; // Unit: pixel/ms.
private long mLastDownTime = 0;
private CaptureLayoutHelper mCaptureLayoutHelper = null;
private SettingsCling mSettingsCling = null;
private class CurrentStateManager {
private ModeListState mCurrentState;
ModeListState getCurrentState() {
return mCurrentState;
}
void setCurrentState(ModeListState state) {
mCurrentState = state;
state.onCurrentState();
}
}
/**
* ModeListState defines a set of functions through which the view could manage
* or change the states. Sub-classes could selectively override these functions
* accordingly to respect the specific requirements for each state. By overriding
* these methods, state transition can also be achieved.
*/
private abstract class ModeListState implements GestureDetector.OnGestureListener {
protected AnimationEffects mCurrentAnimationEffects = null;
/**
* Called by the state manager when this state instance becomes the current
* mode list state.
*/
public void onCurrentState() {
// Do nothing.
showSettingsClingIfEnabled(false);
}
/**
* If supported, this should show the mode switcher and starts the accordion
* animation with a delay. If the view does not currently have focus, (e.g.
* There are popups on top of it.) start the delayed accordion animation
* when it gains focus. Otherwise, start the animation with a delay right
* away.
*/
public void showSwitcherHint() {
// Do nothing.
}
/**
* Gets the currently running animation effects for the current state.
*/
public AnimationEffects getCurrentAnimationEffects() {
return mCurrentAnimationEffects;
}
/**
* Returns true if the touch event should be handled, false otherwise.
*
* @param ev motion event to be handled
* @return true if the event should be handled, false otherwise.
*/
public boolean shouldHandleTouchEvent(MotionEvent ev) {
return true;
}
/**
* Handles touch event. This will be called if
* {@link ModeListState#shouldHandleTouchEvent(android.view.MotionEvent)}
* returns {@code true}
*
* @param ev touch event to be handled
* @return always true
*/
public boolean onTouchEvent(MotionEvent ev) {
return true;
}
/**
* Gets called when the window focus has changed.
*
* @param hasFocus whether current window has focus
*/
public void onWindowFocusChanged(boolean hasFocus) {
// Default to do nothing.
}
/**
* Gets called when back key is pressed.
*
* @return true if handled, false otherwise.
*/
public boolean onBackPressed() {
return false;
}
/**
* Gets called when menu key is pressed.
*
* @return true if handled, false otherwise.
*/
public boolean onMenuPressed() {
return false;
}
/**
* Gets called when there is a {@link View#setVisibility(int)} call to
* change the visibility of the mode drawer. Visibility change does not
* always make sense, for example there can be an outside call to make
* the mode drawer visible when it is in the fully hidden state. The logic
* is that the mode drawer can only be made visible when user swipe it in.
*
* @param visibility the proposed visibility change
* @return true if the visibility change is valid and therefore should be
* handled, false otherwise.
*/
public boolean shouldHandleVisibilityChange(int visibility) {
return true;
}
/**
* If supported, this should start blurring the camera preview and
* start the mode switch.
*
* @param selectedItem mode item that has been selected
*/
public void onItemSelected(ModeSelectorItem selectedItem) {
// Do nothing.
}
/**
* This gets called when mode switch has finished and UI needs to
* pinhole into the new mode through animation.
*/
public void startModeSelectionAnimation() {
// Do nothing.
}
/**
* Hide the mode drawer and switch to fully hidden state.
*/
public void hide() {
// Do nothing.
}
/**
* Hide the mode drawer (with animation, if supported)
* and switch to fully hidden state.
* Default is to simply call {@link #hide()}.
*/
public void hideAnimated() {
hide();
}
/***************GestureListener implementation*****************/
@Override
public boolean onDown(MotionEvent e) {
return false;
}
@Override
public void onShowPress(MotionEvent e) {
// Do nothing.
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return false;
}
@Override
public void onLongPress(MotionEvent e) {
// Do nothing.
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
}
/**
* Fully hidden state. Transitioning to ScrollingState and ShimmyState are supported
* in this state.
*/
private class FullyHiddenState extends ModeListState {
private Animator mAnimator = null;
private boolean mShouldBeVisible = false;
public FullyHiddenState() {
reset();
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
mShouldBeVisible = true;
// Change visibility, and switch to scrolling state.
resetModeSelectors();
mCurrentStateManager.setCurrentState(new ScrollingState());
return true;
}
@Override
public void showSwitcherHint() {
mShouldBeVisible = true;
mCurrentStateManager.setCurrentState(new ShimmyState());
}
@Override
public boolean shouldHandleTouchEvent(MotionEvent ev) {
return true;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
mFocusItem = getFocusItem(ev.getX(), ev.getY());
setSwipeMode(true);
}
return true;
}
@Override
public boolean onMenuPressed() {
if (mAnimator != null) {
return false;
}
snapOpenAndShow();
return true;
}
@Override
public boolean shouldHandleVisibilityChange(int visibility) {
if (mAnimator != null) {
return false;
}
if (visibility == VISIBLE && !mShouldBeVisible) {
return false;
}
return true;
}
/**
* Snaps open the mode list and go to the fully shown state.
*/
private void snapOpenAndShow() {
mShouldBeVisible = true;
setVisibility(VISIBLE);
mAnimator = snapToFullScreen();
if (mAnimator != null) {
mAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
mAnimator = null;
mCurrentStateManager.setCurrentState(new FullyShownState());
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
} else {
mCurrentStateManager.setCurrentState(new FullyShownState());
UsageStatistics.instance().controlUsed(
eventprotos.ControlEvent.ControlType.MENU_FULL_FROM_HIDDEN);
}
}
@Override
public void onCurrentState() {
super.onCurrentState();
announceForAccessibility(
getContext().getResources().getString(R.string.accessibility_mode_list_hidden));
}
}
/**
* Fully shown state. This state represents when the mode list is entirely shown
* on screen without any on-going animation. Transitions from this state could be
* to ScrollingState, SelectedState, or FullyHiddenState.
*/
private class FullyShownState extends ModeListState {
private Animator mAnimator = null;
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// Go to scrolling state.
if (distanceX > 0) {
// Swipe out
cancelForwardingTouchEvent();
mCurrentStateManager.setCurrentState(new ScrollingState());
}
return true;
}
@Override
public boolean shouldHandleTouchEvent(MotionEvent ev) {
if (mAnimator != null && mAnimator.isRunning()) {
return false;
}
return true;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
mFocusItem = NO_ITEM_SELECTED;
setSwipeMode(false);
// If the down event happens inside the mode list, find out which
// mode item is being touched and forward all the subsequent touch
// events to that mode item for its pressed state and click handling.
if (isTouchInsideList(ev)) {
mChildViewTouched = mModeSelectorItems[getFocusItem(ev.getX(), ev.getY())];
}
}
forwardTouchEventToChild(ev);
return true;
}
@Override
public boolean onSingleTapUp(MotionEvent ev) {
// If the tap is not inside the mode drawer area, snap back.
if(!isTouchInsideList(ev)) {
snapBackAndHide();
return false;
}
return true;
}
@Override
public boolean onBackPressed() {
snapBackAndHide();
return true;
}
@Override
public boolean onMenuPressed() {
snapBackAndHide();
return true;
}
@Override
public void onItemSelected(ModeSelectorItem selectedItem) {
mCurrentStateManager.setCurrentState(new SelectedState(selectedItem));
}
/**
* Snaps back the mode list and go to the fully hidden state.
*/
private void snapBackAndHide() {
mAnimator = snapBack(true);
if (mAnimator != null) {
mAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
mAnimator = null;
mCurrentStateManager.setCurrentState(new FullyHiddenState());
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
} else {
mCurrentStateManager.setCurrentState(new FullyHiddenState());
}
}
@Override
public void hide() {
if (mAnimator != null) {
mAnimator.cancel();
} else {
mCurrentStateManager.setCurrentState(new FullyHiddenState());
}
}
@Override
public void onCurrentState() {
announceForAccessibility(
getContext().getResources().getString(R.string.accessibility_mode_list_shown));
showSettingsClingIfEnabled(true);
}
}
/**
* Shimmy state handles the specifics for shimmy animation, including
* setting up to show mode drawer (without text) and hide it with shimmy animation.
*
* This state can be interrupted when scrolling or mode selection happened,
* in which case the state will transition into ScrollingState, or SelectedState.
* Otherwise, after shimmy finishes successfully, a transition to fully hidden
* state will happen.
*/
private class ShimmyState extends ModeListState {
private boolean mStartHidingShimmyWhenWindowGainsFocus = false;
private Animator mAnimator = null;
private final Runnable mHideShimmy = new Runnable() {
@Override
public void run() {
startHidingShimmy();
}
};
public ShimmyState() {
setVisibility(VISIBLE);
mSettingsButton.setVisibility(INVISIBLE);
mModeListOpenFactor = 0f;
onModeListOpenRatioUpdate(0);
int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
for (int i = 0; i < mModeSelectorItems.length; i++) {
mModeSelectorItems[i].setVisibleWidth(maxVisibleWidth);
}
if (hasWindowFocus()) {
hideShimmyWithDelay();
} else {
mStartHidingShimmyWhenWindowGainsFocus = true;
}
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// Scroll happens during accordion animation.
cancelAnimation();
cancelForwardingTouchEvent();
// Go to scrolling state
mCurrentStateManager.setCurrentState(new ScrollingState());
UsageStatistics.instance().controlUsed(
eventprotos.ControlEvent.ControlType.MENU_SCROLL_FROM_SHIMMY);
return true;
}
@Override
public boolean shouldHandleTouchEvent(MotionEvent ev) {
if (MotionEvent.ACTION_DOWN == ev.getActionMasked()) {
if (isTouchInsideList(ev) &&
ev.getX() <= mModeSelectorItems[0].getMaxVisibleWidth()) {
mChildViewTouched = mModeSelectorItems[getFocusItem(ev.getX(), ev.getY())];
return true;
}
// If shimmy is on-going, reject the first down event, so that it can be handled
// by the view underneath. If a swipe is detected, the same series of touch will
// re-enter this function, in which case we will consume the touch events.
if (mLastDownTime != ev.getDownTime()) {
mLastDownTime = ev.getDownTime();
return false;
}
}
return true;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (MotionEvent.ACTION_DOWN == ev.getActionMasked()) {
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
mFocusItem = getFocusItem(ev.getX(), ev.getY());
setSwipeMode(true);
}
}
forwardTouchEventToChild(ev);
return true;
}
@Override
public void onItemSelected(ModeSelectorItem selectedItem) {
cancelAnimation();
mCurrentStateManager.setCurrentState(new SelectedState(selectedItem));
}
private void hideShimmyWithDelay() {
postDelayed(mHideShimmy, HIDE_SHIMMY_DELAY_MS);
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
if (mStartHidingShimmyWhenWindowGainsFocus && hasFocus) {
mStartHidingShimmyWhenWindowGainsFocus = false;
hideShimmyWithDelay();
}
}
/**
* This starts the accordion animation, unless it's already running, in which
* case the start animation call will be ignored.
*/
private void startHidingShimmy() {
if (mAnimator != null) {
return;
}
int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
mAnimator = animateListToWidth(START_DELAY_MS * (-1), TOTAL_DURATION_MS,
Gusterpolator.INSTANCE, maxVisibleWidth, 0);
mAnimator.addListener(new Animator.AnimatorListener() {
private boolean mSuccess = true;
@Override
public void onAnimationStart(Animator animation) {
// Do nothing.
}
@Override
public void onAnimationEnd(Animator animation) {
mAnimator = null;
ShimmyState.this.onAnimationEnd(mSuccess);
}
@Override
public void onAnimationCancel(Animator animation) {
mSuccess = false;
}
@Override
public void onAnimationRepeat(Animator animation) {
// Do nothing.
}
});
}
/**
* Cancels the pending/on-going animation.
*/
private void cancelAnimation() {
removeCallbacks(mHideShimmy);
if (mAnimator != null && mAnimator.isRunning()) {
mAnimator.cancel();
} else {
mAnimator = null;
onAnimationEnd(false);
}
}
@Override
public void onCurrentState() {
super.onCurrentState();
ModeListView.this.disableA11yOnModeSelectorItems();
}
/**
* Gets called when the animation finishes or gets canceled.
*
* @param success indicates whether the animation finishes successfully
*/
private void onAnimationEnd(boolean success) {
mSettingsButton.setVisibility(VISIBLE);
// If successfully finish hiding shimmy, then we should go back to
// fully hidden state.
if (success) {
ModeListView.this.enableA11yOnModeSelectorItems();
mModeListOpenFactor = 1;
mCurrentStateManager.setCurrentState(new FullyHiddenState());
return;
}
// If the animation was canceled before it's finished, animate the mode
// list open factor from 0 to 1 to ensure a smooth visual transition.
final ValueAnimator openFactorAnimator = ValueAnimator.ofFloat(mModeListOpenFactor, 1f);
openFactorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mModeListOpenFactor = (Float) openFactorAnimator.getAnimatedValue();
onVisibleWidthChanged(mVisibleWidth);
}
});
openFactorAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
// Do nothing.
}
@Override
public void onAnimationEnd(Animator animation) {
mModeListOpenFactor = 1f;
}
@Override
public void onAnimationCancel(Animator animation) {
// Do nothing.
}
@Override
public void onAnimationRepeat(Animator animation) {
// Do nothing.
}
});
openFactorAnimator.start();
}
@Override
public void hide() {
cancelAnimation();
mCurrentStateManager.setCurrentState(new FullyHiddenState());
}
@Override
public void hideAnimated() {
cancelAnimation();
animateListToWidth(0).addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mCurrentStateManager.setCurrentState(new FullyHiddenState());
}
});
}
}
/**
* When the mode list is being scrolled, it will be in ScrollingState. From
* this state, the mode list could transition to fully hidden, fully open
* depending on which direction the scrolling goes.
*/
private class ScrollingState extends ModeListState {
private Animator mAnimator = null;
public ScrollingState() {
setVisibility(VISIBLE);
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// Scroll based on the scrolling distance on the currently focused
// item.
scroll(mFocusItem, distanceX * SCROLL_FACTOR,
distanceY * SCROLL_FACTOR);
return true;
}
@Override
public boolean shouldHandleTouchEvent(MotionEvent ev) {
// If the snap back/to full screen animation is on going, ignore any
// touch.
if (mAnimator != null) {
return false;
}
return true;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (ev.getActionMasked() == MotionEvent.ACTION_UP ||
ev.getActionMasked() == MotionEvent.ACTION_CANCEL) {
final boolean shouldSnapBack = shouldSnapBack();
if (shouldSnapBack) {
mAnimator = snapBack();
} else {
mAnimator = snapToFullScreen();
}
mAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
mAnimator = null;
mFocusItem = NO_ITEM_SELECTED;
if (shouldSnapBack) {
mCurrentStateManager.setCurrentState(new FullyHiddenState());
} else {
mCurrentStateManager.setCurrentState(new FullyShownState());
UsageStatistics.instance().controlUsed(
eventprotos.ControlEvent.ControlType.MENU_FULL_FROM_SCROLL);
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
return true;
}
}
/**
* Mode list gets in this state when a mode item has been selected/clicked.
* There will be an animation with the blurred preview fading in, a potential
* pause to wait for the new mode to be ready, and then the new mode will
* be revealed through a pinhole animation. After all the animations finish,
* mode list will transition into fully hidden state.
*/
private class SelectedState extends ModeListState {
public SelectedState(ModeSelectorItem selectedItem) {
final int modeId = selectedItem.getModeId();
// Un-highlight all the modes.
for (int i = 0; i < mModeSelectorItems.length; i++) {
mModeSelectorItems[i].setSelected(false);
}
PeepholeAnimationEffect effect = new PeepholeAnimationEffect();
effect.setSize(mWidth, mHeight);
// Calculate the position of the icon in the selected item, and
// start animation from that position.
int[] location = new int[2];
// Gets icon's center position in relative to the window.
selectedItem.getIconCenterLocationInWindow(location);
int iconX = location[0];
int iconY = location[1];
// Gets current view's top left position relative to the window.
getLocationInWindow(location);
// Calculate icon location relative to this view
iconX -= location[0];
iconY -= location[1];
effect.setAnimationStartingPosition(iconX, iconY);
effect.setModeSpecificColor(selectedItem.getHighlightColor());
if (mScreenShotProvider != null) {
effect.setBackground(mScreenShotProvider
.getPreviewFrame(PREVIEW_DOWN_SAMPLE_FACTOR),
mCaptureLayoutHelper.getPreviewRect());
effect.setBackgroundOverlay(mScreenShotProvider.getPreviewOverlayAndControls());
}
mCurrentAnimationEffects = effect;
effect.startFadeoutAnimation(null, selectedItem, iconX, iconY, modeId);
invalidate();
}
@Override
public boolean shouldHandleTouchEvent(MotionEvent ev) {
return false;
}
@Override
public void startModeSelectionAnimation() {
mCurrentAnimationEffects.startAnimation(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mCurrentAnimationEffects = null;
mCurrentStateManager.setCurrentState(new FullyHiddenState());
}
});
}
@Override
public void hide() {
if (!mCurrentAnimationEffects.cancelAnimation()) {
mCurrentAnimationEffects = null;
mCurrentStateManager.setCurrentState(new FullyHiddenState());
}
}
}
public interface ModeSwitchListener {
public void onModeSelected(int modeIndex);
public int getCurrentModeIndex();
public void onSettingsSelected();
}
public interface ModeListOpenListener {
/**
* Mode list will open to full screen after current animation.
*/
public void onOpenFullScreen();
/**
* Updates the listener with the current progress of mode drawer opening.
*
* @param progress progress of the mode drawer opening, ranging [0f, 1f]
* 0 means mode drawer is fully closed, 1 indicates a fully
* open mode drawer.
*/
public void onModeListOpenProgress(float progress);
/**
* Gets called when mode list is completely closed.
*/
public void onModeListClosed();
}
public static abstract class ModeListVisibilityChangedListener {
private Boolean mCurrentVisibility = null;
/** Whether the mode list is (partially or fully) visible. */
public abstract void onVisibilityChanged(boolean visible);
/**
* Internal method to be called by the mode list whenever a visibility
* even occurs.
* <p>
* Do not call {@link #onVisibilityChanged(boolean)} directly, as this
* is only called when the visibility has actually changed and not on
* each visibility event.
*
* @param visible whether the mode drawer is currently visible.
*/
private void onVisibilityEvent(boolean visible) {
if (mCurrentVisibility == null || mCurrentVisibility != visible) {
mCurrentVisibility = visible;
onVisibilityChanged(visible);
}
}
}
/**
* This class aims to help store time and position in pairs.
*/
private static class TimeBasedPosition {
private final float mPosition;
private final long mTimeStamp;
public TimeBasedPosition(float position, long time) {
mPosition = position;
mTimeStamp = time;
}
public float getPosition() {
return mPosition;
}
public long getTimeStamp() {
return mTimeStamp;
}
}
/**
* This is a highly customized interpolator. The purpose of having this subclass
* is to encapsulate intricate animation timing, so that the actual animation
* implementation can be re-used with other interpolators to achieve different
* animation effects.
*
* The accordion animation consists of three stages:
* 1) Animate into the screen within a pre-specified fly in duration.
* 2) Hold in place for a certain amount of time (Optional).
* 3) Animate out of the screen within the given time.
*
* The accordion animator is initialized with 3 parameter: 1) initial position,
* 2) how far out the view should be before flying back out, 3) end position.
* The interpolation output should be [0f, 0.5f] during animation between 1)
* to 2), and [0.5f, 1f] for flying from 2) to 3).
*/
private final TimeInterpolator mAccordionInterpolator = new TimeInterpolator() {
@Override
public float getInterpolation(float input) {
float flyInDuration = (float) FLY_OUT_DURATION_MS / (float) TOTAL_DURATION_MS;
float holdDuration = (float) (FLY_OUT_DURATION_MS + HOLD_DURATION_MS)
/ (float) TOTAL_DURATION_MS;
if (input == 0) {
return 0;
} else if (input < flyInDuration) {
// Stage 1, project result to [0f, 0.5f]
input /= flyInDuration;
float result = Gusterpolator.INSTANCE.getInterpolation(input);
return result * 0.5f;
} else if (input < holdDuration) {
// Stage 2
return 0.5f;
} else {
// Stage 3, project result to [0.5f, 1f]
input -= holdDuration;
input /= (1 - holdDuration);
float result = Gusterpolator.INSTANCE.getInterpolation(input);
return 0.5f + result * 0.5f;
}
}
};
/**
* The listener that is used to notify when gestures occur.
* Here we only listen to a subset of gestures.
*/
private final GestureDetector.OnGestureListener mOnGestureListener
= new GestureDetector.SimpleOnGestureListener(){
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
mCurrentStateManager.getCurrentState().onScroll(e1, e2, distanceX, distanceY);
mLastScrollTime = System.currentTimeMillis();
return true;
}
@Override
public boolean onSingleTapUp(MotionEvent ev) {
mCurrentStateManager.getCurrentState().onSingleTapUp(ev);
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
// Cache velocity in the unit pixel/ms.
mVelocityX = velocityX / 1000f * SCROLL_FACTOR;
mCurrentStateManager.getCurrentState().onFling(e1, e2, velocityX, velocityY);
return true;
}
@Override
public boolean onDown(MotionEvent ev) {
mVelocityX = 0;
mCurrentStateManager.getCurrentState().onDown(ev);
return true;
}
};
/**
* Gets called when a mode item in the mode drawer is clicked.
*
* @param selectedItem the item being clicked
*/
private void onItemSelected(ModeSelectorItem selectedItem) {
mCurrentStateManager.getCurrentState().onItemSelected(selectedItem);
}
/**
* Checks whether a touch event is inside of the bounds of the mode list.
*
* @param ev touch event to be checked
* @return whether the touch is inside the bounds of the mode list
*/
private boolean isTouchInsideList(MotionEvent ev) {
// Ignore the tap if it happens outside of the mode list linear layout.
float x = ev.getX() - mListView.getX();
float y = ev.getY() - mListView.getY();
if (x < 0 || x > mListView.getWidth() || y < 0 || y > mListView.getHeight()) {
return false;
}
return true;
}
public ModeListView(Context context, AttributeSet attrs) {
super(context, attrs);
mGestureDetector = new GestureDetector(context, mOnGestureListener);
mListBackgroundColor = getResources().getColor(R.color.mode_list_background);
mSettingsButtonMargin = getResources().getDimensionPixelSize(
R.dimen.mode_list_settings_icon_margin);
}
private void disableA11yOnModeSelectorItems() {
for (View selectorItem : mModeSelectorItems) {
selectorItem.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
}
}
private void enableA11yOnModeSelectorItems() {
for (View selectorItem : mModeSelectorItems) {
selectorItem.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
}
}
/**
* Sets the alpha on the list background. This is called whenever the list
* is scrolling or animating, so that background can adjust its dimness.
*
* @param alpha new alpha to be applied on list background color
*/
private void setBackgroundAlpha(int alpha) {
// Make sure alpha is valid.
alpha = alpha & 0xFF;
// Change alpha on the background color.
mListBackgroundColor = mListBackgroundColor & 0xFFFFFF;
mListBackgroundColor = mListBackgroundColor | (alpha << 24);
// Set new color to list background.
setBackgroundColor(mListBackgroundColor);
}
/**
* Initialize mode list with a list of indices of supported modes.
*
* @param modeIndexList a list of indices of supported modes
*/
public void init(List<Integer> modeIndexList) {
int[] modeSequence = getResources()
.getIntArray(R.array.camera_modes_in_nav_drawer_if_supported);
int[] visibleModes = getResources()
.getIntArray(R.array.camera_modes_always_visible);
// Mark the supported modes in a boolean array to preserve the
// sequence of the modes
SparseBooleanArray modeIsSupported = new SparseBooleanArray();
for (int i = 0; i < modeIndexList.size(); i++) {
int mode = modeIndexList.get(i);
modeIsSupported.put(mode, true);
}
for (int i = 0; i < visibleModes.length; i++) {
int mode = visibleModes[i];
modeIsSupported.put(mode, true);
}
// Put the indices of supported modes into an array preserving their
// display order.
mSupportedModes = new ArrayList<Integer>();
for (int i = 0; i < modeSequence.length; i++) {
int mode = modeSequence[i];
if (modeIsSupported.get(mode, false)) {
mSupportedModes.add(mode);
}
}
mTotalModes = mSupportedModes.size();
initializeModeSelectorItems();
mSettingsButton = findViewById(R.id.settings_button);
mSettingsButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// Post this callback to make sure current user interaction has
// been reflected in the UI. Specifically, the pressed state gets
// unset after click happens. In order to ensure the pressed state
// gets unset in UI before getting in the low frame rate settings
// activity launch stage, the settings selected callback is posted.
post(new Runnable() {
@Override
public void run() {
mModeSwitchListener.onSettingsSelected();
}
});
}
});
// The mode list is initialized to be all the way closed.
onModeListOpenRatioUpdate(0);
if (mCurrentStateManager.getCurrentState() == null) {
mCurrentStateManager.setCurrentState(new FullyHiddenState());
}
}
/**
* Sets the screen shot provider for getting a preview frame and a bitmap
* of the controls and overlay.
*/
public void setCameraModuleScreenShotProvider(
CameraAppUI.CameraModuleScreenShotProvider provider) {
mScreenShotProvider = provider;
}
private void initializeModeSelectorItems() {
mModeSelectorItems = new ModeSelectorItem[mTotalModes];
// Inflate the mode selector items and add them to a linear layout
LayoutInflater inflater = (LayoutInflater) getContext()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mListView = (LinearLayout) findViewById(R.id.mode_list);
for (int i = 0; i < mTotalModes; i++) {
final ModeSelectorItem selectorItem =
(ModeSelectorItem) inflater.inflate(R.layout.mode_selector, null);
mListView.addView(selectorItem);
// Sets the top padding of the top item to 0.
if (i == 0) {
selectorItem.setPadding(selectorItem.getPaddingLeft(), 0,
selectorItem.getPaddingRight(), selectorItem.getPaddingBottom());
}
// Sets the bottom padding of the bottom item to 0.
if (i == mTotalModes - 1) {
selectorItem.setPadding(selectorItem.getPaddingLeft(), selectorItem.getPaddingTop(),
selectorItem.getPaddingRight(), 0);
}
int modeId = getModeIndex(i);
selectorItem.setHighlightColor(getResources()
.getColor(CameraUtil.getCameraThemeColorId(modeId, getContext())));
// Set image
selectorItem.setImageResource(CameraUtil.getCameraModeIconResId(modeId, getContext()));
// Set text
selectorItem.setText(CameraUtil.getCameraModeText(modeId, getContext()));
// Set content description (for a11y)
selectorItem.setContentDescription(CameraUtil
.getCameraModeContentDescription(modeId, getContext()));
selectorItem.setModeId(modeId);
selectorItem.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
onItemSelected(selectorItem);
}
});
mModeSelectorItems[i] = selectorItem;
}
// During drawer opening/closing, we change the visible width of the mode
// items in sequence, so we listen to the last item's visible width change
// for a good timing to do corresponding UI adjustments.
mModeSelectorItems[mTotalModes - 1].setVisibleWidthChangedListener(this);
resetModeSelectors();
}
/**
* Maps between the UI mode selector index to the actual mode id.
*
* @param modeSelectorIndex the index of the UI item
* @return the index of the corresponding camera mode
*/
private int getModeIndex(int modeSelectorIndex) {
if (modeSelectorIndex < mTotalModes && modeSelectorIndex >= 0) {
return mSupportedModes.get(modeSelectorIndex);
}
Log.e(TAG, "Invalid mode selector index: " + modeSelectorIndex + ", total modes: " +
mTotalModes);
return getResources().getInteger(R.integer.camera_mode_photo);
}
/** Notify ModeSwitchListener, if any, of the mode change. */
private void onModeSelected(int modeIndex) {
if (mModeSwitchListener != null) {
mModeSwitchListener.onModeSelected(modeIndex);
}
}
/**
* Sets a listener that listens to receive mode switch event.
*
* @param listener a listener that gets notified when mode changes.
*/
public void setModeSwitchListener(ModeSwitchListener listener) {
mModeSwitchListener = listener;
}
/**
* Sets a listener that gets notified when the mode list is open full screen.
*
* @param listener a listener that listens to mode list open events
*/
public void setModeListOpenListener(ModeListOpenListener listener) {
mModeListOpenListener = listener;
}
/**
* Sets or replaces a listener that is called when the visibility of the
* mode list changed.
*/
public void setVisibilityChangedListener(ModeListVisibilityChangedListener listener) {
mVisibilityChangedListener = listener;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
// Reset touch forward recipient
if (MotionEvent.ACTION_DOWN == ev.getActionMasked()) {
mChildViewTouched = null;
}
if (!mCurrentStateManager.getCurrentState().shouldHandleTouchEvent(ev)) {
return false;
}
getParent().requestDisallowInterceptTouchEvent(true);
super.onTouchEvent(ev);
// Pass all touch events to gesture detector for gesture handling.
mGestureDetector.onTouchEvent(ev);
mCurrentStateManager.getCurrentState().onTouchEvent(ev);
return true;
}
/**
* Forward touch events to a recipient child view. Before feeding the motion
* event into the child view, the event needs to be converted in child view's
* coordinates.
*/
private void forwardTouchEventToChild(MotionEvent ev) {
if (mChildViewTouched != null) {
float x = ev.getX() - mListView.getX();
float y = ev.getY() - mListView.getY();
x -= mChildViewTouched.getLeft();
y -= mChildViewTouched.getTop();
mLastChildTouchEvent = MotionEvent.obtain(ev);
mLastChildTouchEvent.setLocation(x, y);
mChildViewTouched.onTouchEvent(mLastChildTouchEvent);
}
}
/**
* Sets the swipe mode to indicate whether this is a swiping in
* or out, and therefore we can have different animations.
*
* @param swipeIn indicates whether the swipe should reveal/hide the list.
*/
private void setSwipeMode(boolean swipeIn) {
for (int i = 0 ; i < mModeSelectorItems.length; i++) {
mModeSelectorItems[i].onSwipeModeChanged(swipeIn);
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mWidth = right - left;
mHeight = bottom - top - getPaddingTop() - getPaddingBottom();
updateModeListLayout();
if (mCurrentStateManager.getCurrentState().getCurrentAnimationEffects() != null) {
mCurrentStateManager.getCurrentState().getCurrentAnimationEffects().setSize(
mWidth, mHeight);
}
}
/**
* Sets a capture layout helper to query layout rect from.
*/
public void setCaptureLayoutHelper(CaptureLayoutHelper helper) {
mCaptureLayoutHelper = helper;
}
@Override
public void onPreviewAreaChanged(RectF previewArea) {
if (getVisibility() == View.VISIBLE && !hasWindowFocus()) {
// When the preview area has changed, to avoid visual disruption we
// only make corresponding UI changes when mode list does not have
// window focus.
updateModeListLayout();
}
}
private void updateModeListLayout() {
if (mCaptureLayoutHelper == null) {
Log.e(TAG, "Capture layout helper needs to be set first.");
return;
}
// Center mode drawer in the portion of camera preview that is not covered by
// bottom bar.
RectF uncoveredPreviewArea = mCaptureLayoutHelper.getUncoveredPreviewRect();
// Align left:
mListView.setTranslationX(uncoveredPreviewArea.left);
// Align center vertical:
mListView.setTranslationY(uncoveredPreviewArea.centerY()
- mListView.getMeasuredHeight() / 2);
updateSettingsButtonLayout(uncoveredPreviewArea);
}
private void updateSettingsButtonLayout(RectF uncoveredPreviewArea) {
if (mWidth > mHeight) {
// Align to the top right.
mSettingsButton.setTranslationX(uncoveredPreviewArea.right - mSettingsButtonMargin
- mSettingsButton.getMeasuredWidth());
mSettingsButton.setTranslationY(uncoveredPreviewArea.top + mSettingsButtonMargin);
} else {
// Align to the bottom right.
mSettingsButton.setTranslationX(uncoveredPreviewArea.right - mSettingsButtonMargin
- mSettingsButton.getMeasuredWidth());
mSettingsButton.setTranslationY(uncoveredPreviewArea.bottom - mSettingsButtonMargin
- mSettingsButton.getMeasuredHeight());
}
if (mSettingsCling != null) {
mSettingsCling.updatePosition(mSettingsButton);
}
}
@Override
public void draw(Canvas canvas) {
ModeListState currentState = mCurrentStateManager.getCurrentState();
AnimationEffects currentEffects = currentState.getCurrentAnimationEffects();
if (currentEffects != null) {
currentEffects.drawBackground(canvas);
if (currentEffects.shouldDrawSuper()) {
super.draw(canvas);
}
currentEffects.drawForeground(canvas);
} else {
super.draw(canvas);
}
}
/**
* Sets whether a cling for settings button should be shown. If not, remove
* the cling from view hierarchy if any. If a cling should be shown, inflate
* the cling into this view group.
*
* @param show whether the cling needs to be shown.
*/
public void setShouldShowSettingsCling(boolean show) {
if (show) {
if (mSettingsCling == null) {
inflate(getContext(), R.layout.settings_cling, this);
mSettingsCling = (SettingsCling) findViewById(R.id.settings_cling);
}
} else {
if (mSettingsCling != null) {
// Remove settings cling from view hierarchy.
removeView(mSettingsCling);
mSettingsCling = null;
}
}
}
/**
* Show or hide cling for settings button. The cling will only be shown if
* settings button has never been clicked. Otherwise, cling will be null,
* and will not show even if this method is called to show it.
*/
private void showSettingsClingIfEnabled(boolean show) {
if (mSettingsCling != null) {
int visibility = show ? VISIBLE : INVISIBLE;
mSettingsCling.setVisibility(visibility);
}
}
/**
* This shows the mode switcher and starts the accordion animation with a delay.
* If the view does not currently have focus, (e.g. There are popups on top of
* it.) start the delayed accordion animation when it gains focus. Otherwise,
* start the animation with a delay right away.
*/
public void showModeSwitcherHint() {
mCurrentStateManager.getCurrentState().showSwitcherHint();
}
/**
* Hide the mode list immediately (provided the current state allows it).
*/
public void hide() {
mCurrentStateManager.getCurrentState().hide();
}
/**
* Hide the mode list with an animation.
*/
public void hideAnimated() {
mCurrentStateManager.getCurrentState().hideAnimated();
}
/**
* Resets the visible width of all the mode selectors to 0.
*/
private void resetModeSelectors() {
for (int i = 0; i < mModeSelectorItems.length; i++) {
mModeSelectorItems[i].setVisibleWidth(0);
}
}
private boolean isRunningAccordionAnimation() {
return mAnimatorSet != null && mAnimatorSet.isRunning();
}
/**
* Calculate the mode selector item in the list that is at position (x, y).
* If the position is above the top item or below the bottom item, return
* the top item or bottom item respectively.
*
* @param x horizontal position
* @param y vertical position
* @return index of the item that is at position (x, y)
*/
private int getFocusItem(float x, float y) {
// Convert coordinates into child view's coordinates.
x -= mListView.getX();
y -= mListView.getY();
for (int i = 0; i < mModeSelectorItems.length; i++) {
if (y <= mModeSelectorItems[i].getBottom()) {
return i;
}
}
return mModeSelectorItems.length - 1;
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
mCurrentStateManager.getCurrentState().onWindowFocusChanged(hasFocus);
}
@Override
public void onVisibilityChanged(View v, int visibility) {
super.onVisibilityChanged(v, visibility);
if (visibility == VISIBLE) {
// Highlight current module
if (mModeSwitchListener != null) {
int modeId = mModeSwitchListener.getCurrentModeIndex();
int parentMode = CameraUtil.getCameraModeParentModeId(modeId, getContext());
// Find parent mode in the nav drawer.
for (int i = 0; i < mSupportedModes.size(); i++) {
if (mSupportedModes.get(i) == parentMode) {
mModeSelectorItems[i].setSelected(true);
}
}
}
updateModeListLayout();
} else {
if (mModeSelectorItems != null) {
// When becoming invisible/gone after initializing mode selector items.
for (int i = 0; i < mModeSelectorItems.length; i++) {
mModeSelectorItems[i].setSelected(false);
}
}
if (mModeListOpenListener != null) {
mModeListOpenListener.onModeListClosed();
}
}
if (mVisibilityChangedListener != null) {
mVisibilityChangedListener.onVisibilityEvent(getVisibility() == VISIBLE);
}
}
@Override
public void setVisibility(int visibility) {
ModeListState currentState = mCurrentStateManager.getCurrentState();
if (currentState != null && !currentState.shouldHandleVisibilityChange(visibility)) {
return;
}
super.setVisibility(visibility);
}
private void scroll(int itemId, float deltaX, float deltaY) {
// Scrolling trend on X and Y axis, to track the trend by biasing
// towards latest touch events.
mScrollTrendX = mScrollTrendX * 0.3f + deltaX * 0.7f;
mScrollTrendY = mScrollTrendY * 0.3f + deltaY * 0.7f;
// TODO: Change how the curve is calculated below when UX finalize their design.
mCurrentTime = SystemClock.uptimeMillis();
float longestWidth;
if (itemId != NO_ITEM_SELECTED) {
longestWidth = mModeSelectorItems[itemId].getVisibleWidth();
} else {
longestWidth = mModeSelectorItems[0].getVisibleWidth();
}
float newPosition = longestWidth - deltaX;
int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
newPosition = Math.min(newPosition, getMaxMovementBasedOnPosition((int) longestWidth,
maxVisibleWidth));
newPosition = Math.max(newPosition, 0);
insertNewPosition(newPosition, mCurrentTime);
for (int i = 0; i < mModeSelectorItems.length; i++) {
mModeSelectorItems[i].setVisibleWidth(calculateVisibleWidthForItem(i,
(int) newPosition));
}
}
/**
* Calculate the width of a specified item based on its position relative to
* the item with longest width.
*/
private int calculateVisibleWidthForItem(int itemId, int longestWidth) {
if (itemId == mFocusItem || mFocusItem == NO_ITEM_SELECTED) {
return longestWidth;
}
int delay = Math.abs(itemId - mFocusItem) * DELAY_MS;
return (int) getPosition(mCurrentTime - delay,
mModeSelectorItems[itemId].getVisibleWidth());
}
/**
* Insert new position and time stamp into the history position list, and
* remove stale position items.
*
* @param position latest position of the focus item
* @param time current time in milliseconds
*/
private void insertNewPosition(float position, long time) {
// TODO: Consider re-using stale position objects rather than
// always creating new position objects.
mPositionHistory.add(new TimeBasedPosition(position, time));
// Positions that are from too long ago will not be of any use for
// future position interpolation. So we need to remove those positions
// from the list.
long timeCutoff = time - (mTotalModes - 1) * DELAY_MS;
while (mPositionHistory.size() > 0) {
// Remove all the position items that are prior to the cutoff time.
TimeBasedPosition historyPosition = mPositionHistory.getFirst();
if (historyPosition.getTimeStamp() < timeCutoff) {
mPositionHistory.removeFirst();
} else {
break;
}
}
}
/**
* Gets the interpolated position at the specified time. This involves going
* through the recorded positions until a {@link TimeBasedPosition} is found
* such that the position the recorded before the given time, and the
* {@link TimeBasedPosition} after that is recorded no earlier than the given
* time. These two positions are then interpolated to get the position at the
* specified time.
*/
private float getPosition(long time, float currentPosition) {
int i;
for (i = 0; i < mPositionHistory.size(); i++) {
TimeBasedPosition historyPosition = mPositionHistory.get(i);
if (historyPosition.getTimeStamp() > time) {
// Found the winner. Now interpolate between position i and position i - 1
if (i == 0) {
// Slowly approaching to the destination if there isn't enough data points
float weight = 0.2f;
return historyPosition.getPosition() * weight + (1f - weight) * currentPosition;
} else {
TimeBasedPosition prevTimeBasedPosition = mPositionHistory.get(i - 1);
// Start interpolation
float fraction = (float) (time - prevTimeBasedPosition.getTimeStamp()) /
(float) (historyPosition.getTimeStamp() - prevTimeBasedPosition.getTimeStamp());
float position = fraction * (historyPosition.getPosition()
- prevTimeBasedPosition.getPosition()) + prevTimeBasedPosition.getPosition();
return position;
}
}
}
// It should never get here.
Log.e(TAG, "Invalid time input for getPosition(). time: " + time);
if (mPositionHistory.size() == 0) {
Log.e(TAG, "TimeBasedPosition history size is 0");
} else {
Log.e(TAG, "First position recorded at " + mPositionHistory.getFirst().getTimeStamp()
+ " , last position recorded at " + mPositionHistory.getLast().getTimeStamp());
}
assert (i < mPositionHistory.size());
return i;
}
private void reset() {
resetModeSelectors();
mScrollTrendX = 0f;
mScrollTrendY = 0f;
setVisibility(INVISIBLE);
}
/**
* When visible width of list is changed, the background of the list needs
* to darken/lighten correspondingly.
*/
@Override
public void onVisibleWidthChanged(int visibleWidth) {
mVisibleWidth = visibleWidth;
// When the longest mode item is entirely shown (across the screen), the
// background should be 50% transparent.
int maxVisibleWidth = mModeSelectorItems[0].getMaxVisibleWidth();
visibleWidth = Math.min(maxVisibleWidth, visibleWidth);
if (visibleWidth != maxVisibleWidth) {
// No longer full screen.
cancelForwardingTouchEvent();
}
float openRatio = (float) visibleWidth / maxVisibleWidth;
onModeListOpenRatioUpdate(openRatio * mModeListOpenFactor);
}
/**
* Gets called when UI elements such as background and gear icon need to adjust
* their appearance based on the percentage of the mode list opening.
*
* @param openRatio percentage of the mode list opening, ranging [0f, 1f]
*/
private void onModeListOpenRatioUpdate(float openRatio) {
for (int i = 0; i < mModeSelectorItems.length; i++) {
mModeSelectorItems[i].setTextAlpha(openRatio);
}
setBackgroundAlpha((int) (BACKGROUND_TRANSPARENTCY * openRatio));
if (mModeListOpenListener != null) {
mModeListOpenListener.onModeListOpenProgress(openRatio);
}
if (mSettingsButton != null) {
mSettingsButton.setAlpha(openRatio);
}
}
/**
* Cancels the touch event forwarding by sending a cancel event to the recipient
* view and resetting the touch forward recipient to ensure no more events
* can be forwarded in the current series of the touch events.
*/
private void cancelForwardingTouchEvent() {
if (mChildViewTouched != null) {
mLastChildTouchEvent.setAction(MotionEvent.ACTION_CANCEL);
mChildViewTouched.onTouchEvent(mLastChildTouchEvent);
mChildViewTouched = null;
}
}
@Override
public void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
if (visibility != VISIBLE) {
mCurrentStateManager.getCurrentState().hide();
}
}
/**
* Defines how the list view should respond to a menu button pressed
* event.
*/
public boolean onMenuPressed() {
return mCurrentStateManager.getCurrentState().onMenuPressed();
}
/**
* 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
* on the current position of the list and the gesture we can decide which way
* to snap.
*/
private void snap() {
if (shouldSnapBack()) {
snapBack();
} else {
snapToFullScreen();
}
}
private boolean shouldSnapBack() {
int itemId = Math.max(0, mFocusItem);
if (Math.abs(mVelocityX) > VELOCITY_THRESHOLD) {
// Fling to open / close
return mVelocityX < 0;
} else if (mModeSelectorItems[itemId].getVisibleWidth()
< mModeSelectorItems[itemId].getMaxVisibleWidth() * SNAP_BACK_THRESHOLD_RATIO) {
return true;
} else if (Math.abs(mScrollTrendX) > Math.abs(mScrollTrendY) && mScrollTrendX > 0) {
return true;
} else {
return false;
}
}
/**
* Snaps back out of the screen.
*
* @param withAnimation whether snapping back should be animated
*/
public Animator snapBack(boolean withAnimation) {
if (withAnimation) {
if (mVelocityX > -VELOCITY_THRESHOLD * SCROLL_FACTOR) {
return animateListToWidth(0);
} else {
return animateListToWidthAtVelocity(mVelocityX, 0);
}
} else {
setVisibility(INVISIBLE);
resetModeSelectors();
return null;
}
}
/**
* Snaps the mode list back out with animation.
*/
private Animator snapBack() {
return snapBack(true);
}
private Animator snapToFullScreen() {
Animator animator;
int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem;
int fullWidth = mModeSelectorItems[focusItem].getMaxVisibleWidth();
if (mVelocityX <= VELOCITY_THRESHOLD) {
animator = animateListToWidth(fullWidth);
} else {
// If the fling velocity exceeds this threshold, snap to full screen
// at a constant speed.
animator = animateListToWidthAtVelocity(VELOCITY_THRESHOLD, fullWidth);
}
if (mModeListOpenListener != null) {
mModeListOpenListener.onOpenFullScreen();
}
return animator;
}
/**
* Overloaded function to provide a simple way to start animation. Animation
* will use default duration, and a value of <code>null</code> for interpolator
* means linear interpolation will be used.
*
* @param width a set of values that the animation will animate between over time
*/
private Animator animateListToWidth(int... width) {
return animateListToWidth(0, DEFAULT_DURATION_MS, null, width);
}
/**
* Animate the mode list between the given set of visible width.
*
* @param delay start delay between consecutive mode item. If delay < 0, the
* leader in the animation will be the bottom item.
* @param duration duration for the animation of each mode item
* @param interpolator interpolator to be used by the animation
* @param width a set of values that the animation will animate between over time
*/
private Animator animateListToWidth(int delay, int duration,
TimeInterpolator interpolator, int... width) {
if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
mAnimatorSet.end();
}
ArrayList<Animator> animators = new ArrayList<Animator>();
boolean animateModeItemsInOrder = true;
if (delay < 0) {
animateModeItemsInOrder = false;
delay *= -1;
}
for (int i = 0; i < mTotalModes; i++) {
ObjectAnimator animator;
if (animateModeItemsInOrder) {
animator = ObjectAnimator.ofInt(mModeSelectorItems[i],
"visibleWidth", width);
} else {
animator = ObjectAnimator.ofInt(mModeSelectorItems[mTotalModes - 1 -i],
"visibleWidth", width);
}
animator.setDuration(duration);
animator.setStartDelay(i * delay);
animators.add(animator);
}
mAnimatorSet = new AnimatorSet();
mAnimatorSet.playTogether(animators);
mAnimatorSet.setInterpolator(interpolator);
mAnimatorSet.start();
return mAnimatorSet;
}
/**
* Animate the mode list to the given width at a constant velocity.
*
* @param velocity the velocity that animation will be at
* @param width final width of the list
*/
private Animator animateListToWidthAtVelocity(float velocity, int width) {
if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
mAnimatorSet.end();
}
ArrayList<Animator> animators = new ArrayList<Animator>();
int focusItem = mFocusItem == NO_ITEM_SELECTED ? 0 : mFocusItem;
for (int i = 0; i < mTotalModes; i++) {
ObjectAnimator animator = ObjectAnimator.ofInt(mModeSelectorItems[i],
"visibleWidth", width);
int duration = (int) (width / velocity);
animator.setDuration(duration);
animators.add(animator);
}
mAnimatorSet = new AnimatorSet();
mAnimatorSet.playTogether(animators);
mAnimatorSet.setInterpolator(null);
mAnimatorSet.start();
return mAnimatorSet;
}
/**
* Called when the back key is pressed.
*
* @return Whether the UI responded to the key event.
*/
public boolean onBackPressed() {
return mCurrentStateManager.getCurrentState().onBackPressed();
}
public void startModeSelectionAnimation() {
mCurrentStateManager.getCurrentState().startModeSelectionAnimation();
}
public float getMaxMovementBasedOnPosition(int lastVisibleWidth, int maxWidth) {
int timeElapsed = (int) (System.currentTimeMillis() - mLastScrollTime);
if (timeElapsed > SCROLL_INTERVAL_MS) {
timeElapsed = SCROLL_INTERVAL_MS;
}
float position;
int slowZone = (int) (maxWidth * SLOW_ZONE_PERCENTAGE);
if (lastVisibleWidth < (maxWidth - slowZone)) {
position = VELOCITY_THRESHOLD * timeElapsed + lastVisibleWidth;
} else {
float percentageIntoSlowZone = (lastVisibleWidth - (maxWidth - slowZone)) / slowZone;
float velocity = (1 - percentageIntoSlowZone) * VELOCITY_THRESHOLD;
position = velocity * timeElapsed + lastVisibleWidth;
}
position = Math.min(maxWidth, position);
return position;
}
private class PeepholeAnimationEffect extends AnimationEffects {
private final static int UNSET = -1;
private final static int PEEP_HOLE_ANIMATION_DURATION_MS = 500;
private final Paint mMaskPaint = new Paint();
private final RectF mBackgroundDrawArea = new RectF();
private int mPeepHoleCenterX = UNSET;
private int mPeepHoleCenterY = UNSET;
private float mRadius = 0f;
private ValueAnimator mPeepHoleAnimator;
private ValueAnimator mFadeOutAlphaAnimator;
private ValueAnimator mRevealAlphaAnimator;
private Bitmap mBackground;
private Bitmap mBackgroundOverlay;
private Paint mCirclePaint = new Paint();
private Paint mCoverPaint = new Paint();
private TouchCircleDrawable mCircleDrawable;
public PeepholeAnimationEffect() {
mMaskPaint.setAlpha(0);
mMaskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
mCirclePaint.setColor(0);
mCirclePaint.setAlpha(0);
mCoverPaint.setColor(0);
mCoverPaint.setAlpha(0);
setupAnimators();
}
private void setupAnimators() {
mFadeOutAlphaAnimator = ValueAnimator.ofInt(0, 255);
mFadeOutAlphaAnimator.setDuration(100);
mFadeOutAlphaAnimator.setInterpolator(Gusterpolator.INSTANCE);
mFadeOutAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCoverPaint.setAlpha((Integer) animation.getAnimatedValue());
invalidate();
}
});
mFadeOutAlphaAnimator.addListener(new AnimatorListenerAdapter() {
@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);
}
});
/////////////////
mRevealAlphaAnimator = ValueAnimator.ofInt(255, 0);
mRevealAlphaAnimator.setDuration(PEEP_HOLE_ANIMATION_DURATION_MS);
mRevealAlphaAnimator.setInterpolator(Gusterpolator.INSTANCE);
mRevealAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int alpha = (Integer) animation.getAnimatedValue();
mCirclePaint.setAlpha(alpha);
mCoverPaint.setAlpha(alpha);
}
});
mRevealAlphaAnimator.addListener(new AnimatorListenerAdapter() {
@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);
}
});
////////////////
int horizontalDistanceToFarEdge = Math.max(mPeepHoleCenterX, mWidth - mPeepHoleCenterX);
int verticalDistanceToFarEdge = Math.max(mPeepHoleCenterY, mHeight - mPeepHoleCenterY);
int endRadius = (int) (Math.sqrt(horizontalDistanceToFarEdge * horizontalDistanceToFarEdge
+ verticalDistanceToFarEdge * verticalDistanceToFarEdge));
int startRadius = getResources().getDimensionPixelSize(
R.dimen.mode_selector_icon_block_width) / 2;
mPeepHoleAnimator = ValueAnimator.ofFloat(startRadius, 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 AnimatorListenerAdapter() {
@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);
}
});
////////////////
int size = getContext().getResources()
.getDimensionPixelSize(R.dimen.mode_selector_icon_block_width);
mCircleDrawable = new TouchCircleDrawable(getContext().getResources());
mCircleDrawable.setSize(size, size);
mCircleDrawable.setUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
invalidate();
}
});
}
@Override
public void setSize(int width, int height) {
mWidth = width;
mHeight = height;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return true;
}
@Override
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);
canvas.drawCircle(mPeepHoleCenterX, mPeepHoleCenterY, mRadius, mCirclePaint);
}
}
public void setAnimationStartingPosition(int x, int y) {
mPeepHoleCenterX = x;
mPeepHoleCenterY = y;
}
public void setModeSpecificColor(int color) {
mCirclePaint.setColor(color & 0x00ffffff);
}
/**
* Sets the bitmap to be drawn in the background and the drawArea to draw
* the bitmap.
*
* @param background image to be drawn in the background
* @param drawArea area to draw the background image
*/
public void setBackground(Bitmap background, RectF drawArea) {
mBackground = background;
mBackgroundDrawArea.set(drawArea);
}
/**
* Sets the overlay image to be drawn on top of the background.
*/
public void setBackgroundOverlay(Bitmap overlay) {
mBackgroundOverlay = overlay;
}
@Override
public void drawBackground(Canvas canvas) {
if (mBackground != null && mBackgroundOverlay != null) {
canvas.drawBitmap(mBackground, null, mBackgroundDrawArea, null);
canvas.drawPaint(mCoverPaint);
canvas.drawBitmap(mBackgroundOverlay, 0, 0, null);
if (mCircleDrawable != null) {
mCircleDrawable.draw(canvas);
}
}
}
@Override
public boolean shouldDrawSuper() {
// No need to draw super when mBackgroundOverlay is being drawn, as
// background overlay already contains what's drawn in super.
return (mBackground == null || mBackgroundOverlay == null);
}
public void startFadeoutAnimation(Animator.AnimatorListener listener,
final ModeSelectorItem selectedItem,
int x, int y, final int modeId) {
mCoverPaint.setColor(0);
mCoverPaint.setAlpha(0);
mCircleDrawable.setIconDrawable(
selectedItem.getIcon().getIconDrawableClone(),
selectedItem.getIcon().getIconDrawableSize());
mCircleDrawable.setCenter(new Point(x, y));
mCircleDrawable.setColor(selectedItem.getHighlightColor());
mCircleDrawable.setAnimatorListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// Post mode selection runnable to the end of the message queue
// so that current UI changes can finish before mode initialization
// clogs up UI thread.
post(new Runnable() {
@Override
public void run() {
// Select the focused item.
selectedItem.setSelected(true);
onModeSelected(modeId);
}
});
}
});
// add fade out animator to a set, so we can freely add
// the listener without having to worry about listener dupes
AnimatorSet s = new AnimatorSet();
s.play(mFadeOutAlphaAnimator);
if (listener != null) {
s.addListener(listener);
}
mCircleDrawable.animate();
s.start();
}
@Override
public void startAnimation(Animator.AnimatorListener listener) {
if (mPeepHoleAnimator != null && mPeepHoleAnimator.isRunning()) {
return;
}
if (mPeepHoleCenterY == UNSET || mPeepHoleCenterX == UNSET) {
mPeepHoleCenterX = mWidth / 2;
mPeepHoleCenterY = mHeight / 2;
}
mCirclePaint.setAlpha(255);
mCoverPaint.setAlpha(255);
// add peephole and reveal animators to a set, so we can
// freely add the listener without having to worry about
// listener dupes
AnimatorSet s = new AnimatorSet();
s.play(mPeepHoleAnimator).with(mRevealAlphaAnimator);
if (listener != null) {
s.addListener(listener);
}
s.start();
}
@Override
public void endAnimation() {
}
@Override
public boolean cancelAnimation() {
if (mPeepHoleAnimator == null || !mPeepHoleAnimator.isRunning()) {
return false;
} else {
mPeepHoleAnimator.cancel();
return true;
}
}
}
}