| /* |
| * Copyright (C) 2014 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.systemui.statusbar.phone; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.ValueAnimator; |
| import android.content.Context; |
| import android.os.PowerManager; |
| import android.view.MotionEvent; |
| import android.view.VelocityTracker; |
| import android.view.View; |
| import android.view.ViewConfiguration; |
| import android.view.ViewPropertyAnimator; |
| import android.view.animation.AnimationUtils; |
| import android.view.animation.Interpolator; |
| import com.android.systemui.R; |
| import com.android.systemui.statusbar.FlingAnimationUtils; |
| |
| import java.util.ArrayList; |
| |
| /** |
| * A touch handler of the Keyguard which is responsible for swiping the content left or right. |
| */ |
| public class KeyguardPageSwipeHelper { |
| |
| private static final float SWIPE_MAX_ICON_SCALE_AMOUNT = 2.0f; |
| private static final float SWIPE_RESTING_ALPHA_AMOUNT = 0.7f; |
| private final Context mContext; |
| |
| private FlingAnimationUtils mFlingAnimationUtils; |
| private Callback mCallback; |
| private int mTrackingPointer; |
| private VelocityTracker mVelocityTracker; |
| private boolean mSwipingInProgress; |
| private float mInitialTouchX; |
| private float mInitialTouchY; |
| private float mTranslation; |
| private float mTranslationOnDown; |
| private int mTouchSlop; |
| private int mMinTranslationAmount; |
| private int mMinFlingVelocity; |
| private PowerManager mPowerManager; |
| private final View mLeftIcon; |
| private final View mCenterIcon; |
| private final View mRightIcon; |
| private Interpolator mFastOutSlowIn; |
| private Animator mSwipeAnimator; |
| private boolean mCallbackCalled; |
| |
| KeyguardPageSwipeHelper(Callback callback, Context context) { |
| mContext = context; |
| mCallback = callback; |
| mLeftIcon = mCallback.getLeftIcon(); |
| mCenterIcon = mCallback.getCenterIcon(); |
| mRightIcon = mCallback.getRightIcon(); |
| updateIcon(mLeftIcon, 1.0f, SWIPE_RESTING_ALPHA_AMOUNT, false); |
| updateIcon(mCenterIcon, 1.0f, SWIPE_RESTING_ALPHA_AMOUNT, false); |
| updateIcon(mRightIcon, 1.0f, SWIPE_RESTING_ALPHA_AMOUNT, false); |
| mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); |
| initDimens(); |
| } |
| |
| private void initDimens() { |
| final ViewConfiguration configuration = ViewConfiguration.get(mContext); |
| mTouchSlop = configuration.getScaledTouchSlop(); |
| mMinFlingVelocity = configuration.getScaledMinimumFlingVelocity(); |
| mMinTranslationAmount = mContext.getResources().getDimensionPixelSize( |
| R.dimen.keyguard_min_swipe_amount); |
| mFlingAnimationUtils = new FlingAnimationUtils(mContext, 0.4f); |
| mFastOutSlowIn = AnimationUtils.loadInterpolator(mContext, |
| android.R.interpolator.fast_out_slow_in); |
| } |
| |
| public boolean onTouchEvent(MotionEvent event) { |
| int pointerIndex = event.findPointerIndex(mTrackingPointer); |
| if (pointerIndex < 0) { |
| pointerIndex = 0; |
| mTrackingPointer = event.getPointerId(pointerIndex); |
| } |
| final float y = event.getY(pointerIndex); |
| final float x = event.getX(pointerIndex); |
| |
| switch (event.getActionMasked()) { |
| case MotionEvent.ACTION_DOWN: |
| if (mSwipingInProgress) { |
| cancelAnimations(); |
| } |
| mInitialTouchY = y; |
| mInitialTouchX = x; |
| mTranslationOnDown = mTranslation; |
| initVelocityTracker(); |
| trackMovement(event); |
| break; |
| |
| case MotionEvent.ACTION_POINTER_UP: |
| final int upPointer = event.getPointerId(event.getActionIndex()); |
| if (mTrackingPointer == upPointer) { |
| // gesture is ongoing, find a new pointer to track |
| final int newIndex = event.getPointerId(0) != upPointer ? 0 : 1; |
| final float newY = event.getY(newIndex); |
| final float newX = event.getX(newIndex); |
| mTrackingPointer = event.getPointerId(newIndex); |
| mInitialTouchY = newY; |
| mInitialTouchX = newX; |
| mTranslationOnDown = mTranslation; |
| } |
| break; |
| |
| case MotionEvent.ACTION_MOVE: |
| final float w = x - mInitialTouchX; |
| trackMovement(event); |
| if (((leftSwipePossible() && w > mTouchSlop) |
| || (rightSwipePossible() && w < -mTouchSlop)) |
| && Math.abs(w) > Math.abs(y - mInitialTouchY) |
| && !mSwipingInProgress) { |
| cancelAnimations(); |
| mInitialTouchY = y; |
| mInitialTouchX = x; |
| mTranslationOnDown = mTranslation; |
| mSwipingInProgress = true; |
| } |
| if (mSwipingInProgress) { |
| setTranslation(mTranslationOnDown + x - mInitialTouchX, false); |
| onUserActivity(event.getEventTime()); |
| } |
| break; |
| |
| case MotionEvent.ACTION_UP: |
| case MotionEvent.ACTION_CANCEL: |
| mTrackingPointer = -1; |
| trackMovement(event); |
| if (mSwipingInProgress) { |
| flingWithCurrentVelocity(); |
| } |
| if (mVelocityTracker != null) { |
| mVelocityTracker.recycle(); |
| mVelocityTracker = null; |
| } |
| break; |
| } |
| return true; |
| } |
| |
| private boolean rightSwipePossible() { |
| return mRightIcon.getVisibility() == View.VISIBLE; |
| } |
| |
| private boolean leftSwipePossible() { |
| return mLeftIcon.getVisibility() == View.VISIBLE; |
| } |
| |
| public boolean onInterceptTouchEvent(MotionEvent ev) { |
| return false; |
| } |
| |
| private void onUserActivity(long when) { |
| mPowerManager.userActivity(when, false); |
| } |
| |
| private void cancelAnimations() { |
| ArrayList<View> targetViews = mCallback.getTranslationViews(); |
| for (View target : targetViews) { |
| target.animate().cancel(); |
| } |
| View targetView = mTranslation > 0 ? mLeftIcon : mRightIcon; |
| targetView.animate().cancel(); |
| if (mSwipeAnimator != null) { |
| mSwipeAnimator.removeAllListeners(); |
| mSwipeAnimator.cancel(); |
| hideInactiveIcons(true); |
| } |
| } |
| |
| private void flingWithCurrentVelocity() { |
| float vel = getCurrentVelocity(); |
| |
| // We snap back if the current translation is not far enough |
| boolean snapBack = Math.abs(mTranslation) < mMinTranslationAmount; |
| |
| // or if the velocity is in the opposite direction. |
| boolean velIsInWrongDirection = vel * mTranslation < 0; |
| snapBack |= Math.abs(vel) > mMinFlingVelocity && velIsInWrongDirection; |
| vel = snapBack ^ velIsInWrongDirection ? 0 : vel; |
| fling(vel, snapBack); |
| } |
| |
| private void fling(float vel, final boolean snapBack) { |
| float target = mTranslation < 0 ? -mCallback.getPageWidth() : mCallback.getPageWidth(); |
| target = snapBack ? 0 : target; |
| |
| // translation Animation |
| startTranslationAnimations(vel, target); |
| |
| // animate left / right icon |
| startIconAnimation(vel, snapBack, target); |
| |
| ValueAnimator animator = ValueAnimator.ofFloat(mTranslation, target); |
| mFlingAnimationUtils.apply(animator, mTranslation, target, vel); |
| animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { |
| @Override |
| public void onAnimationUpdate(ValueAnimator animation) { |
| mTranslation = (float) animation.getAnimatedValue(); |
| } |
| }); |
| animator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mSwipeAnimator = null; |
| mSwipingInProgress = false; |
| if (!snapBack && !mCallbackCalled) { |
| |
| // ensure that the callback is called eventually |
| mCallback.onAnimationToSideStarted(mTranslation < 0); |
| mCallbackCalled = true; |
| } |
| } |
| }); |
| if (!snapBack) { |
| mCallbackCalled = false; |
| animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { |
| int frameNumber; |
| |
| @Override |
| public void onAnimationUpdate(ValueAnimator animation) { |
| if (frameNumber == 2 && !mCallbackCalled) { |
| |
| // we have to wait for the second frame for this call, |
| // until the render thread has definitely kicked in, to avoid a lag. |
| mCallback.onAnimationToSideStarted(mTranslation < 0); |
| mCallbackCalled = true; |
| } |
| frameNumber++; |
| } |
| }); |
| } else { |
| showAllIcons(true); |
| } |
| animator.start(); |
| mSwipeAnimator = animator; |
| } |
| |
| private void startTranslationAnimations(float vel, float target) { |
| ArrayList<View> targetViews = mCallback.getTranslationViews(); |
| for (View targetView : targetViews) { |
| ViewPropertyAnimator animator = targetView.animate(); |
| mFlingAnimationUtils.apply(animator, mTranslation, target, vel); |
| animator.translationX(target); |
| } |
| } |
| |
| private void startIconAnimation(float vel, boolean snapBack, float target) { |
| float scale = snapBack ? 1.0f : SWIPE_MAX_ICON_SCALE_AMOUNT; |
| float alpha = snapBack ? SWIPE_RESTING_ALPHA_AMOUNT : 1.0f; |
| View targetView = mTranslation > 0 |
| ? mLeftIcon |
| : mRightIcon; |
| if (targetView.getVisibility() == View.VISIBLE) { |
| ViewPropertyAnimator iconAnimator = targetView.animate(); |
| mFlingAnimationUtils.apply(iconAnimator, mTranslation, target, vel); |
| iconAnimator.scaleX(scale); |
| iconAnimator.scaleY(scale); |
| iconAnimator.alpha(alpha); |
| } |
| } |
| |
| private void setTranslation(float translation, boolean isReset) { |
| translation = rightSwipePossible() ? translation : Math.max(0, translation); |
| translation = leftSwipePossible() ? translation : Math.min(0, translation); |
| if (translation != mTranslation) { |
| ArrayList<View> translatedViews = mCallback.getTranslationViews(); |
| for (View view : translatedViews) { |
| view.setTranslationX(translation); |
| } |
| if (translation == 0.0f) { |
| boolean animate = !isReset; |
| showAllIcons(animate); |
| } else { |
| View targetView = translation > 0 ? mLeftIcon : mRightIcon; |
| float progress = Math.abs(translation) / mCallback.getPageWidth(); |
| progress = Math.min(progress, 1.0f); |
| float alpha = SWIPE_RESTING_ALPHA_AMOUNT * (1.0f - progress) + progress; |
| float scale = (1.0f - progress) + progress * SWIPE_MAX_ICON_SCALE_AMOUNT; |
| updateIcon(targetView, scale, alpha, false); |
| View otherView = translation < 0 ? mLeftIcon : mRightIcon; |
| if (mTranslation * translation <= 0) { |
| // The sign of the translation has changed so we need to hide the other icons |
| updateIcon(otherView, 0, 0, true); |
| updateIcon(mCenterIcon, 0, 0, true); |
| } |
| } |
| mTranslation = translation; |
| } |
| } |
| |
| private void showAllIcons(boolean animate) { |
| float scale = 1.0f; |
| float alpha = SWIPE_RESTING_ALPHA_AMOUNT; |
| updateIcon(mRightIcon, scale, alpha, animate); |
| updateIcon(mCenterIcon, scale, alpha, animate); |
| updateIcon(mLeftIcon, scale, alpha, animate); |
| } |
| |
| private void hideInactiveIcons(boolean animate){ |
| View otherView = mTranslation < 0 ? mLeftIcon : mRightIcon; |
| updateIcon(otherView, 0, 0, animate); |
| updateIcon(mCenterIcon, 0, 0, animate); |
| } |
| |
| private void updateIcon(View view, float scale, float alpha, boolean animate) { |
| if (view.getVisibility() != View.VISIBLE) { |
| return; |
| } |
| if (!animate) { |
| view.setAlpha(alpha); |
| view.setScaleX(scale); |
| view.setScaleY(scale); |
| // TODO: remove this invalidate once the property setters invalidate it properly |
| view.invalidate(); |
| } else { |
| if (view.getAlpha() != alpha || view.getScaleX() != scale) { |
| view.animate() |
| .setInterpolator(mFastOutSlowIn) |
| .alpha(alpha) |
| .scaleX(scale) |
| .scaleY(scale); |
| } |
| } |
| } |
| |
| private void trackMovement(MotionEvent event) { |
| if (mVelocityTracker != null) { |
| mVelocityTracker.addMovement(event); |
| } |
| } |
| |
| private void initVelocityTracker() { |
| if (mVelocityTracker != null) { |
| mVelocityTracker.recycle(); |
| } |
| mVelocityTracker = VelocityTracker.obtain(); |
| } |
| |
| private float getCurrentVelocity() { |
| if (mVelocityTracker == null) { |
| return 0; |
| } |
| mVelocityTracker.computeCurrentVelocity(1000); |
| return mVelocityTracker.getXVelocity(); |
| } |
| |
| public void onConfigurationChanged() { |
| initDimens(); |
| } |
| |
| public void reset() { |
| setTranslation(0.0f, true); |
| mSwipingInProgress = false; |
| } |
| |
| public boolean isSwipingInProgress() { |
| return mSwipingInProgress; |
| } |
| |
| public interface Callback { |
| |
| /** |
| * Notifies the callback when an animation to a side page was started. |
| * |
| * @param rightPage Is the page animated to the right page? |
| */ |
| void onAnimationToSideStarted(boolean rightPage); |
| |
| float getPageWidth(); |
| |
| ArrayList<View> getTranslationViews(); |
| |
| View getLeftIcon(); |
| |
| View getCenterIcon(); |
| |
| View getRightIcon(); |
| } |
| } |