From 4c6969a512cd70831249ec1d07691f16fe5465f5 Mon Sep 17 00:00:00 2001 From: Selim Cinek Date: Mon, 26 May 2014 19:22:17 +0200 Subject: [PATCH] Implemented basic camera and phone affordance. The phone and the camera can now be accessed when swiping anywhere on the background of the keyguard in the corresponding direction. Bug: 15126905 Change-Id: If5551078676275764d5b7ddbca6e71cf008a1904 --- .../SystemUI/res/layout/keyguard_bottom_area.xml | 16 +- packages/SystemUI/res/values/attrs.xml | 6 - packages/SystemUI/res/values/dimens.xml | 3 + .../android/systemui/statusbar/AlphaImageView.java | 48 +++ .../systemui/statusbar/FlingAnimationUtils.java | 96 ++++- .../statusbar/phone/KeyguardBottomAreaView.java | 65 ++-- .../statusbar/phone/KeyguardPageSwipeHelper.java | 398 +++++++++++++++++++++ .../statusbar/phone/NotificationPanelView.java | 214 +++++++---- .../systemui/statusbar/phone/PanelView.java | 42 ++- .../systemui/statusbar/phone/PhoneStatusBar.java | 2 +- .../statusbar/phone/SwipeAffordanceView.java | 222 ------------ 11 files changed, 756 insertions(+), 356 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/AlphaImageView.java create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardPageSwipeHelper.java delete mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/phone/SwipeAffordanceView.java diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml index 936f73be5675..9bf42b2cfc90 100644 --- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml +++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml @@ -22,7 +22,7 @@ android:layout_height="match_parent" android:layout_width="match_parent" > - + android:contentDescription="@string/accessibility_camera_button" /> - + android:contentDescription="@string/accessibility_phone_button" /> - + android:tint="#ffffffff" /> diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml index 35496897b94c..c45361874f1c 100644 --- a/packages/SystemUI/res/values/attrs.xml +++ b/packages/SystemUI/res/values/attrs.xml @@ -43,12 +43,6 @@ - - - - - - diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 6e35230dd991..610b37606dcc 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -294,6 +294,9 @@ 22dp 36dp + + 75dp + 16dp diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/AlphaImageView.java b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaImageView.java new file mode 100644 index 000000000000..06dc4e6b8984 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/AlphaImageView.java @@ -0,0 +1,48 @@ +/* + * 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; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; + +/** + * An ImageView which does not have overlapping renderings commands and therefore does not need a + * layer when alpha is changed. + */ +public class AlphaImageView extends ImageView { + public AlphaImageView(Context context) { + super(context); + } + + public AlphaImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AlphaImageView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public AlphaImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/FlingAnimationUtils.java b/packages/SystemUI/src/com/android/systemui/statusbar/FlingAnimationUtils.java index 7d576cb34243..5f1325b56ebd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/FlingAnimationUtils.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/FlingAnimationUtils.java @@ -18,6 +18,7 @@ package com.android.systemui.statusbar; import android.animation.ValueAnimator; import android.content.Context; +import android.view.ViewPropertyAnimator; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; @@ -46,6 +47,8 @@ public class FlingAnimationUtils { private float mMaxLengthSeconds; private float mHighVelocityPxPerSecond; + private AnimatorProperties mAnimatorProperties = new AnimatorProperties(); + public FlingAnimationUtils(Context ctx, float maxLengthSeconds) { mMaxLengthSeconds = maxLengthSeconds; mLinearOutSlowIn = new PathInterpolator(0, 0, LINEAR_OUT_SLOW_IN_X2, 1); @@ -80,18 +83,59 @@ public class FlingAnimationUtils { * @param currValue the current value * @param endValue the end value of the animator * @param velocity the current velocity of the motion + */ + public void apply(ViewPropertyAnimator animator, float currValue, float endValue, + float velocity) { + apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue)); + } + + /** + * Applies the interpolator and length to the animator, such that the fling animation is + * consistent with the finger motion. + * + * @param animator the animator to apply + * @param currValue the current value + * @param endValue the end value of the animator + * @param velocity the current velocity of the motion * @param maxDistance the maximum distance for this interaction; the maximum animation length * gets multiplied by the ratio between the actual distance and this value */ public void apply(ValueAnimator animator, float currValue, float endValue, float velocity, float maxDistance) { + AnimatorProperties properties = getProperties(currValue, endValue, velocity, + maxDistance); + animator.setDuration(properties.duration); + animator.setInterpolator(properties.interpolator); + } + + /** + * Applies the interpolator and length to the animator, such that the fling animation is + * consistent with the finger motion. + * + * @param animator the animator to apply + * @param currValue the current value + * @param endValue the end value of the animator + * @param velocity the current velocity of the motion + * @param maxDistance the maximum distance for this interaction; the maximum animation length + * gets multiplied by the ratio between the actual distance and this value + */ + public void apply(ViewPropertyAnimator animator, float currValue, float endValue, + float velocity, float maxDistance) { + AnimatorProperties properties = getProperties(currValue, endValue, velocity, + maxDistance); + animator.setDuration(properties.duration); + animator.setInterpolator(properties.interpolator); + } + + private AnimatorProperties getProperties(float currValue, + float endValue, float velocity, float maxDistance) { float maxLengthSeconds = (float) (mMaxLengthSeconds * Math.sqrt(Math.abs(endValue - currValue) / maxDistance)); float diff = Math.abs(endValue - currValue); float velAbs = Math.abs(velocity); float durationSeconds = LINEAR_OUT_SLOW_IN_START_GRADIENT * diff / velAbs; if (durationSeconds <= maxLengthSeconds) { - animator.setInterpolator(mLinearOutSlowIn); + mAnimatorProperties.interpolator = mLinearOutSlowIn; } else if (velAbs >= mMinVelocityPxPerSecond) { // Cross fade between fast-out-slow-in and linear interpolator with current velocity. @@ -100,14 +144,15 @@ public class FlingAnimationUtils { = new VelocityInterpolator(durationSeconds, velAbs, diff); InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator( velocityInterpolator, mLinearOutSlowIn, mLinearOutSlowIn); - animator.setInterpolator(superInterpolator); + mAnimatorProperties.interpolator = superInterpolator; } else { // Just use a normal interpolator which doesn't take the velocity into account. durationSeconds = maxLengthSeconds; - animator.setInterpolator(mFastOutSlowIn); + mAnimatorProperties.interpolator = mFastOutSlowIn; } - animator.setDuration((long) (durationSeconds * 1000)); + mAnimatorProperties.duration = (long) (durationSeconds * 1000); + return mAnimatorProperties; } /** @@ -124,6 +169,34 @@ public class FlingAnimationUtils { */ public void applyDismissing(ValueAnimator animator, float currValue, float endValue, float velocity, float maxDistance) { + AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity, + maxDistance); + animator.setDuration(properties.duration); + animator.setInterpolator(properties.interpolator); + } + + /** + * Applies the interpolator and length to the animator, such that the fling animation is + * consistent with the finger motion for the case when the animation is making something + * disappear. + * + * @param animator the animator to apply + * @param currValue the current value + * @param endValue the end value of the animator + * @param velocity the current velocity of the motion + * @param maxDistance the maximum distance for this interaction; the maximum animation length + * gets multiplied by the ratio between the actual distance and this value + */ + public void applyDismissing(ViewPropertyAnimator animator, float currValue, float endValue, + float velocity, float maxDistance) { + AnimatorProperties properties = getDismissingProperties(currValue, endValue, velocity, + maxDistance); + animator.setDuration(properties.duration); + animator.setInterpolator(properties.interpolator); + } + + private AnimatorProperties getDismissingProperties(float currValue, float endValue, + float velocity, float maxDistance) { float maxLengthSeconds = (float) (mMaxLengthSeconds * Math.pow(Math.abs(endValue - currValue) / maxDistance, 0.5f)); float diff = Math.abs(endValue - currValue); @@ -135,7 +208,7 @@ public class FlingAnimationUtils { Interpolator mLinearOutFasterIn = new PathInterpolator(0, 0, 1, y2); float durationSeconds = startGradient * diff / velAbs; if (durationSeconds <= maxLengthSeconds) { - animator.setInterpolator(mLinearOutFasterIn); + mAnimatorProperties.interpolator = mLinearOutFasterIn; } else if (velAbs >= mMinVelocityPxPerSecond) { // Cross fade between linear-out-faster-in and linear interpolator with current @@ -145,14 +218,15 @@ public class FlingAnimationUtils { = new VelocityInterpolator(durationSeconds, velAbs, diff); InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator( velocityInterpolator, mLinearOutFasterIn, mLinearOutSlowIn); - animator.setInterpolator(superInterpolator); + mAnimatorProperties.interpolator = superInterpolator; } else { // Just use a normal interpolator which doesn't take the velocity into account. durationSeconds = maxLengthSeconds; - animator.setInterpolator(mFastOutLinearIn); + mAnimatorProperties.interpolator = mFastOutLinearIn; } - animator.setDuration((long) (durationSeconds * 1000)); + mAnimatorProperties.duration = (long) (durationSeconds * 1000); + return mAnimatorProperties; } /** @@ -221,4 +295,10 @@ public class FlingAnimationUtils { return time * mVelocity / mDiff; } } + + private static class AnimatorProperties { + Interpolator interpolator; + long duration; + } + } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java index 714ad06ef095..994b3292e551 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java @@ -23,7 +23,6 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; -import android.os.PowerManager; import android.os.RemoteException; import android.os.UserHandle; import android.provider.MediaStore; @@ -33,26 +32,23 @@ import android.view.View; import android.view.accessibility.AccessibilityManager; import android.widget.FrameLayout; import android.widget.ImageView; - import com.android.systemui.R; /** * Implementation for the bottom area of the Keyguard, including camera/phone affordance and status * text. */ -public class KeyguardBottomAreaView extends FrameLayout - implements SwipeAffordanceView.AffordanceListener, +public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickListener, UnlockMethodCache.OnUnlockMethodChangedListener { final static String TAG = "PhoneStatusBar/KeyguardBottomAreaView"; private static final Intent PHONE_INTENT = new Intent(Intent.ACTION_DIAL); - private SwipeAffordanceView mCameraButton; - private SwipeAffordanceView mPhoneButton; + private ImageView mCameraImageView; + private ImageView mPhoneImageView; private ImageView mLockIcon; - private PowerManager mPowerManager; private ActivityStarter mActivityStarter; private UnlockMethodCache mUnlockMethodCache; @@ -76,11 +72,9 @@ public class KeyguardBottomAreaView extends FrameLayout @Override protected void onFinishInflate() { super.onFinishInflate(); - mCameraButton = (SwipeAffordanceView) findViewById(R.id.camera_button); - mPhoneButton = (SwipeAffordanceView) findViewById(R.id.phone_button); + mCameraImageView = (ImageView) findViewById(R.id.camera_button); + mPhoneImageView = (ImageView) findViewById(R.id.phone_button); mLockIcon = (ImageView) findViewById(R.id.lock_icon); - mCameraButton.setAffordanceListener(this); - mPhoneButton.setAffordanceListener(this); watchForDevicePolicyChanges(); watchForAccessibilityChanges(); updateCameraVisibility(); @@ -88,7 +82,6 @@ public class KeyguardBottomAreaView extends FrameLayout mUnlockMethodCache = UnlockMethodCache.getInstance(getContext()); mUnlockMethodCache.addListener(this); updateTrust(); - mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); } public void setActivityStarter(ActivityStarter activityStarter) { @@ -97,12 +90,12 @@ public class KeyguardBottomAreaView extends FrameLayout private void updateCameraVisibility() { boolean visible = !isCameraDisabledByDpm(); - mCameraButton.setVisibility(visible ? View.VISIBLE : View.GONE); + mCameraImageView.setVisibility(visible ? View.VISIBLE : View.GONE); } private void updatePhoneVisibility() { boolean visible = isPhoneVisible(); - mPhoneButton.setVisibility(visible ? View.VISIBLE : View.GONE); + mPhoneImageView.setVisibility(visible ? View.VISIBLE : View.GONE); } private boolean isPhoneVisible() { @@ -162,33 +155,31 @@ public class KeyguardBottomAreaView extends FrameLayout } private void enableAccessibility(boolean touchExplorationEnabled) { - mCameraButton.enableAccessibility(touchExplorationEnabled); - mPhoneButton.enableAccessibility(touchExplorationEnabled); + mCameraImageView.setOnClickListener(touchExplorationEnabled ? this : null); + mCameraImageView.setClickable(touchExplorationEnabled); + mPhoneImageView.setOnClickListener(touchExplorationEnabled ? this : null); + mPhoneImageView.setClickable(touchExplorationEnabled); } - private void launchCamera() { + @Override + public void onClick(View v) { + if (v == mCameraImageView) { + launchCamera(); + } else if (v == mPhoneImageView) { + launchPhone(); + } + } + + public void launchCamera() { mContext.startActivityAsUser( new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE), UserHandle.CURRENT); } - private void launchPhone() { + public void launchPhone() { mActivityStarter.startActivity(PHONE_INTENT); } - @Override - public void onUserActivity(long when) { - mPowerManager.userActivity(when, false); - } - - @Override - public void onActionPerformed(SwipeAffordanceView view) { - if (view == mCameraButton) { - launchCamera(); - } else if (view == mPhoneButton) { - launchPhone(); - } - } @Override protected void onVisibilityChanged(View changedView, int visibility) { @@ -208,6 +199,18 @@ public class KeyguardBottomAreaView extends FrameLayout mLockIcon.setImageResource(iconRes); } + public ImageView getPhoneImageView() { + return mPhoneImageView; + } + + public ImageView getCameraImageView() { + return mCameraImageView; + } + + public ImageView getLockIcon() { + return mLockIcon; + } + @Override public void onMethodSecureChanged(boolean methodSecure) { updateTrust(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardPageSwipeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardPageSwipeHelper.java new file mode 100644 index 000000000000..b4f4865395d2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardPageSwipeHelper.java @@ -0,0 +1,398 @@ +/* + * 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 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 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 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 getTranslationViews(); + + View getLeftIcon(); + + View getCenterIcon(); + + View getRightIcon(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java index 52688df0576b..838e5e3aeaf7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelView.java @@ -21,6 +21,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; +import android.content.res.Configuration; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.VelocityTracker; @@ -40,10 +41,13 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; import com.android.systemui.statusbar.stack.StackStateAnimator; +import java.util.ArrayList; + public class NotificationPanelView extends PanelView implements ExpandableView.OnHeightChangedListener, ObservableScrollView.Listener, - View.OnClickListener { + View.OnClickListener, KeyguardPageSwipeHelper.Callback { + private KeyguardPageSwipeHelper mPageSwiper; PhoneStatusBar mStatusBar; private StatusBarHeaderView mHeader; private View mQsContainer; @@ -58,7 +62,7 @@ public class NotificationPanelView extends PanelView implements private int mTrackingPointer; private VelocityTracker mVelocityTracker; - private boolean mTracking; + private boolean mQsTracking; /** * Whether we are currently handling a motion gesture in #onInterceptTouchEvent, but haven't @@ -92,6 +96,11 @@ public class NotificationPanelView extends PanelView implements new KeyguardClockPositionAlgorithm(); private KeyguardClockPositionAlgorithm.Result mClockPositionResult = new KeyguardClockPositionAlgorithm.Result(); + private boolean mIsSwipedHorizontally; + private boolean mIsExpanding; + private KeyguardBottomAreaView mKeyguardBottomArea; + private boolean mBlockTouches; + private ArrayList mSwipeTranslationViews = new ArrayList<>(); public NotificationPanelView(Context context, AttributeSet attrs) { super(context, attrs); @@ -129,6 +138,10 @@ public class NotificationPanelView extends PanelView implements mNotificationStackScroller.setOnHeightChangedListener(this); mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(getContext(), android.R.interpolator.fast_out_slow_in); + mKeyguardBottomArea = (KeyguardBottomAreaView) findViewById(R.id.keyguard_bottom_area); + mSwipeTranslationViews.add(mNotificationStackScroller); + mSwipeTranslationViews.add(mKeyguardStatusView); + mPageSwiper = new KeyguardPageSwipeHelper(this, getContext()); } @Override @@ -247,6 +260,12 @@ public class NotificationPanelView extends PanelView implements mQsExpansionEnabled = qsExpansionEnabled; } + public void resetViews() { + mBlockTouches = false; + mPageSwiper.reset(); + closeQs(); + } + public void closeQs() { cancelAnimation(); setQsExpansion(mQsMinExpansionHeight); @@ -263,9 +282,7 @@ public class NotificationPanelView extends PanelView implements public void fling(float vel, boolean always) { GestureRecorder gr = ((PhoneStatusBarView) mBar).mBar.getGestureRecorder(); if (gr != null) { - gr.tag( - "fling " + ((vel > 0) ? "open" : "closed"), - "notifications,v=" + vel); + gr.tag("fling " + ((vel > 0) ? "open" : "closed"), "notifications,v=" + vel); } super.fling(vel, always); } @@ -283,6 +300,9 @@ public class NotificationPanelView extends PanelView implements @Override public boolean onInterceptTouchEvent(MotionEvent event) { + if (mBlockTouches) { + return false; + } int pointerIndex = event.findPointerIndex(mTrackingPointer); if (pointerIndex < 0) { pointerIndex = 0; @@ -298,7 +318,7 @@ public class NotificationPanelView extends PanelView implements mInitialTouchX = x; initVelocityTracker(); trackMovement(event); - if (shouldIntercept(mInitialTouchX, mInitialTouchY, 0)) { + if (shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) { getParent().requestDisallowInterceptTouchEvent(true); } break; @@ -316,7 +336,7 @@ public class NotificationPanelView extends PanelView implements case MotionEvent.ACTION_MOVE: final float h = y - mInitialTouchY; trackMovement(event); - if (mTracking) { + if (mQsTracking) { // Already tracking because onOverscrolled was called. We need to update here // so we don't stop for a frame until the next touch event gets handled in @@ -327,12 +347,12 @@ public class NotificationPanelView extends PanelView implements return true; } if (Math.abs(h) > mTouchSlop && Math.abs(h) > Math.abs(x - mInitialTouchX) - && shouldIntercept(mInitialTouchX, mInitialTouchY, h)) { + && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, h)) { onQsExpansionStarted(); mInitialHeightOnTouch = mQsExpansionHeight; mInitialTouchY = y; mInitialTouchX = x; - mTracking = true; + mQsTracking = true; mIntercepting = false; return true; } @@ -341,9 +361,9 @@ public class NotificationPanelView extends PanelView implements case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: trackMovement(event); - if (mTracking) { - flingWithCurrentVelocity(); - mTracking = false; + if (mQsTracking) { + flingQsWithCurrentVelocity(); + mQsTracking = false; } mIntercepting = false; break; @@ -362,7 +382,7 @@ public class NotificationPanelView extends PanelView implements super.requestDisallowInterceptTouchEvent(disallowIntercept); } - private void flingWithCurrentVelocity() { + private void flingQsWithCurrentVelocity() { float vel = getCurrentVelocity(); // TODO: Better logic whether we should expand or not. @@ -371,65 +391,83 @@ public class NotificationPanelView extends PanelView implements @Override public boolean onTouchEvent(MotionEvent event) { + if (mBlockTouches) { + return false; + } // TODO: Handle doublefinger swipe to notifications again. Look at history for a reference // implementation. - if (mTracking) { - int pointerIndex = event.findPointerIndex(mTrackingPointer); - if (pointerIndex < 0) { - pointerIndex = 0; - mTrackingPointer = event.getPointerId(pointerIndex); + if (!mIsExpanding && !mQsExpanded && mStatusBar.getBarState() != StatusBarState.SHADE) { + mPageSwiper.onTouchEvent(event); + if (mPageSwiper.isSwipingInProgress()) { + return true; } - final float y = event.getY(pointerIndex); - final float x = event.getX(pointerIndex); + } + if (mQsTracking || mQsExpanded) { + return onQsTouch(event); + } - switch (event.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - mTracking = true; - mInitialTouchY = y; - mInitialTouchX = x; - onQsExpansionStarted(); - mInitialHeightOnTouch = mQsExpansionHeight; - 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); - mInitialHeightOnTouch = mQsExpansionHeight; - mInitialTouchY = newY; - mInitialTouchX = newX; - } - break; + super.onTouchEvent(event); + return true; + } - case MotionEvent.ACTION_MOVE: - final float h = y - mInitialTouchY; - setQsExpansion(h + mInitialHeightOnTouch); - trackMovement(event); - break; + @Override + protected boolean hasConflictingGestures() { + return mStatusBar.getBarState() != StatusBarState.SHADE; + } - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - mTracking = false; - mTrackingPointer = -1; - trackMovement(event); - flingWithCurrentVelocity(); - if (mVelocityTracker != null) { - mVelocityTracker.recycle(); - mVelocityTracker = null; - } - break; - } - return true; + private boolean onQsTouch(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: + mQsTracking = true; + mInitialTouchY = y; + mInitialTouchX = x; + onQsExpansionStarted(); + mInitialHeightOnTouch = mQsExpansionHeight; + 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); + mInitialHeightOnTouch = mQsExpansionHeight; + mInitialTouchY = newY; + mInitialTouchX = newX; + } + break; - // Consume touch events when QS are expanded. - return mQsExpanded || super.onTouchEvent(event); + case MotionEvent.ACTION_MOVE: + final float h = y - mInitialTouchY; + setQsExpansion(h + mInitialHeightOnTouch); + trackMovement(event); + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mQsTracking = false; + mTrackingPointer = -1; + trackMovement(event); + flingQsWithCurrentVelocity(); + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + break; + } + return true; } @Override @@ -439,7 +477,7 @@ public class NotificationPanelView extends PanelView implements mInitialHeightOnTouch = mQsExpansionHeight; mInitialTouchY = mLastTouchY; mInitialTouchX = mLastTouchX; - mTracking = true; + mQsTracking = true; } } @@ -569,7 +607,7 @@ public class NotificationPanelView extends PanelView implements /** * @return Whether we should intercept a gesture to open Quick Settings. */ - private boolean shouldIntercept(float x, float y, float yDiff) { + private boolean shouldQuickSettingsIntercept(float x, float y, float yDiff) { if (!mQsExpansionEnabled) { return false; } @@ -647,12 +685,14 @@ public class NotificationPanelView extends PanelView implements protected void onExpandingStarted() { super.onExpandingStarted(); mNotificationStackScroller.onExpansionStarted(); + mIsExpanding = true; } @Override protected void onExpandingFinished() { super.onExpandingFinished(); mNotificationStackScroller.onExpansionStopped(); + mIsExpanding = false; } @Override @@ -686,6 +726,12 @@ public class NotificationPanelView extends PanelView implements } @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mPageSwiper.onConfigurationChanged(); + } + + @Override public void onClick(View v) { if (v == mHeader.getBackgroundView()) { onQsExpansionStarted(); @@ -696,4 +742,40 @@ public class NotificationPanelView extends PanelView implements } } } + + @Override + public void onAnimationToSideStarted(boolean rightPage) { + if (rightPage) { + mKeyguardBottomArea.launchCamera(); + } else { + mKeyguardBottomArea.launchPhone(); + } + mBlockTouches = true; + } + + + @Override + public float getPageWidth() { + return getWidth(); + } + + @Override + public ArrayList getTranslationViews() { + return mSwipeTranslationViews; + } + + @Override + public View getLeftIcon() { + return mKeyguardBottomArea.getPhoneImageView(); + } + + @Override + public View getCenterIcon() { + return mKeyguardBottomArea.getLockIcon(); + } + + @Override + public View getRightIcon() { + return mKeyguardBottomArea.getCameraImageView(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java index 7500c105e9f9..f6db8a82b0bf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PanelView.java @@ -35,7 +35,7 @@ import com.android.systemui.statusbar.FlingAnimationUtils; import java.io.FileDescriptor; import java.io.PrintWriter; -public class PanelView extends FrameLayout { +public abstract class PanelView extends FrameLayout { public static final boolean DEBUG = PanelBar.DEBUG; public static final String TAG = PanelView.class.getSimpleName(); protected float mOverExpansion; @@ -130,21 +130,24 @@ public class PanelView extends FrameLayout { final float y = event.getY(pointerIndex); final float x = event.getX(pointerIndex); + boolean waitForTouchSlop = hasConflictingGestures(); + switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: - mTracking = true; mInitialTouchY = y; mInitialTouchX = x; + mInitialOffsetOnTouch = mExpandedHeight; if (mVelocityTracker == null) { initVelocityTracker(); } trackMovement(event); - if (mHeightAnimator != null) { - mHeightAnimator.cancel(); // end any outstanding animations + if (!waitForTouchSlop || mHeightAnimator != null) { + if (mHeightAnimator != null) { + mHeightAnimator.cancel(); // end any outstanding animations + } + onTrackingStarted(); } - onTrackingStarted(); - mInitialOffsetOnTouch = mExpandedHeight; if (mExpandedHeight == 0) { mJustPeeked = true; runPeekAnimation(); @@ -166,15 +169,27 @@ public class PanelView extends FrameLayout { break; case MotionEvent.ACTION_MOVE: - final float h = y - mInitialTouchY + mInitialOffsetOnTouch; - if (h > mPeekHeight) { + float h = y - mInitialTouchY; + if (waitForTouchSlop && !mTracking && Math.abs(h) > mTouchSlop + && Math.abs(h) > Math.abs(x - mInitialTouchX)) { + mInitialOffsetOnTouch = mExpandedHeight; + mInitialTouchX = x; + mInitialTouchY = y; + if (mHeightAnimator != null) { + mHeightAnimator.cancel(); // end any outstanding animations + } + onTrackingStarted(); + h = 0; + } + final float newHeight = h + mInitialOffsetOnTouch; + if (newHeight > mPeekHeight) { if (mPeekAnimator != null && mPeekAnimator.isStarted()) { mPeekAnimator.cancel(); } mJustPeeked = false; } - if (!mJustPeeked) { - setExpandedHeightInternal(h); + if (!mJustPeeked && (!waitForTouchSlop || mTracking)) { + setExpandedHeightInternal(newHeight); mBar.panelExpansionChanged(PanelView.this, mExpandedFraction); } @@ -183,7 +198,6 @@ public class PanelView extends FrameLayout { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: - mTracking = false; mTrackingPointer = -1; trackMovement(event); boolean expand = flingWithCurrentVelocity(); @@ -194,14 +208,18 @@ public class PanelView extends FrameLayout { } break; } - return true; + return !waitForTouchSlop || mTracking; } + protected abstract boolean hasConflictingGestures(); + protected void onTrackingStopped(boolean expand) { + mTracking = false; mBar.onTrackingStopped(PanelView.this, expand); } protected void onTrackingStarted() { + mTracking = true; mBar.onTrackingStarted(PanelView.this); onExpandingStarted(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java index 15ad7090155b..c61392addb43 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java @@ -2774,7 +2774,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mKeyguardStatusView.setVisibility(View.VISIBLE); mKeyguardIndicationTextView.setVisibility(View.VISIBLE); mKeyguardIndicationTextView.switchIndication(mKeyguardHotwordPhrase); - mNotificationPanel.closeQs(); + mNotificationPanel.resetViews(); } else { mKeyguardStatusView.setVisibility(View.GONE); mKeyguardIndicationTextView.setVisibility(View.GONE); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SwipeAffordanceView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SwipeAffordanceView.java deleted file mode 100644 index 049c5fc5786d..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SwipeAffordanceView.java +++ /dev/null @@ -1,222 +0,0 @@ -/* - * 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.content.Context; -import android.content.res.TypedArray; -import android.os.SystemClock; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.animation.AccelerateInterpolator; -import android.view.animation.DecelerateInterpolator; -import android.widget.Button; - -import com.android.systemui.R; -import com.android.systemui.statusbar.policy.KeyButtonView; - -/** - * A swipeable button for affordances on the lockscreen. This is used for the camera and phone - * affordance. - */ -public class SwipeAffordanceView extends KeyButtonView { - - private static final int SWIPE_DIRECTION_START = 0; - private static final int SWIPE_DIRECTION_END = 1; - - private static final int SWIPE_DIRECTION_LEFT = 0; - private static final int SWIPE_DIRECTION_RIGHT = 1; - - private AffordanceListener mListener; - private int mScaledTouchSlop; - private float mDragDistance; - private int mResolvedSwipeDirection; - private int mSwipeDirection; - - public SwipeAffordanceView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public SwipeAffordanceView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - TypedArray a = context.getTheme().obtainStyledAttributes( - attrs, - R.styleable.SwipeAffordanceView, - 0, 0); - try { - mSwipeDirection = a.getInt(R.styleable.SwipeAffordanceView_swipeDirection, 0); - } finally { - a.recycle(); - } - } - - @Override - public void onRtlPropertiesChanged(int layoutDirection) { - super.onRtlPropertiesChanged(layoutDirection); - if (!isLayoutRtl()) { - mResolvedSwipeDirection = mSwipeDirection; - } else { - mResolvedSwipeDirection = mSwipeDirection == SWIPE_DIRECTION_START - ? SWIPE_DIRECTION_RIGHT - : SWIPE_DIRECTION_LEFT; - } - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mDragDistance = getResources().getDimension(R.dimen.affordance_drag_distance); - mScaledTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); - } - - public void enableAccessibility(boolean touchExplorationEnabled) { - - // Add a touch handler or accessibility click listener for camera button. - if (touchExplorationEnabled) { - setOnTouchListener(null); - setOnClickListener(mClickListener); - } else { - setOnTouchListener(mTouchListener); - setOnClickListener(null); - } - } - - public void setAffordanceListener(AffordanceListener listener) { - mListener = listener; - } - - private void onActionPerformed() { - if (mListener != null) { - mListener.onActionPerformed(this); - } - } - - private void onUserActivity(long when) { - if (mListener != null) { - mListener.onUserActivity(when); - } - } - - private final OnClickListener mClickListener = new OnClickListener() { - @Override - public void onClick(View v) { - onActionPerformed(); - } - }; - - private final OnTouchListener mTouchListener = new OnTouchListener() { - private float mStartX; - private boolean mTouchSlopReached; - private boolean mSkipCancelAnimation; - - @Override - public boolean onTouch(final View view, MotionEvent event) { - float realX = event.getRawX(); - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - mStartX = realX; - mTouchSlopReached = false; - mSkipCancelAnimation = false; - break; - case MotionEvent.ACTION_MOVE: - if (mResolvedSwipeDirection == SWIPE_DIRECTION_LEFT - ? realX > mStartX - : realX < mStartX) { - realX = mStartX; - } - if (mResolvedSwipeDirection == SWIPE_DIRECTION_LEFT - ? realX < mStartX - mDragDistance - : realX > mStartX + mDragDistance) { - view.setPressed(true); - onUserActivity(event.getEventTime()); - } else { - view.setPressed(false); - } - if (mResolvedSwipeDirection == SWIPE_DIRECTION_LEFT - ? realX < mStartX - mScaledTouchSlop - : realX > mStartX + mScaledTouchSlop) { - mTouchSlopReached = true; - } - view.setTranslationX(mResolvedSwipeDirection == SWIPE_DIRECTION_LEFT - ? Math.max(realX - mStartX, -mDragDistance) - : Math.min(realX - mStartX, mDragDistance)); - break; - case MotionEvent.ACTION_UP: - if (mResolvedSwipeDirection == SWIPE_DIRECTION_LEFT - ? realX < mStartX - mDragDistance - : realX > mStartX + mDragDistance) { - onActionPerformed(); - view.animate().x(mResolvedSwipeDirection == SWIPE_DIRECTION_LEFT - ? -view.getWidth() - : ((View) view.getParent()).getWidth() + view.getWidth()) - .setInterpolator(new AccelerateInterpolator(2f)).withEndAction( - new Runnable() { - @Override - public void run() { - view.setTranslationX(0); - } - }); - mSkipCancelAnimation = true; - } - if (mResolvedSwipeDirection == SWIPE_DIRECTION_LEFT - ? realX < mStartX - mScaledTouchSlop - : realX > mStartX + mScaledTouchSlop) { - mTouchSlopReached = true; - } - if (!mTouchSlopReached) { - mSkipCancelAnimation = true; - view.animate().translationX(mResolvedSwipeDirection == SWIPE_DIRECTION_LEFT - ? -mDragDistance / 2 - : mDragDistance / 2). - setInterpolator(new DecelerateInterpolator()).withEndAction( - new Runnable() { - @Override - public void run() { - view.animate().translationX(0). - setInterpolator(new AccelerateInterpolator()); - } - }); - } - case MotionEvent.ACTION_CANCEL: - view.setPressed(false); - if (!mSkipCancelAnimation) { - view.animate().translationX(0) - .setInterpolator(new AccelerateInterpolator(2f)); - } - break; - } - return true; - } - }; - - public interface AffordanceListener { - - /** - * Called when the view would like to report user activity. - * - * @param when The timestamp of the user activity in {@link SystemClock#uptimeMillis} time - * base. - */ - void onUserActivity(long when); - - /** - * Called when the action of the affordance has been performed. - */ - void onActionPerformed(SwipeAffordanceView view); - } -} -- 2.11.0