| /* |
| * 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.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; |
| import android.util.SparseArray; |
| import android.view.GestureDetector; |
| import android.view.LayoutInflater; |
| import android.view.MotionEvent; |
| import android.widget.LinearLayout; |
| import android.widget.ScrollView; |
| |
| import com.android.camera.util.CameraUtil; |
| import com.android.camera.util.Gusterpolator; |
| import com.android.camera.widget.AnimationEffects; |
| import com.android.camera2.R; |
| |
| 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 ScrollView { |
| |
| private static final String TAG = "ModeListView"; |
| |
| // Animation Durations |
| private static final int DEFAULT_DURATION_MS = 200; |
| private static final int FLY_IN_DURATION_MS = 850; |
| 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 float ROWS_TO_SHOW_IN_LANDSCAPE = 4.5f; |
| private static final int NO_ITEM_SELECTED = -1; |
| |
| // Scrolling states |
| private static final int IDLE = 0; |
| 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; |
| // 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; |
| |
| private final GestureDetector mGestureDetector; |
| private final int mIconBlockWidth; |
| |
| private int mListBackgroundColor; |
| private LinearLayout mListView; |
| private int mState = IDLE; |
| private int mTotalModes; |
| private ModeSelectorItem[] mModeSelectorItems; |
| private AnimatorSet mAnimatorSet; |
| private int mFocusItem = NO_ITEM_SELECTED; |
| private AnimationEffects mCurrentEffect; |
| private ModeListOpenListener mModeListOpenListener; |
| |
| // 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 final Animator.AnimatorListener mModeListAnimatorListener = |
| new Animator.AnimatorListener() { |
| |
| @Override |
| public void onAnimationStart(Animator animation) { |
| setVisibility(VISIBLE); |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mAnimatorSet = null; |
| if (mState == ACCORDION_ANIMATION || mState == IDLE) { |
| resetModeSelectors(); |
| setVisibility(INVISIBLE); |
| mState = IDLE; |
| } |
| } |
| |
| @Override |
| public void onAnimationCancel(Animator animation) { |
| } |
| |
| @Override |
| public void onAnimationRepeat(Animator animation) { |
| |
| } |
| }; |
| |
| public interface ModeSwitchListener { |
| public void onModeSelected(int modeIndex); |
| } |
| |
| public interface ModeListOpenListener { |
| public void onOpenFullScreen(); |
| } |
| |
| /** |
| * 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) { |
| |
| if (mState == ACCORDION_ANIMATION) { |
| // Scroll happens during accordion animation. |
| if (isRunningAccordionAnimation()) { |
| mAnimatorSet.cancel(); |
| } |
| setVisibility(VISIBLE); |
| } |
| |
| if (mState == IDLE) { |
| resetModeSelectors(); |
| setVisibility(VISIBLE); |
| } |
| |
| mState = SCROLLING; |
| // Scroll based on the scrolling distance on the currently focused |
| // item. |
| scroll(mFocusItem, distanceX, distanceY); |
| return true; |
| } |
| |
| @Override |
| public boolean onSingleTapUp(MotionEvent ev) { |
| if (mState != FULLY_SHOWN) { |
| // Only allows tap to choose mode when the list is fully shown |
| return false; |
| } |
| int index = getFocusItem(ev.getX(), ev.getY()); |
| // Validate the selection |
| if (index != NO_ITEM_SELECTED) { |
| final 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; |
| |
| // 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() { |
| onModeSelected(modeId); |
| } |
| }); |
| } |
| return true; |
| } |
| |
| @Override |
| public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { |
| // Cache velocity in the unit pixel/ms. |
| mVelocityX = velocityX / 1000f; |
| return true; |
| } |
| }; |
| |
| public ModeListView(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| mGestureDetector = new GestureDetector(context, mOnGestureListener); |
| mIconBlockWidth = getResources() |
| .getDimensionPixelSize(R.dimen.mode_selector_icon_block_width); |
| mListBackgroundColor = getResources().getColor(R.color.mode_list_background); |
| } |
| |
| /** |
| * 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. |
| mListView.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 |
| SparseArray<Boolean> modeIsSupported = new SparseArray<Boolean>(); |
| 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(); |
| } |
| |
| 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++) { |
| ModeSelectorItem selectorItem = |
| (ModeSelectorItem) inflater.inflate(R.layout.mode_selector, null); |
| mListView.addView(selectorItem); |
| // Set alternating background color for each mode selector in the list |
| if (i % 2 == 0) { |
| selectorItem.setDefaultBackgroundColor(getResources() |
| .getColor(R.color.mode_selector_background_light)); |
| } else { |
| selectorItem.setDefaultBackgroundColor(getResources() |
| .getColor(R.color.mode_selector_background_dark)); |
| } |
| int modeId = getModeIndex(i); |
| selectorItem.setIconBackgroundColor(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())); |
| |
| mModeSelectorItems[i] = selectorItem; |
| } |
| |
| 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; |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent ev) { |
| if (mCurrentEffect != null) { |
| return mCurrentEffect.onTouchEvent(ev); |
| } |
| |
| super.onTouchEvent(ev); |
| if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { |
| mVelocityX = 0; |
| if (mState == ACCORDION_ANIMATION) { |
| // Let taps go through to take a capture during the accordion |
| return false; |
| } |
| getParent().requestDisallowInterceptTouchEvent(true); |
| if (mState == FULLY_SHOWN) { |
| mFocusItem = NO_ITEM_SELECTED; |
| setSwipeMode(false); |
| } else { |
| mFocusItem = getFocusItem(ev.getX(), ev.getY()); |
| setSwipeMode(true); |
| } |
| } else if (mState == ACCORDION_ANIMATION) { |
| // This is a swipe during accordion animation |
| mFocusItem = getFocusItem(ev.getX(), ev.getY()); |
| setSwipeMode(true); |
| |
| } |
| // Pass all touch events to gesture detector for gesture handling. |
| mGestureDetector.onTouchEvent(ev); |
| if (ev.getActionMasked() == MotionEvent.ACTION_UP || |
| ev.getActionMasked() == MotionEvent.ACTION_CANCEL) { |
| snap(); |
| mFocusItem = NO_ITEM_SELECTED; |
| } |
| return true; |
| } |
| |
| /** |
| * 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(); |
| if (mCurrentEffect != null) { |
| mCurrentEffect.setSize(mWidth, mHeight); |
| } |
| } |
| |
| /** |
| * Here we calculate the children size based on the orientation, change |
| * their layout parameters if needed before propagating onMeasure call |
| * to the children, so the newly changed params will take effect in this |
| * pass. |
| * |
| * @param widthMeasureSpec Horizontal space requirements as imposed by the |
| * parent |
| * @param heightMeasureSpec Vertical space requirements as imposed by the |
| * parent |
| */ |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| float height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() |
| - getPaddingBottom(); |
| |
| Configuration config = getResources().getConfiguration(); |
| if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) { |
| height = height / ROWS_TO_SHOW_IN_LANDSCAPE; |
| setVerticalScrollBarEnabled(true); |
| } else { |
| height = height / mTotalModes; |
| setVerticalScrollBarEnabled(false); |
| } |
| LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(mWidth, 0); |
| lp.width = LayoutParams.MATCH_PARENT; |
| for (int i = 0; i < mTotalModes; i++) { |
| // This is to avoid rounding that would cause the total height of the |
| // list a few pixels off the height of the screen. |
| int itemHeight = (int) (height * (i + 1)) - (int) (height * i); |
| lp.height = itemHeight; |
| mModeSelectorItems[i].setLayoutParams(lp); |
| } |
| 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; |
| } |
| mState = ACCORDION_ANIMATION; |
| resetModeSelectors(); |
| animateListToWidth(START_DELAY_MS, TOTAL_DURATION_MS, mAccordionInterpolator, |
| 0, mIconBlockWidth, 0); |
| } |
| |
| /** |
| * This starts the accordion animation with a delay. |
| * |
| * @param delay delay in milliseconds before starting animation |
| */ |
| public void startAccordionAnimationWithDelay(int delay) { |
| postDelayed(new Runnable() { |
| @Override |
| public void run() { |
| startAccordionAnimation(); |
| } |
| }, delay); |
| } |
| |
| /** |
| * 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); |
| mModeSelectorItems[i].unHighlight(); |
| } |
| // Visible width has been changed to 0 |
| onVisibleWidthChanged(0); |
| } |
| |
| private boolean isRunningAccordionAnimation() { |
| return mAnimatorSet != null && mAnimatorSet.isRunning(); |
| } |
| |
| /** |
| * Calculate the mode selector item in the list that is at position (x, y). |
| * |
| * @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) { |
| // Take into account the scrolling offset |
| x += getScrollX(); |
| y += getScrollY(); |
| |
| for (int i = 0; i < mModeSelectorItems.length; i++) { |
| if (mModeSelectorItems[i].getTop() <= y && mModeSelectorItems[i].getBottom() >= y) { |
| return i; |
| } |
| } |
| return NO_ITEM_SELECTED; |
| } |
| |
| 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() - deltaX; |
| } else { |
| longestWidth = mModeSelectorItems[0].getVisibleWidth() - deltaX; |
| } |
| insertNewPosition(longestWidth, mCurrentTime); |
| |
| for (int i = 0; i < mModeSelectorItems.length; i++) { |
| mModeSelectorItems[i].setVisibleWidth(calculateVisibleWidthForItem(i, |
| (int) longestWidth)); |
| } |
| if (longestWidth <= 0) { |
| reset(); |
| } |
| |
| itemId = itemId == NO_ITEM_SELECTED ? 0 : itemId; |
| onVisibleWidthChanged(mModeSelectorItems[itemId].getVisibleWidth()); |
| } |
| |
| /** |
| * 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); |
| } |
| |
| /** |
| * 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) { |
| 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) { |
| return historyPosition.getPosition(); |
| } 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; |
| mCurrentEffect = null; |
| setVisibility(INVISIBLE); |
| } |
| |
| /** |
| * When visible width of list is changed, the background of the list needs |
| * to darken/lighten correspondingly. |
| */ |
| private void onVisibleWidthChanged(int focusItemWidth) { |
| // Background alpha should be 0 before the icon block is entirely visible, |
| // and when the longest mode item is entirely shown (across the screen), the |
| // background should be 50% transparent. |
| if (focusItemWidth <= mIconBlockWidth) { |
| setBackgroundAlpha(0); |
| } else { |
| // Alpha should increase linearly when mode item goes beyond the icon block |
| // till it reaches its max width |
| int alpha = 127 * (focusItemWidth - mIconBlockWidth) / (mWidth - mIconBlockWidth); |
| setBackgroundAlpha(alpha); |
| } |
| } |
| |
| @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 |
| * on the current position of the list and the gesture we can decide which way |
| * to snap. |
| */ |
| private void snap() { |
| if (mState == SCROLLING) { |
| int itemId = Math.max(0, mFocusItem); |
| if (mModeSelectorItems[itemId].getVisibleWidth() < mIconBlockWidth) { |
| snapBack(); |
| } else if (Math.abs(mScrollTrendX) > Math.abs(mScrollTrendY) && mScrollTrendX > 0) { |
| snapBack(); |
| } else { |
| snapToFullScreen(); |
| } |
| } |
| } |
| |
| /** |
| * 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() { |
| snapBack(true); |
| } |
| |
| private void snapToFullScreen() { |
| if (mVelocityX <= VELOCITY_THRESHOLD) { |
| animateListToWidth(mWidth); |
| } else { |
| // If the fling velocity exceeds this threshold, snap to full screen |
| // at a constant speed. |
| animateListToWidthAtVelocity(mVelocityX, mWidth); |
| } |
| mState = FULLY_SHOWN; |
| if (mModeListOpenListener != null) { |
| mModeListOpenListener.onOpenFullScreen(); |
| } |
| } |
| |
| /** |
| * 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 void animateListToWidth(int... width) { |
| 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 |
| * @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 void animateListToWidth(int delay, int duration, |
| TimeInterpolator interpolator, 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); |
| animator.setDuration(duration); |
| animator.setStartDelay(i * delay); |
| animators.add(animator); |
| if (i == focusItem) { |
| animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { |
| @Override |
| public void onAnimationUpdate(ValueAnimator animation) { |
| onVisibleWidthChanged((Integer) animation.getAnimatedValue()); |
| } |
| }); |
| } |
| } |
| |
| mAnimatorSet = new AnimatorSet(); |
| mAnimatorSet.playTogether(animators); |
| mAnimatorSet.setInterpolator(interpolator); |
| mAnimatorSet.addListener(mModeListAnimatorListener); |
| mAnimatorSet.start(); |
| } |
| |
| /** |
| * 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 void 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) ((float) width / velocity); |
| animator.setDuration(duration); |
| animators.add(animator); |
| if (i == focusItem) { |
| animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { |
| @Override |
| public void onAnimationUpdate(ValueAnimator animation) { |
| onVisibleWidthChanged((Integer) animation.getAnimatedValue()); |
| } |
| }); |
| } |
| } |
| |
| mAnimatorSet = new AnimatorSet(); |
| mAnimatorSet.playTogether(animators); |
| mAnimatorSet.setInterpolator(null); |
| mAnimatorSet.addListener(mModeListAnimatorListener); |
| mAnimatorSet.start(); |
| } |
| |
| /** |
| * Called when the back key is pressed. |
| * |
| * @return Whether the UI responded to the key event. |
| */ |
| public boolean onBackPressed() { |
| if (mState == FULLY_SHOWN) { |
| snapBack(); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| 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)); |
| } |
| |
| @Override |
| public void setSize(int width, int height) { |
| mWidth = width; |
| mHeight = height; |
| } |
| |
| @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); |
| } |
| } |
| |
| public void setAnimationStartingPosition(int x, int y) { |
| mPeepHoleCenterX = x; |
| mPeepHoleCenterY = y; |
| } |
| |
| @Override |
| 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; |
| } |
| } |
| } |