diff options
author | Eric Erfanian <erfanian@google.com> | 2017-02-22 16:32:36 -0800 |
---|---|---|
committer | Eric Erfanian <erfanian@google.com> | 2017-03-01 09:56:52 -0800 |
commit | ccca31529c07970e89419fb85a9e8153a5396838 (patch) | |
tree | a7034c0a01672b97728c13282a2672771cd28baa /java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java | |
parent | e7ae4624ba6f25cb8e648db74e0d64c0113a16ba (diff) |
Update dialer sources.
Test: Built package and system image.
This change clobbers the old source, and is an export
from an internal Google repository.
The internal repository was forked form Android in March,
and this change includes modifications since then, to
near the v8 release.
Since the fork, we've moved code from monolithic to independent modules. In addition,
we've switched to Blaze/Bazel as the build sysetm. This export, however, still uses make.
New dependencies have been added:
- Dagger
- Auto-Value
- Glide
- Libshortcutbadger
Going forward, development will still be in Google3, and the Gerrit release
will become an automated export, with the next drop happening in ~ two weeks.
Android.mk includes local modifications from ToT.
Abridged changelog:
Bug fixes
● Not able to mute, add a call when using Phone app in multiwindow mode
● Double tap on keypad triggering multiple key and tones
● Reported spam numbers not showing as spam in the call log
● Crash when user tries to block number while Phone app is not set as default
● Crash when user picks a number from search auto-complete list
Visual Voicemail (VVM) improvements
● Share Voicemail audio via standard exporting mechanisms that support file attachment
(email, MMS, etc.)
● Make phone number, email and web sites in VVM transcript clickable
● Set PIN before declining VVM Terms of Service {Carrier}
● Set client type for outbound visual voicemail SMS {Carrier}
New incoming call and incall UI on older devices
(Android M)
● Updated Phone app icon
● New incall UI (large buttons, button labels)
● New and animated Answer/Reject gestures
Accessibility
● Add custom answer/decline call buttons on answer screen for touch exploration
accessibility services
● Increase size of touch target
● Add verbal feedback when a Voicemail fails to load
● Fix pressing of Phone buttons while in a phone call using Switch Access
● Fix selecting and opening contacts in talkback mode
● Split focus for ‘Learn More’ link in caller id & spam to help distinguish similar text
Other
● Backup & Restore for App Preferences
● Prompt user to enable Wi-Fi calling if the call ends due to out of service and Wi-Fi is
connected
● Rename “Dialpad” to “Keypad”
● Show "Private number" for restricted calls
● Delete unused items (vcard, add contact, call history) from Phone menu
Change-Id: I2a7e53532a24c21bf308bf0a6d178d7ddbca4958
Diffstat (limited to 'java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java')
-rw-r--r-- | java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java | 642 |
1 files changed, 642 insertions, 0 deletions
diff --git a/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java b/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java new file mode 100644 index 000000000..62845b748 --- /dev/null +++ b/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java @@ -0,0 +1,642 @@ +/* + * Copyright (C) 2016 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.incallui.answer.impl.affordance; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.Context; +import android.support.annotation.Nullable; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import com.android.incallui.answer.impl.utils.FlingAnimationUtils; +import com.android.incallui.answer.impl.utils.Interpolators; + +/** A touch handler of the swipe buttons */ +public class SwipeButtonHelper { + + public static final float SWIPE_RESTING_ALPHA_AMOUNT = 0.87f; + public static final long HINT_PHASE1_DURATION = 200; + private static final long HINT_PHASE2_DURATION = 350; + private static final float BACKGROUND_RADIUS_SCALE_FACTOR = 0.25f; + private static final int HINT_CIRCLE_OPEN_DURATION = 500; + + private final Context context; + private final Callback callback; + + private FlingAnimationUtils flingAnimationUtils; + private VelocityTracker velocityTracker; + private boolean swipingInProgress; + private float initialTouchX; + private float initialTouchY; + private float translation; + private float translationOnDown; + private int touchSlop; + private int minTranslationAmount; + private int minFlingVelocity; + private int hintGrowAmount; + @Nullable private SwipeButtonView leftIcon; + @Nullable private SwipeButtonView rightIcon; + private Animator swipeAnimator; + private int minBackgroundRadius; + private boolean motionCancelled; + private int touchTargetSize; + private View targetedView; + private boolean touchSlopExeeded; + private AnimatorListenerAdapter flingEndListener = + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + swipeAnimator = null; + swipingInProgress = false; + targetedView = null; + } + }; + private Runnable animationEndRunnable = + new Runnable() { + @Override + public void run() { + callback.onAnimationToSideEnded(); + } + }; + + public SwipeButtonHelper(Callback callback, Context context) { + this.context = context; + this.callback = callback; + init(); + } + + public void init() { + initIcons(); + updateIcon( + leftIcon, + 0.0f, + leftIcon != null ? leftIcon.getRestingAlpha() : 0, + false, + false, + true, + false); + updateIcon( + rightIcon, + 0.0f, + rightIcon != null ? rightIcon.getRestingAlpha() : 0, + false, + false, + true, + false); + initDimens(); + } + + private void initDimens() { + final ViewConfiguration configuration = ViewConfiguration.get(context); + touchSlop = configuration.getScaledPagingTouchSlop(); + minFlingVelocity = configuration.getScaledMinimumFlingVelocity(); + minTranslationAmount = + context.getResources().getDimensionPixelSize(R.dimen.answer_min_swipe_amount); + minBackgroundRadius = + context + .getResources() + .getDimensionPixelSize(R.dimen.answer_affordance_min_background_radius); + touchTargetSize = + context.getResources().getDimensionPixelSize(R.dimen.answer_affordance_touch_target_size); + hintGrowAmount = + context.getResources().getDimensionPixelSize(R.dimen.hint_grow_amount_sideways); + flingAnimationUtils = new FlingAnimationUtils(context, 0.4f); + } + + private void initIcons() { + leftIcon = callback.getLeftIcon(); + rightIcon = callback.getRightIcon(); + updatePreviews(); + } + + public void updatePreviews() { + if (leftIcon != null) { + leftIcon.setPreviewView(callback.getLeftPreview()); + } + if (rightIcon != null) { + rightIcon.setPreviewView(callback.getRightPreview()); + } + } + + public boolean onTouchEvent(MotionEvent event) { + int action = event.getActionMasked(); + if (motionCancelled && action != MotionEvent.ACTION_DOWN) { + return false; + } + final float y = event.getY(); + final float x = event.getX(); + + boolean isUp = false; + switch (action) { + case MotionEvent.ACTION_DOWN: + View targetView = getIconAtPosition(x, y); + if (targetView == null || (targetedView != null && targetedView != targetView)) { + motionCancelled = true; + return false; + } + if (targetedView != null) { + cancelAnimation(); + } else { + touchSlopExeeded = false; + } + startSwiping(targetView); + initialTouchX = x; + initialTouchY = y; + translationOnDown = translation; + initVelocityTracker(); + trackMovement(event); + motionCancelled = false; + break; + case MotionEvent.ACTION_POINTER_DOWN: + motionCancelled = true; + endMotion(true /* forceSnapBack */, x, y); + break; + case MotionEvent.ACTION_MOVE: + trackMovement(event); + float xDist = x - initialTouchX; + float yDist = y - initialTouchY; + float distance = (float) Math.hypot(xDist, yDist); + if (!touchSlopExeeded && distance > touchSlop) { + touchSlopExeeded = true; + } + if (swipingInProgress) { + if (targetedView == rightIcon) { + distance = translationOnDown - distance; + distance = Math.min(0, distance); + } else { + distance = translationOnDown + distance; + distance = Math.max(0, distance); + } + setTranslation(distance, false /* isReset */, false /* animateReset */); + } + break; + + case MotionEvent.ACTION_UP: + isUp = true; + //fallthrough_intended + case MotionEvent.ACTION_CANCEL: + boolean hintOnTheRight = targetedView == rightIcon; + trackMovement(event); + endMotion(!isUp, x, y); + if (!touchSlopExeeded && isUp) { + callback.onIconClicked(hintOnTheRight); + } + break; + } + return true; + } + + private void startSwiping(View targetView) { + callback.onSwipingStarted(targetView == rightIcon); + swipingInProgress = true; + targetedView = targetView; + } + + private View getIconAtPosition(float x, float y) { + if (leftSwipePossible() && isOnIcon(leftIcon, x, y)) { + return leftIcon; + } + if (rightSwipePossible() && isOnIcon(rightIcon, x, y)) { + return rightIcon; + } + return null; + } + + public boolean isOnAffordanceIcon(float x, float y) { + return isOnIcon(leftIcon, x, y) || isOnIcon(rightIcon, x, y); + } + + private boolean isOnIcon(View icon, float x, float y) { + float iconX = icon.getX() + icon.getWidth() / 2.0f; + float iconY = icon.getY() + icon.getHeight() / 2.0f; + double distance = Math.hypot(x - iconX, y - iconY); + return distance <= touchTargetSize / 2; + } + + private void endMotion(boolean forceSnapBack, float lastX, float lastY) { + if (swipingInProgress) { + flingWithCurrentVelocity(forceSnapBack, lastX, lastY); + } else { + targetedView = null; + } + if (velocityTracker != null) { + velocityTracker.recycle(); + velocityTracker = null; + } + } + + private boolean rightSwipePossible() { + return rightIcon != null && rightIcon.getVisibility() == View.VISIBLE; + } + + private boolean leftSwipePossible() { + return leftIcon != null && leftIcon.getVisibility() == View.VISIBLE; + } + + public void startHintAnimation(boolean right, @Nullable Runnable onFinishedListener) { + cancelAnimation(); + startHintAnimationPhase1(right, onFinishedListener); + } + + private void startHintAnimationPhase1( + final boolean right, @Nullable final Runnable onFinishedListener) { + final SwipeButtonView targetView = right ? rightIcon : leftIcon; + ValueAnimator animator = getAnimatorToRadius(right, hintGrowAmount); + if (animator == null) { + if (onFinishedListener != null) { + onFinishedListener.run(); + } + return; + } + animator.addListener( + new AnimatorListenerAdapter() { + private boolean mCancelled; + + @Override + public void onAnimationCancel(Animator animation) { + mCancelled = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + if (mCancelled) { + swipeAnimator = null; + targetedView = null; + if (onFinishedListener != null) { + onFinishedListener.run(); + } + } else { + startUnlockHintAnimationPhase2(right, onFinishedListener); + } + } + }); + animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); + animator.setDuration(HINT_PHASE1_DURATION); + animator.start(); + swipeAnimator = animator; + targetedView = targetView; + } + + /** Phase 2: Move back. */ + private void startUnlockHintAnimationPhase2( + boolean right, @Nullable final Runnable onFinishedListener) { + ValueAnimator animator = getAnimatorToRadius(right, 0); + if (animator == null) { + if (onFinishedListener != null) { + onFinishedListener.run(); + } + return; + } + animator.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + swipeAnimator = null; + targetedView = null; + if (onFinishedListener != null) { + onFinishedListener.run(); + } + } + }); + animator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); + animator.setDuration(HINT_PHASE2_DURATION); + animator.setStartDelay(HINT_CIRCLE_OPEN_DURATION); + animator.start(); + swipeAnimator = animator; + } + + private ValueAnimator getAnimatorToRadius(final boolean right, int radius) { + final SwipeButtonView targetView = right ? rightIcon : leftIcon; + if (targetView == null) { + return null; + } + ValueAnimator animator = ValueAnimator.ofFloat(targetView.getCircleRadius(), radius); + animator.addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float newRadius = (float) animation.getAnimatedValue(); + targetView.setCircleRadiusWithoutAnimation(newRadius); + float translation = getTranslationFromRadius(newRadius); + SwipeButtonHelper.this.translation = right ? -translation : translation; + updateIconsFromTranslation(targetView); + } + }); + return animator; + } + + private void cancelAnimation() { + if (swipeAnimator != null) { + swipeAnimator.cancel(); + } + } + + private void flingWithCurrentVelocity(boolean forceSnapBack, float lastX, float lastY) { + float vel = getCurrentVelocity(lastX, lastY); + + // We snap back if the current translation is not far enough + boolean snapBack = isBelowFalsingThreshold(); + + // or if the velocity is in the opposite direction. + boolean velIsInWrongDirection = vel * translation < 0; + snapBack |= Math.abs(vel) > minFlingVelocity && velIsInWrongDirection; + vel = snapBack ^ velIsInWrongDirection ? 0 : vel; + fling(vel, snapBack || forceSnapBack, translation < 0); + } + + private boolean isBelowFalsingThreshold() { + return Math.abs(translation) < Math.abs(translationOnDown) + getMinTranslationAmount(); + } + + private int getMinTranslationAmount() { + float factor = callback.getAffordanceFalsingFactor(); + return (int) (minTranslationAmount * factor); + } + + private void fling(float vel, final boolean snapBack, boolean right) { + float target = + right ? -callback.getMaxTranslationDistance() : callback.getMaxTranslationDistance(); + target = snapBack ? 0 : target; + + ValueAnimator animator = ValueAnimator.ofFloat(translation, target); + flingAnimationUtils.apply(animator, translation, target, vel); + animator.addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + translation = (float) animation.getAnimatedValue(); + } + }); + animator.addListener(flingEndListener); + if (!snapBack) { + startFinishingCircleAnimation(vel * 0.375f, animationEndRunnable, right); + callback.onAnimationToSideStarted(right, translation, vel); + } else { + reset(true); + } + animator.start(); + swipeAnimator = animator; + if (snapBack) { + callback.onSwipingAborted(); + } + } + + private void startFinishingCircleAnimation( + float velocity, Runnable mAnimationEndRunnable, boolean right) { + SwipeButtonView targetView = right ? rightIcon : leftIcon; + if (targetView != null) { + targetView.finishAnimation(velocity, mAnimationEndRunnable); + } + } + + private void setTranslation(float translation, boolean isReset, boolean animateReset) { + translation = rightSwipePossible() ? translation : Math.max(0, translation); + translation = leftSwipePossible() ? translation : Math.min(0, translation); + float absTranslation = Math.abs(translation); + if (translation != this.translation || isReset) { + SwipeButtonView targetView = translation > 0 ? leftIcon : rightIcon; + SwipeButtonView otherView = translation > 0 ? rightIcon : leftIcon; + float alpha = absTranslation / getMinTranslationAmount(); + + // We interpolate the alpha of the other icons to 0 + float fadeOutAlpha = 1.0f - alpha; + fadeOutAlpha = Math.max(fadeOutAlpha, 0.0f); + + boolean animateIcons = isReset && animateReset; + boolean forceNoCircleAnimation = isReset && !animateReset; + float radius = getRadiusFromTranslation(absTranslation); + boolean slowAnimation = isReset && isBelowFalsingThreshold(); + if (targetView != null) { + if (!isReset) { + updateIcon( + targetView, + radius, + alpha + fadeOutAlpha * targetView.getRestingAlpha(), + false, + false, + false, + false); + } else { + updateIcon( + targetView, + 0.0f, + fadeOutAlpha * targetView.getRestingAlpha(), + animateIcons, + slowAnimation, + false, + forceNoCircleAnimation); + } + } + if (otherView != null) { + updateIcon( + otherView, + 0.0f, + fadeOutAlpha * otherView.getRestingAlpha(), + animateIcons, + slowAnimation, + false, + forceNoCircleAnimation); + } + + this.translation = translation; + } + } + + private void updateIconsFromTranslation(SwipeButtonView targetView) { + float absTranslation = Math.abs(translation); + float alpha = absTranslation / getMinTranslationAmount(); + + // We interpolate the alpha of the other icons to 0 + float fadeOutAlpha = 1.0f - alpha; + fadeOutAlpha = Math.max(0.0f, fadeOutAlpha); + + // We interpolate the alpha of the targetView to 1 + SwipeButtonView otherView = targetView == rightIcon ? leftIcon : rightIcon; + updateIconAlpha(targetView, alpha + fadeOutAlpha * targetView.getRestingAlpha(), false); + if (otherView != null) { + updateIconAlpha(otherView, fadeOutAlpha * otherView.getRestingAlpha(), false); + } + } + + private float getTranslationFromRadius(float circleSize) { + float translation = (circleSize - minBackgroundRadius) / BACKGROUND_RADIUS_SCALE_FACTOR; + return translation > 0.0f ? translation + touchSlop : 0.0f; + } + + private float getRadiusFromTranslation(float translation) { + if (translation <= touchSlop) { + return 0.0f; + } + return (translation - touchSlop) * BACKGROUND_RADIUS_SCALE_FACTOR + minBackgroundRadius; + } + + public void animateHideLeftRightIcon() { + cancelAnimation(); + updateIcon(rightIcon, 0f, 0f, true, false, false, false); + updateIcon(leftIcon, 0f, 0f, true, false, false, false); + } + + private void updateIcon( + @Nullable SwipeButtonView view, + float circleRadius, + float alpha, + boolean animate, + boolean slowRadiusAnimation, + boolean force, + boolean forceNoCircleAnimation) { + if (view == null) { + return; + } + if (view.getVisibility() != View.VISIBLE && !force) { + return; + } + if (forceNoCircleAnimation) { + view.setCircleRadiusWithoutAnimation(circleRadius); + } else { + view.setCircleRadius(circleRadius, slowRadiusAnimation); + } + updateIconAlpha(view, alpha, animate); + } + + private void updateIconAlpha(SwipeButtonView view, float alpha, boolean animate) { + float scale = getScale(alpha, view); + alpha = Math.min(1.0f, alpha); + view.setImageAlpha(alpha, animate); + view.setImageScale(scale, animate); + } + + private float getScale(float alpha, SwipeButtonView icon) { + float scale = alpha / icon.getRestingAlpha() * 0.2f + SwipeButtonView.MIN_ICON_SCALE_AMOUNT; + return Math.min(scale, SwipeButtonView.MAX_ICON_SCALE_AMOUNT); + } + + private void trackMovement(MotionEvent event) { + if (velocityTracker != null) { + velocityTracker.addMovement(event); + } + } + + private void initVelocityTracker() { + if (velocityTracker != null) { + velocityTracker.recycle(); + } + velocityTracker = VelocityTracker.obtain(); + } + + private float getCurrentVelocity(float lastX, float lastY) { + if (velocityTracker == null) { + return 0; + } + velocityTracker.computeCurrentVelocity(1000); + float aX = velocityTracker.getXVelocity(); + float aY = velocityTracker.getYVelocity(); + float bX = lastX - initialTouchX; + float bY = lastY - initialTouchY; + float bLen = (float) Math.hypot(bX, bY); + // Project the velocity onto the distance vector: a * b / |b| + float projectedVelocity = (aX * bX + aY * bY) / bLen; + if (targetedView == rightIcon) { + projectedVelocity = -projectedVelocity; + } + return projectedVelocity; + } + + public void onConfigurationChanged() { + initDimens(); + initIcons(); + } + + public void onRtlPropertiesChanged() { + initIcons(); + } + + public void reset(boolean animate) { + cancelAnimation(); + setTranslation(0.0f, true, animate); + motionCancelled = true; + if (swipingInProgress) { + callback.onSwipingAborted(); + swipingInProgress = false; + } + } + + public boolean isSwipingInProgress() { + return swipingInProgress; + } + + public void launchAffordance(boolean animate, boolean left) { + SwipeButtonView targetView = left ? leftIcon : rightIcon; + if (swipingInProgress || targetView == null) { + // We don't want to mess with the state if the user is actually swiping already. + return; + } + SwipeButtonView otherView = left ? rightIcon : leftIcon; + startSwiping(targetView); + if (animate) { + fling(0, false, !left); + updateIcon(otherView, 0.0f, 0, true, false, true, false); + } else { + callback.onAnimationToSideStarted(!left, translation, 0); + translation = + left ? callback.getMaxTranslationDistance() : callback.getMaxTranslationDistance(); + updateIcon(otherView, 0.0f, 0.0f, false, false, true, false); + targetView.instantFinishAnimation(); + flingEndListener.onAnimationEnd(null); + animationEndRunnable.run(); + } + } + + /** Callback interface for various actions */ + 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 translation, float vel); + + /** Notifies the callback the animation to a side page has ended. */ + void onAnimationToSideEnded(); + + float getMaxTranslationDistance(); + + void onSwipingStarted(boolean rightIcon); + + void onSwipingAborted(); + + void onIconClicked(boolean rightIcon); + + @Nullable + SwipeButtonView getLeftIcon(); + + @Nullable + SwipeButtonView getRightIcon(); + + @Nullable + View getLeftPreview(); + + @Nullable + View getRightPreview(); + + /** @return The factor the minimum swipe amount should be multiplied with. */ + float getAffordanceFalsingFactor(); + } +} |