Using common fling detection logic for notification and all-apps
> Refactoring SwipeDetector to both allow vertical and horizontal swipes > Using SwipeDetector and common overscroll effect for notification swipes instead of a separate logic Change-Id: Ib706ee179811ade59ddb68184e1c202365d147c4
This commit is contained in:
@@ -48,6 +48,7 @@ import android.view.animation.Interpolator;
|
||||
|
||||
import com.android.launcher3.anim.PropertyListBuilder;
|
||||
import com.android.launcher3.pageindicators.PageIndicator;
|
||||
import com.android.launcher3.touch.OverScroll;
|
||||
import com.android.launcher3.util.Themes;
|
||||
import com.android.launcher3.util.Thunk;
|
||||
|
||||
@@ -68,10 +69,8 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc
|
||||
public static final int PAGE_SNAP_ANIMATION_DURATION = 750;
|
||||
protected static final int SLOW_PAGE_SNAP_ANIMATION_DURATION = 950;
|
||||
|
||||
// Overscroll constants
|
||||
// OverScroll constants
|
||||
private final static int OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION = 270;
|
||||
private static final float OVERSCROLL_ACCELERATE_FACTOR = 2;
|
||||
private static final float OVERSCROLL_DAMP_FACTOR = 0.07f;
|
||||
|
||||
private static final float RETURN_TO_ORIGINAL_PAGE_THRESHOLD = 0.33f;
|
||||
// The page is moved more than halfway, automatically move to the next page on touch up.
|
||||
@@ -188,7 +187,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc
|
||||
// Convenience/caching
|
||||
private static final Matrix sTmpInvMatrix = new Matrix();
|
||||
private static final float[] sTmpPoint = new float[2];
|
||||
private static final int[] sTmpIntPoint = new int[2];
|
||||
private static final Rect sTmpRect = new Rect();
|
||||
|
||||
protected final Rect mInsets = new Rect();
|
||||
@@ -233,8 +231,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc
|
||||
mMinSnapVelocity = (int) (MIN_SNAP_VELOCITY * density);
|
||||
setOnHierarchyChangeListener(this);
|
||||
setWillNotDraw(false);
|
||||
|
||||
int edgeEffectColor = Themes.getAttrColor(getContext(), android.R.attr.colorEdgeEffect);
|
||||
}
|
||||
|
||||
protected void setDefaultInterpolator(Interpolator interpolator) {
|
||||
@@ -1305,29 +1301,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc
|
||||
}
|
||||
}
|
||||
|
||||
// This curve determines how the effect of scrolling over the limits of the page dimishes
|
||||
// as the user pulls further and further from the bounds
|
||||
private float overScrollInfluenceCurve(float f) {
|
||||
f -= 1.0f;
|
||||
return f * f * f + 1.0f;
|
||||
}
|
||||
|
||||
protected float acceleratedOverFactor(float amount) {
|
||||
int screenSize = getViewportWidth();
|
||||
|
||||
// We want to reach the max over scroll effect when the user has
|
||||
// over scrolled half the size of the screen
|
||||
float f = OVERSCROLL_ACCELERATE_FACTOR * (amount / screenSize);
|
||||
|
||||
if (Float.compare(f, 0f) == 0) return 0;
|
||||
|
||||
// Clamp this factor, f, to -1 < f < 1
|
||||
if (Math.abs(f) >= 1) {
|
||||
f /= Math.abs(f);
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
// While layout transitions are occurring, a child's position may stray from its baseline
|
||||
// position. This method returns the magnitude of this stray at any given time.
|
||||
public int getLayoutTransitionOffsetForPage(int index) {
|
||||
@@ -1348,20 +1321,9 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc
|
||||
}
|
||||
|
||||
protected void dampedOverScroll(float amount) {
|
||||
int screenSize = getViewportWidth();
|
||||
if (Float.compare(amount, 0f) == 0) return;
|
||||
|
||||
float f = (amount / screenSize);
|
||||
|
||||
if (Float.compare(f, 0f) == 0) return;
|
||||
|
||||
f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f)));
|
||||
|
||||
// Clamp this factor, f, to -1 < f < 1
|
||||
if (Math.abs(f) >= 1) {
|
||||
f /= Math.abs(f);
|
||||
}
|
||||
|
||||
int overScrollAmount = (int) Math.round(OVERSCROLL_DAMP_FACTOR * f * screenSize);
|
||||
int overScrollAmount = OverScroll.dampedScroll(amount, getViewportWidth());
|
||||
if (amount < 0) {
|
||||
mOverScrollX = overScrollAmount;
|
||||
super.scrollTo(mOverScrollX, getScrollY());
|
||||
@@ -1376,14 +1338,6 @@ public abstract class PagedView extends ViewGroup implements ViewGroup.OnHierarc
|
||||
dampedOverScroll(amount);
|
||||
}
|
||||
|
||||
protected float maxOverScroll() {
|
||||
// Using the formula in overScroll, assuming that f = 1.0 (which it should generally not
|
||||
// exceed). Used to find out how much extra wallpaper we need for the over scroll effect
|
||||
float f = 1.0f;
|
||||
f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f)));
|
||||
return OVERSCROLL_DAMP_FACTOR * f;
|
||||
}
|
||||
|
||||
/**
|
||||
* return true if freescroll has been enabled, false otherwise
|
||||
*/
|
||||
|
||||
@@ -50,8 +50,7 @@ public class ShortcutMenuAccessibilityDelegate extends LauncherAccessibilityDele
|
||||
if ((host.getParent() instanceof DeepShortcutView)) {
|
||||
info.addAction(mActions.get(ADD_TO_WORKSPACE));
|
||||
} else if (host instanceof NotificationMainView) {
|
||||
NotificationMainView notificationView = (NotificationMainView) host;
|
||||
if (notificationView.canChildBeDismissed(notificationView)) {
|
||||
if (((NotificationMainView) host).canChildBeDismissed()) {
|
||||
info.addAction(mActions.get(DISMISS_NOTIFICATION));
|
||||
}
|
||||
}
|
||||
@@ -88,8 +87,7 @@ public class ShortcutMenuAccessibilityDelegate extends LauncherAccessibilityDele
|
||||
if (!(host instanceof NotificationMainView)) {
|
||||
return false;
|
||||
}
|
||||
NotificationMainView notificationView = (NotificationMainView) host;
|
||||
notificationView.onChildDismissed(notificationView);
|
||||
((NotificationMainView) host).onChildDismissed();
|
||||
announceConfirmation(R.string.notification_dismissed);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import com.android.launcher3.anim.SpringAnimationHandler;
|
||||
import com.android.launcher3.config.FeatureFlags;
|
||||
import com.android.launcher3.graphics.DrawableFactory;
|
||||
import com.android.launcher3.logging.UserEventDispatcher.LogContainerProvider;
|
||||
import com.android.launcher3.touch.OverScroll;
|
||||
import com.android.launcher3.touch.SwipeDetector;
|
||||
import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
|
||||
import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
|
||||
@@ -98,8 +99,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView implements LogContaine
|
||||
R.dimen.all_apps_empty_search_bg_top_offset);
|
||||
|
||||
mOverScrollHelper = new OverScrollHelper();
|
||||
mPullDetector = new SwipeDetector(getContext());
|
||||
mPullDetector.setListener(mOverScrollHelper);
|
||||
mPullDetector = new SwipeDetector(getContext(), mOverScrollHelper, SwipeDetector.VERTICAL);
|
||||
mPullDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_BOTH, true);
|
||||
}
|
||||
|
||||
@@ -564,37 +564,7 @@ public class AllAppsRecyclerView extends BaseRecyclerView implements LogContaine
|
||||
}
|
||||
|
||||
private float getDampedOverScroll(float y) {
|
||||
return dampedOverScroll(y, getHeight()) * MAX_OVERSCROLL_PERCENTAGE;
|
||||
}
|
||||
|
||||
/**
|
||||
* This curve determines how the effect of scrolling over the limits of the page diminishes
|
||||
* as the user pulls further and further from the bounds
|
||||
*
|
||||
* @param f The percentage of how much the user has overscrolled.
|
||||
* @return A transformed percentage based on the influence curve.
|
||||
*/
|
||||
private float overScrollInfluenceCurve(float f) {
|
||||
f -= 1.0f;
|
||||
return f * f * f + 1.0f;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param amount The original amount overscrolled.
|
||||
* @param max The maximum amount that the View can overscroll.
|
||||
* @return The dampened overscroll amount.
|
||||
*/
|
||||
private float dampedOverScroll(float amount, float max) {
|
||||
float f = amount / max;
|
||||
if (Float.compare(f, 0) == 0) return 0;
|
||||
f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f)));
|
||||
|
||||
// Clamp this factor, f, to -1 < f < 1
|
||||
if (Math.abs(f) >= 1) {
|
||||
f /= Math.abs(f);
|
||||
}
|
||||
|
||||
return Math.round(f * max);
|
||||
return OverScroll.dampedScroll(y, getHeight());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,8 +107,7 @@ public class AllAppsTransitionController implements TouchController, SwipeDetect
|
||||
|
||||
public AllAppsTransitionController(Launcher l) {
|
||||
mLauncher = l;
|
||||
mDetector = new SwipeDetector(l);
|
||||
mDetector.setListener(this);
|
||||
mDetector = new SwipeDetector(l, this, SwipeDetector.VERTICAL);
|
||||
mShiftRange = DEFAULT_SHIFT_RANGE;
|
||||
mProgress = 1f;
|
||||
|
||||
@@ -137,15 +136,15 @@ public class AllAppsTransitionController implements TouchController, SwipeDetect
|
||||
|
||||
if (mDetector.isIdleState()) {
|
||||
if (mLauncher.isAllAppsVisible()) {
|
||||
directionsToDetectScroll |= SwipeDetector.DIRECTION_DOWN;
|
||||
directionsToDetectScroll |= SwipeDetector.DIRECTION_NEGATIVE;
|
||||
} else {
|
||||
directionsToDetectScroll |= SwipeDetector.DIRECTION_UP;
|
||||
directionsToDetectScroll |= SwipeDetector.DIRECTION_POSITIVE;
|
||||
}
|
||||
} else {
|
||||
if (isInDisallowRecatchBottomZone()) {
|
||||
directionsToDetectScroll |= SwipeDetector.DIRECTION_UP;
|
||||
directionsToDetectScroll |= SwipeDetector.DIRECTION_POSITIVE;
|
||||
} else if (isInDisallowRecatchTopZone()) {
|
||||
directionsToDetectScroll |= SwipeDetector.DIRECTION_DOWN;
|
||||
directionsToDetectScroll |= SwipeDetector.DIRECTION_NEGATIVE;
|
||||
} else {
|
||||
directionsToDetectScroll |= SwipeDetector.DIRECTION_BOTH;
|
||||
ignoreSlopWhenSettling = true;
|
||||
@@ -368,7 +367,7 @@ public class AllAppsTransitionController implements TouchController, SwipeDetect
|
||||
}
|
||||
|
||||
private void calculateDuration(float velocity, float disp) {
|
||||
mAnimationDuration = mDetector.calculateDuration(velocity, disp / mShiftRange);
|
||||
mAnimationDuration = SwipeDetector.calculateDuration(velocity, disp / mShiftRange);
|
||||
}
|
||||
|
||||
public boolean animateToAllApps(AnimatorSet animationOut, long duration) {
|
||||
|
||||
@@ -1,356 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.launcher3.notification;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.content.Context;
|
||||
import android.view.ViewPropertyAnimator;
|
||||
import android.view.animation.Interpolator;
|
||||
import android.view.animation.PathInterpolator;
|
||||
|
||||
/**
|
||||
* Utility class to calculate general fling animation when the finger is released.
|
||||
*
|
||||
* This class was copied from com.android.systemui.statusbar.
|
||||
*/
|
||||
public class FlingAnimationUtils {
|
||||
|
||||
private static final float LINEAR_OUT_SLOW_IN_X2 = 0.35f;
|
||||
private static final float LINEAR_OUT_SLOW_IN_X2_MAX = 0.68f;
|
||||
private static final float LINEAR_OUT_FASTER_IN_X2 = 0.5f;
|
||||
private static final float LINEAR_OUT_FASTER_IN_Y2_MIN = 0.4f;
|
||||
private static final float LINEAR_OUT_FASTER_IN_Y2_MAX = 0.5f;
|
||||
private static final float MIN_VELOCITY_DP_PER_SECOND = 250;
|
||||
private static final float HIGH_VELOCITY_DP_PER_SECOND = 3000;
|
||||
|
||||
private static final float LINEAR_OUT_SLOW_IN_START_GRADIENT = 0.75f;
|
||||
private final float mSpeedUpFactor;
|
||||
private final float mY2;
|
||||
|
||||
private float mMinVelocityPxPerSecond;
|
||||
private float mMaxLengthSeconds;
|
||||
private float mHighVelocityPxPerSecond;
|
||||
private float mLinearOutSlowInX2;
|
||||
|
||||
private AnimatorProperties mAnimatorProperties = new AnimatorProperties();
|
||||
private PathInterpolator mInterpolator;
|
||||
private float mCachedStartGradient = -1;
|
||||
private float mCachedVelocityFactor = -1;
|
||||
|
||||
public FlingAnimationUtils(Context ctx, float maxLengthSeconds) {
|
||||
this(ctx, maxLengthSeconds, 0.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param maxLengthSeconds the longest duration an animation can become in seconds
|
||||
* @param speedUpFactor a factor from 0 to 1 how much the slow down should be shifted towards
|
||||
* the end of the animation. 0 means it's at the beginning and no
|
||||
* acceleration will take place.
|
||||
*/
|
||||
public FlingAnimationUtils(Context ctx, float maxLengthSeconds, float speedUpFactor) {
|
||||
this(ctx, maxLengthSeconds, speedUpFactor, -1.0f, 1.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param maxLengthSeconds the longest duration an animation can become in seconds
|
||||
* @param speedUpFactor a factor from 0 to 1 how much the slow down should be shifted towards
|
||||
* the end of the animation. 0 means it's at the beginning and no
|
||||
* acceleration will take place.
|
||||
* @param x2 the x value to take for the second point of the bezier spline. If a value below 0
|
||||
* is provided, the value is automatically calculated.
|
||||
* @param y2 the y value to take for the second point of the bezier spline
|
||||
*/
|
||||
public FlingAnimationUtils(Context ctx, float maxLengthSeconds, float speedUpFactor, float x2,
|
||||
float y2) {
|
||||
mMaxLengthSeconds = maxLengthSeconds;
|
||||
mSpeedUpFactor = speedUpFactor;
|
||||
if (x2 < 0) {
|
||||
mLinearOutSlowInX2 = interpolate(LINEAR_OUT_SLOW_IN_X2,
|
||||
LINEAR_OUT_SLOW_IN_X2_MAX,
|
||||
mSpeedUpFactor);
|
||||
} else {
|
||||
mLinearOutSlowInX2 = x2;
|
||||
}
|
||||
mY2 = y2;
|
||||
|
||||
mMinVelocityPxPerSecond
|
||||
= MIN_VELOCITY_DP_PER_SECOND * ctx.getResources().getDisplayMetrics().density;
|
||||
mHighVelocityPxPerSecond
|
||||
= HIGH_VELOCITY_DP_PER_SECOND * ctx.getResources().getDisplayMetrics().density;
|
||||
}
|
||||
|
||||
private static float interpolate(float start, float end, float amount) {
|
||||
return start * (1.0f - amount) + end * amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public void apply(Animator 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
|
||||
*/
|
||||
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(Animator 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 velocityFactor = mSpeedUpFactor == 0.0f
|
||||
? 1.0f : Math.min(velAbs / HIGH_VELOCITY_DP_PER_SECOND, 1.0f);
|
||||
float startGradient = interpolate(LINEAR_OUT_SLOW_IN_START_GRADIENT,
|
||||
mY2 / mLinearOutSlowInX2, velocityFactor);
|
||||
float durationSeconds = startGradient * diff / velAbs;
|
||||
Interpolator slowInInterpolator = getInterpolator(startGradient, velocityFactor);
|
||||
if (durationSeconds <= maxLengthSeconds) {
|
||||
mAnimatorProperties.interpolator = slowInInterpolator;
|
||||
} else if (velAbs >= mMinVelocityPxPerSecond) {
|
||||
|
||||
// Cross fade between fast-out-slow-in and linear interpolator with current velocity.
|
||||
durationSeconds = maxLengthSeconds;
|
||||
VelocityInterpolator velocityInterpolator
|
||||
= new VelocityInterpolator(durationSeconds, velAbs, diff);
|
||||
InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator(
|
||||
velocityInterpolator, slowInInterpolator, Interpolators.LINEAR_OUT_SLOW_IN);
|
||||
mAnimatorProperties.interpolator = superInterpolator;
|
||||
} else {
|
||||
|
||||
// Just use a normal interpolator which doesn't take the velocity into account.
|
||||
durationSeconds = maxLengthSeconds;
|
||||
mAnimatorProperties.interpolator = Interpolators.FAST_OUT_SLOW_IN;
|
||||
}
|
||||
mAnimatorProperties.duration = (long) (durationSeconds * 1000);
|
||||
return mAnimatorProperties;
|
||||
}
|
||||
|
||||
private Interpolator getInterpolator(float startGradient, float velocityFactor) {
|
||||
if (startGradient != mCachedStartGradient
|
||||
|| velocityFactor != mCachedVelocityFactor) {
|
||||
float speedup = mSpeedUpFactor * (1.0f - velocityFactor);
|
||||
mInterpolator = new PathInterpolator(speedup,
|
||||
speedup * startGradient,
|
||||
mLinearOutSlowInX2, mY2);
|
||||
mCachedStartGradient = startGradient;
|
||||
mCachedVelocityFactor = velocityFactor;
|
||||
}
|
||||
return mInterpolator;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(Animator 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);
|
||||
float velAbs = Math.abs(velocity);
|
||||
float y2 = calculateLinearOutFasterInY2(velAbs);
|
||||
|
||||
float startGradient = y2 / LINEAR_OUT_FASTER_IN_X2;
|
||||
Interpolator mLinearOutFasterIn = new PathInterpolator(0, 0, LINEAR_OUT_FASTER_IN_X2, y2);
|
||||
float durationSeconds = startGradient * diff / velAbs;
|
||||
if (durationSeconds <= maxLengthSeconds) {
|
||||
mAnimatorProperties.interpolator = mLinearOutFasterIn;
|
||||
} else if (velAbs >= mMinVelocityPxPerSecond) {
|
||||
|
||||
// Cross fade between linear-out-faster-in and linear interpolator with current
|
||||
// velocity.
|
||||
durationSeconds = maxLengthSeconds;
|
||||
VelocityInterpolator velocityInterpolator
|
||||
= new VelocityInterpolator(durationSeconds, velAbs, diff);
|
||||
InterpolatorInterpolator superInterpolator = new InterpolatorInterpolator(
|
||||
velocityInterpolator, mLinearOutFasterIn, Interpolators.LINEAR_OUT_SLOW_IN);
|
||||
mAnimatorProperties.interpolator = superInterpolator;
|
||||
} else {
|
||||
|
||||
// Just use a normal interpolator which doesn't take the velocity into account.
|
||||
durationSeconds = maxLengthSeconds;
|
||||
mAnimatorProperties.interpolator = Interpolators.FAST_OUT_LINEAR_IN;
|
||||
}
|
||||
mAnimatorProperties.duration = (long) (durationSeconds * 1000);
|
||||
return mAnimatorProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the y2 control point for a linear-out-faster-in path interpolator depending on the
|
||||
* velocity. The faster the velocity, the more "linear" the interpolator gets.
|
||||
*
|
||||
* @param velocity the velocity of the gesture.
|
||||
* @return the y2 control point for a cubic bezier path interpolator
|
||||
*/
|
||||
private float calculateLinearOutFasterInY2(float velocity) {
|
||||
float t = (velocity - mMinVelocityPxPerSecond)
|
||||
/ (mHighVelocityPxPerSecond - mMinVelocityPxPerSecond);
|
||||
t = Math.max(0, Math.min(1, t));
|
||||
return (1 - t) * LINEAR_OUT_FASTER_IN_Y2_MIN + t * LINEAR_OUT_FASTER_IN_Y2_MAX;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the minimum velocity a gesture needs to have to be considered a fling
|
||||
*/
|
||||
public float getMinVelocityPxPerSecond() {
|
||||
return mMinVelocityPxPerSecond;
|
||||
}
|
||||
|
||||
/**
|
||||
* An interpolator which interpolates two interpolators with an interpolator.
|
||||
*/
|
||||
private static final class InterpolatorInterpolator implements Interpolator {
|
||||
|
||||
private Interpolator mInterpolator1;
|
||||
private Interpolator mInterpolator2;
|
||||
private Interpolator mCrossfader;
|
||||
|
||||
InterpolatorInterpolator(Interpolator interpolator1, Interpolator interpolator2,
|
||||
Interpolator crossfader) {
|
||||
mInterpolator1 = interpolator1;
|
||||
mInterpolator2 = interpolator2;
|
||||
mCrossfader = crossfader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getInterpolation(float input) {
|
||||
float t = mCrossfader.getInterpolation(input);
|
||||
return (1 - t) * mInterpolator1.getInterpolation(input)
|
||||
+ t * mInterpolator2.getInterpolation(input);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An interpolator which interpolates with a fixed velocity.
|
||||
*/
|
||||
private static final class VelocityInterpolator implements Interpolator {
|
||||
|
||||
private float mDurationSeconds;
|
||||
private float mVelocity;
|
||||
private float mDiff;
|
||||
|
||||
private VelocityInterpolator(float durationSeconds, float velocity, float diff) {
|
||||
mDurationSeconds = durationSeconds;
|
||||
mVelocity = velocity;
|
||||
mDiff = diff;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getInterpolation(float input) {
|
||||
float time = input * mDurationSeconds;
|
||||
return time * mVelocity / mDiff;
|
||||
}
|
||||
}
|
||||
|
||||
private static class AnimatorProperties {
|
||||
Interpolator interpolator;
|
||||
long duration;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
|
||||
import com.android.launcher3.graphics.IconPalette;
|
||||
import com.android.launcher3.logging.UserEventDispatcher.LogContainerProvider;
|
||||
import com.android.launcher3.popup.PopupItemView;
|
||||
import com.android.launcher3.touch.SwipeDetector;
|
||||
import com.android.launcher3.userevent.nano.LauncherLogProto;
|
||||
import com.android.launcher3.util.Themes;
|
||||
|
||||
@@ -56,7 +57,7 @@ public class NotificationItemView extends PopupItemView implements LogContainerP
|
||||
private TextView mHeaderCount;
|
||||
private NotificationMainView mMainView;
|
||||
private NotificationFooterLayout mFooter;
|
||||
private SwipeHelper mSwipeHelper;
|
||||
private SwipeDetector mSwipeDetector;
|
||||
private boolean mAnimatingNextIcon;
|
||||
private int mNotificationHeaderTextColor = Notification.COLOR_DEFAULT;
|
||||
|
||||
@@ -75,12 +76,14 @@ public class NotificationItemView extends PopupItemView implements LogContainerP
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
mHeaderText = (TextView) findViewById(R.id.notification_text);
|
||||
mHeaderCount = (TextView) findViewById(R.id.notification_count);
|
||||
mMainView = (NotificationMainView) findViewById(R.id.main_view);
|
||||
mFooter = (NotificationFooterLayout) findViewById(R.id.footer);
|
||||
mSwipeHelper = new SwipeHelper(SwipeHelper.X, mMainView, getContext());
|
||||
mSwipeHelper.setDisableHardwareLayers(true);
|
||||
mHeaderText = findViewById(R.id.notification_text);
|
||||
mHeaderCount = findViewById(R.id.notification_count);
|
||||
mMainView = findViewById(R.id.main_view);
|
||||
mFooter = findViewById(R.id.footer);
|
||||
|
||||
mSwipeDetector = new SwipeDetector(getContext(), mMainView, SwipeDetector.HORIZONTAL);
|
||||
mSwipeDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_BOTH, false);
|
||||
mMainView.setSwipeDetector(mSwipeDetector);
|
||||
}
|
||||
|
||||
public NotificationMainView getMainView() {
|
||||
@@ -136,7 +139,8 @@ public class NotificationItemView extends PopupItemView implements LogContainerP
|
||||
return false;
|
||||
}
|
||||
getParent().requestDisallowInterceptTouchEvent(true);
|
||||
return mSwipeHelper.onInterceptTouchEvent(ev);
|
||||
mSwipeDetector.onTouchEvent(ev);
|
||||
return mSwipeDetector.isDraggingOrSettling();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -145,7 +149,7 @@ public class NotificationItemView extends PopupItemView implements LogContainerP
|
||||
// The notification hasn't been populated yet.
|
||||
return false;
|
||||
}
|
||||
return mSwipeHelper.onTouchEvent(ev) || super.onTouchEvent(ev);
|
||||
return mSwipeDetector.onTouchEvent(ev) || super.onTouchEvent(ev);
|
||||
}
|
||||
|
||||
public void applyNotificationInfos(final List<NotificationInfo> notificationInfos) {
|
||||
|
||||
@@ -23,15 +23,17 @@ import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.RippleDrawable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewPropertyAnimator;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.launcher3.ItemInfo;
|
||||
import com.android.launcher3.Launcher;
|
||||
import com.android.launcher3.R;
|
||||
import com.android.launcher3.touch.OverScroll;
|
||||
import com.android.launcher3.touch.SwipeDetector;
|
||||
import com.android.launcher3.userevent.nano.LauncherLogProto;
|
||||
import com.android.launcher3.util.Themes;
|
||||
|
||||
@@ -39,7 +41,7 @@ import com.android.launcher3.util.Themes;
|
||||
* A {@link android.widget.FrameLayout} that contains a single notification,
|
||||
* e.g. icon + title + text.
|
||||
*/
|
||||
public class NotificationMainView extends FrameLayout implements SwipeHelper.Callback {
|
||||
public class NotificationMainView extends FrameLayout implements SwipeDetector.Listener {
|
||||
|
||||
private NotificationInfo mNotificationInfo;
|
||||
private ViewGroup mTextAndBackground;
|
||||
@@ -47,6 +49,8 @@ public class NotificationMainView extends FrameLayout implements SwipeHelper.Cal
|
||||
private TextView mTitleView;
|
||||
private TextView mTextView;
|
||||
|
||||
private SwipeDetector mSwipeDetector;
|
||||
|
||||
public NotificationMainView(Context context) {
|
||||
this(context, null, 0);
|
||||
}
|
||||
@@ -78,6 +82,10 @@ public class NotificationMainView extends FrameLayout implements SwipeHelper.Cal
|
||||
applyNotificationInfo(mainNotification, iconView, false);
|
||||
}
|
||||
|
||||
public void setSwipeDetector(SwipeDetector swipeDetector) {
|
||||
mSwipeDetector = swipeDetector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the content of this view, animating it after a new icon shifts up if necessary.
|
||||
*/
|
||||
@@ -113,29 +121,11 @@ public class NotificationMainView extends FrameLayout implements SwipeHelper.Cal
|
||||
}
|
||||
|
||||
|
||||
// SwipeHelper.Callback's
|
||||
|
||||
@Override
|
||||
public View getChildAtPosition(MotionEvent ev) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canChildBeDismissed(View v) {
|
||||
public boolean canChildBeDismissed() {
|
||||
return mNotificationInfo != null && mNotificationInfo.dismissable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAntiFalsingNeeded() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBeginDrag(View v) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildDismissed(View v) {
|
||||
public void onChildDismissed() {
|
||||
Launcher launcher = Launcher.getLauncher(getContext());
|
||||
launcher.getPopupDataProvider().cancelNotification(
|
||||
mNotificationInfo.notificationKey);
|
||||
@@ -145,22 +135,55 @@ public class NotificationMainView extends FrameLayout implements SwipeHelper.Cal
|
||||
LauncherLogProto.ItemType.NOTIFICATION);
|
||||
}
|
||||
|
||||
// SwipeDetector.Listener's
|
||||
@Override
|
||||
public void onDragCancelled(View v) {
|
||||
}
|
||||
public void onDragStart(boolean start) { }
|
||||
|
||||
|
||||
@Override
|
||||
public void onChildSnappedBack(View animView, float targetLeft) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) {
|
||||
// Don't fade out.
|
||||
public boolean onDrag(float displacement, float velocity) {
|
||||
setTranslationX(canChildBeDismissed()
|
||||
? displacement : OverScroll.dampedScroll(displacement, getWidth()));
|
||||
animate().cancel();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getFalsingThresholdFactor() {
|
||||
return 1;
|
||||
public void onDragEnd(float velocity, boolean fling) {
|
||||
final boolean willExit;
|
||||
final float endTranslation;
|
||||
|
||||
if (!canChildBeDismissed()) {
|
||||
willExit = false;
|
||||
endTranslation = 0;
|
||||
} else if (fling) {
|
||||
willExit = true;
|
||||
endTranslation = velocity < 0 ? - getWidth() : getWidth();
|
||||
} else if (Math.abs(getTranslationX()) > getWidth() / 2) {
|
||||
willExit = true;
|
||||
endTranslation = (getTranslationX() < 0 ? -getWidth() : getWidth());
|
||||
} else {
|
||||
willExit = false;
|
||||
endTranslation = 0;
|
||||
}
|
||||
|
||||
SwipeDetector.ScrollInterpolator interpolator = new SwipeDetector.ScrollInterpolator();
|
||||
interpolator.setVelocityAtZero(velocity);
|
||||
|
||||
long duration = SwipeDetector.calculateDuration(velocity,
|
||||
(endTranslation - getTranslationX()) / getWidth());
|
||||
animate()
|
||||
.setDuration(duration)
|
||||
.setInterpolator(interpolator)
|
||||
.translationX(endTranslation)
|
||||
.withEndAction(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mSwipeDetector.finishedScrolling();
|
||||
if (willExit) {
|
||||
onChildDismissed();
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,687 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.launcher3.notification;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.animation.ValueAnimator.AnimatorUpdateListener;
|
||||
import android.content.Context;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Handler;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.VelocityTracker;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import com.android.launcher3.R;
|
||||
|
||||
/**
|
||||
* This class was copied from com.android.systemui.
|
||||
*/
|
||||
public class SwipeHelper {
|
||||
private static final String TAG = "SwipeHelper";
|
||||
private static final boolean DEBUG_INVALIDATE = false;
|
||||
private static final boolean SLOW_ANIMATIONS = false; // DEBUG;
|
||||
private static final boolean CONSTRAIN_SWIPE = true;
|
||||
private static final boolean FADE_OUT_DURING_SWIPE = true;
|
||||
private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true;
|
||||
|
||||
public static final int X = 0;
|
||||
public static final int Y = 1;
|
||||
|
||||
private static final float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec
|
||||
private static final int DEFAULT_ESCAPE_ANIMATION_DURATION = 200; // ms
|
||||
private static final int MAX_ESCAPE_ANIMATION_DURATION = 400; // ms
|
||||
private static final int MAX_DISMISS_VELOCITY = 4000; // dp/sec
|
||||
private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 150; // ms
|
||||
|
||||
static final float SWIPE_PROGRESS_FADE_END = 0.5f; // fraction of thumbnail width
|
||||
// beyond which swipe progress->0
|
||||
private float mMinSwipeProgress = 0f;
|
||||
private float mMaxSwipeProgress = 1f;
|
||||
|
||||
private final FlingAnimationUtils mFlingAnimationUtils;
|
||||
private float mPagingTouchSlop;
|
||||
private final Callback mCallback;
|
||||
private final Handler mHandler;
|
||||
private final int mSwipeDirection;
|
||||
private final VelocityTracker mVelocityTracker;
|
||||
|
||||
private float mInitialTouchPos;
|
||||
private float mPerpendicularInitialTouchPos;
|
||||
private boolean mDragging;
|
||||
private boolean mSnappingChild;
|
||||
private View mCurrView;
|
||||
private boolean mCanCurrViewBeDimissed;
|
||||
private float mDensityScale;
|
||||
private float mTranslation = 0;
|
||||
|
||||
private boolean mLongPressSent;
|
||||
private LongPressListener mLongPressListener;
|
||||
private Runnable mWatchLongPress;
|
||||
private final long mLongPressTimeout;
|
||||
|
||||
final private int[] mTmpPos = new int[2];
|
||||
private final int mFalsingThreshold;
|
||||
private boolean mTouchAboveFalsingThreshold;
|
||||
private boolean mDisableHwLayers;
|
||||
|
||||
private final ArrayMap<View, Animator> mDismissPendingMap = new ArrayMap<>();
|
||||
|
||||
public SwipeHelper(int swipeDirection, Callback callback, Context context) {
|
||||
mCallback = callback;
|
||||
mHandler = new Handler();
|
||||
mSwipeDirection = swipeDirection;
|
||||
mVelocityTracker = VelocityTracker.obtain();
|
||||
mDensityScale = context.getResources().getDisplayMetrics().density;
|
||||
mPagingTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
|
||||
|
||||
mLongPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f); // extra long-press!
|
||||
mFalsingThreshold = context.getResources().getDimensionPixelSize(
|
||||
R.dimen.swipe_helper_falsing_threshold);
|
||||
mFlingAnimationUtils = new FlingAnimationUtils(context, getMaxEscapeAnimDuration() / 1000f);
|
||||
}
|
||||
|
||||
public void setLongPressListener(LongPressListener listener) {
|
||||
mLongPressListener = listener;
|
||||
}
|
||||
|
||||
public void setDensityScale(float densityScale) {
|
||||
mDensityScale = densityScale;
|
||||
}
|
||||
|
||||
public void setPagingTouchSlop(float pagingTouchSlop) {
|
||||
mPagingTouchSlop = pagingTouchSlop;
|
||||
}
|
||||
|
||||
public void setDisableHardwareLayers(boolean disableHwLayers) {
|
||||
mDisableHwLayers = disableHwLayers;
|
||||
}
|
||||
|
||||
private float getPos(MotionEvent ev) {
|
||||
return mSwipeDirection == X ? ev.getX() : ev.getY();
|
||||
}
|
||||
|
||||
private float getPerpendicularPos(MotionEvent ev) {
|
||||
return mSwipeDirection == X ? ev.getY() : ev.getX();
|
||||
}
|
||||
|
||||
protected float getTranslation(View v) {
|
||||
return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY();
|
||||
}
|
||||
|
||||
private float getVelocity(VelocityTracker vt) {
|
||||
return mSwipeDirection == X ? vt.getXVelocity() :
|
||||
vt.getYVelocity();
|
||||
}
|
||||
|
||||
protected ObjectAnimator createTranslationAnimation(View v, float newPos) {
|
||||
ObjectAnimator anim = ObjectAnimator.ofFloat(v,
|
||||
mSwipeDirection == X ? View.TRANSLATION_X : View.TRANSLATION_Y, newPos);
|
||||
return anim;
|
||||
}
|
||||
|
||||
private float getPerpendicularVelocity(VelocityTracker vt) {
|
||||
return mSwipeDirection == X ? vt.getYVelocity() :
|
||||
vt.getXVelocity();
|
||||
}
|
||||
|
||||
protected Animator getViewTranslationAnimator(View v, float target,
|
||||
AnimatorUpdateListener listener) {
|
||||
ObjectAnimator anim = createTranslationAnimation(v, target);
|
||||
if (listener != null) {
|
||||
anim.addUpdateListener(listener);
|
||||
}
|
||||
return anim;
|
||||
}
|
||||
|
||||
protected void setTranslation(View v, float translate) {
|
||||
if (v == null) {
|
||||
return;
|
||||
}
|
||||
if (mSwipeDirection == X) {
|
||||
v.setTranslationX(translate);
|
||||
} else {
|
||||
v.setTranslationY(translate);
|
||||
}
|
||||
}
|
||||
|
||||
protected float getSize(View v) {
|
||||
return mSwipeDirection == X ? v.getMeasuredWidth() :
|
||||
v.getMeasuredHeight();
|
||||
}
|
||||
|
||||
public void setMinSwipeProgress(float minSwipeProgress) {
|
||||
mMinSwipeProgress = minSwipeProgress;
|
||||
}
|
||||
|
||||
public void setMaxSwipeProgress(float maxSwipeProgress) {
|
||||
mMaxSwipeProgress = maxSwipeProgress;
|
||||
}
|
||||
|
||||
private float getSwipeProgressForOffset(View view, float translation) {
|
||||
float viewSize = getSize(view);
|
||||
float result = Math.abs(translation / viewSize);
|
||||
return Math.min(Math.max(mMinSwipeProgress, result), mMaxSwipeProgress);
|
||||
}
|
||||
|
||||
private float getSwipeAlpha(float progress) {
|
||||
return Math.min(0, Math.max(1, progress / SWIPE_PROGRESS_FADE_END));
|
||||
}
|
||||
|
||||
private void updateSwipeProgressFromOffset(View animView, boolean dismissable) {
|
||||
updateSwipeProgressFromOffset(animView, dismissable, getTranslation(animView));
|
||||
}
|
||||
|
||||
private void updateSwipeProgressFromOffset(View animView, boolean dismissable,
|
||||
float translation) {
|
||||
float swipeProgress = getSwipeProgressForOffset(animView, translation);
|
||||
if (!mCallback.updateSwipeProgress(animView, dismissable, swipeProgress)) {
|
||||
if (FADE_OUT_DURING_SWIPE && dismissable) {
|
||||
float alpha = swipeProgress;
|
||||
if (!mDisableHwLayers) {
|
||||
if (alpha != 0f && alpha != 1f) {
|
||||
animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
|
||||
} else {
|
||||
animView.setLayerType(View.LAYER_TYPE_NONE, null);
|
||||
}
|
||||
}
|
||||
animView.setAlpha(getSwipeAlpha(swipeProgress));
|
||||
}
|
||||
}
|
||||
invalidateGlobalRegion(animView);
|
||||
}
|
||||
|
||||
// invalidate the view's own bounds all the way up the view hierarchy
|
||||
public static void invalidateGlobalRegion(View view) {
|
||||
invalidateGlobalRegion(
|
||||
view,
|
||||
new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()));
|
||||
}
|
||||
|
||||
// invalidate a rectangle relative to the view's coordinate system all the way up the view
|
||||
// hierarchy
|
||||
public static void invalidateGlobalRegion(View view, RectF childBounds) {
|
||||
//childBounds.offset(view.getTranslationX(), view.getTranslationY());
|
||||
if (DEBUG_INVALIDATE)
|
||||
Log.v(TAG, "-------------");
|
||||
while (view.getParent() != null && view.getParent() instanceof View) {
|
||||
view = (View) view.getParent();
|
||||
view.getMatrix().mapRect(childBounds);
|
||||
view.invalidate((int) Math.floor(childBounds.left),
|
||||
(int) Math.floor(childBounds.top),
|
||||
(int) Math.ceil(childBounds.right),
|
||||
(int) Math.ceil(childBounds.bottom));
|
||||
if (DEBUG_INVALIDATE) {
|
||||
Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left)
|
||||
+ "," + (int) Math.floor(childBounds.top)
|
||||
+ "," + (int) Math.ceil(childBounds.right)
|
||||
+ "," + (int) Math.ceil(childBounds.bottom));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeLongPressCallback() {
|
||||
if (mWatchLongPress != null) {
|
||||
mHandler.removeCallbacks(mWatchLongPress);
|
||||
mWatchLongPress = null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean onInterceptTouchEvent(final MotionEvent ev) {
|
||||
final int action = ev.getAction();
|
||||
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
mTouchAboveFalsingThreshold = false;
|
||||
mDragging = false;
|
||||
mSnappingChild = false;
|
||||
mLongPressSent = false;
|
||||
mVelocityTracker.clear();
|
||||
mCurrView = mCallback.getChildAtPosition(ev);
|
||||
|
||||
if (mCurrView != null) {
|
||||
onDownUpdate(mCurrView);
|
||||
mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView);
|
||||
mVelocityTracker.addMovement(ev);
|
||||
mInitialTouchPos = getPos(ev);
|
||||
mPerpendicularInitialTouchPos = getPerpendicularPos(ev);
|
||||
mTranslation = getTranslation(mCurrView);
|
||||
if (mLongPressListener != null) {
|
||||
if (mWatchLongPress == null) {
|
||||
mWatchLongPress = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mCurrView != null && !mLongPressSent) {
|
||||
mLongPressSent = true;
|
||||
mCurrView.sendAccessibilityEvent(
|
||||
AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
|
||||
mCurrView.getLocationOnScreen(mTmpPos);
|
||||
final int x = (int) ev.getRawX() - mTmpPos[0];
|
||||
final int y = (int) ev.getRawY() - mTmpPos[1];
|
||||
mLongPressListener.onLongPress(mCurrView, x, y);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
mHandler.postDelayed(mWatchLongPress, mLongPressTimeout);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (mCurrView != null && !mLongPressSent) {
|
||||
mVelocityTracker.addMovement(ev);
|
||||
float pos = getPos(ev);
|
||||
float perpendicularPos = getPerpendicularPos(ev);
|
||||
float delta = pos - mInitialTouchPos;
|
||||
float deltaPerpendicular = perpendicularPos - mPerpendicularInitialTouchPos;
|
||||
if (Math.abs(delta) > mPagingTouchSlop
|
||||
&& Math.abs(delta) > Math.abs(deltaPerpendicular)) {
|
||||
mCallback.onBeginDrag(mCurrView);
|
||||
mDragging = true;
|
||||
mInitialTouchPos = getPos(ev);
|
||||
mTranslation = getTranslation(mCurrView);
|
||||
removeLongPressCallback();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
final boolean captured = (mDragging || mLongPressSent);
|
||||
mDragging = false;
|
||||
mCurrView = null;
|
||||
mLongPressSent = false;
|
||||
removeLongPressCallback();
|
||||
if (captured) return true;
|
||||
break;
|
||||
}
|
||||
return mDragging || mLongPressSent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param view The view to be dismissed
|
||||
* @param velocity The desired pixels/second speed at which the view should move
|
||||
* @param useAccelerateInterpolator Should an accelerating Interpolator be used
|
||||
*/
|
||||
public void dismissChild(final View view, float velocity, boolean useAccelerateInterpolator) {
|
||||
dismissChild(view, velocity, null /* endAction */, 0 /* delay */,
|
||||
useAccelerateInterpolator, 0 /* fixedDuration */, false /* isDismissAll */);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param animView The view to be dismissed
|
||||
* @param velocity The desired pixels/second speed at which the view should move
|
||||
* @param endAction The action to perform at the end
|
||||
* @param delay The delay after which we should start
|
||||
* @param useAccelerateInterpolator Should an accelerating Interpolator be used
|
||||
* @param fixedDuration If not 0, this exact duration will be taken
|
||||
*/
|
||||
public void dismissChild(final View animView, float velocity, final Runnable endAction,
|
||||
long delay, boolean useAccelerateInterpolator, long fixedDuration,
|
||||
boolean isDismissAll) {
|
||||
final boolean canBeDismissed = mCallback.canChildBeDismissed(animView);
|
||||
float newPos;
|
||||
boolean isLayoutRtl = animView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
|
||||
|
||||
// if we use the Menu to dismiss an item in landscape, animate up
|
||||
boolean animateUpForMenu = velocity == 0 && (getTranslation(animView) == 0 || isDismissAll)
|
||||
&& mSwipeDirection == Y;
|
||||
// if the language is rtl we prefer swiping to the left
|
||||
boolean animateLeftForRtl = velocity == 0 && (getTranslation(animView) == 0 || isDismissAll)
|
||||
&& isLayoutRtl;
|
||||
boolean animateLeft = velocity < 0
|
||||
|| (velocity == 0 && getTranslation(animView) < 0 && !isDismissAll);
|
||||
|
||||
if (animateLeft || animateLeftForRtl || animateUpForMenu) {
|
||||
newPos = -getSize(animView);
|
||||
} else {
|
||||
newPos = getSize(animView);
|
||||
}
|
||||
long duration;
|
||||
if (fixedDuration == 0) {
|
||||
duration = MAX_ESCAPE_ANIMATION_DURATION;
|
||||
if (velocity != 0) {
|
||||
duration = Math.min(duration,
|
||||
(int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math
|
||||
.abs(velocity))
|
||||
);
|
||||
} else {
|
||||
duration = DEFAULT_ESCAPE_ANIMATION_DURATION;
|
||||
}
|
||||
} else {
|
||||
duration = fixedDuration;
|
||||
}
|
||||
|
||||
if (!mDisableHwLayers) {
|
||||
animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
|
||||
}
|
||||
AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {
|
||||
public void onAnimationUpdate(ValueAnimator animation) {
|
||||
onTranslationUpdate(animView, (float) animation.getAnimatedValue(), canBeDismissed);
|
||||
}
|
||||
};
|
||||
|
||||
Animator anim = getViewTranslationAnimator(animView, newPos, updateListener);
|
||||
if (anim == null) {
|
||||
return;
|
||||
}
|
||||
if (useAccelerateInterpolator) {
|
||||
anim.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
|
||||
anim.setDuration(duration);
|
||||
} else {
|
||||
mFlingAnimationUtils.applyDismissing(anim, getTranslation(animView),
|
||||
newPos, velocity, getSize(animView));
|
||||
}
|
||||
if (delay > 0) {
|
||||
anim.setStartDelay(delay);
|
||||
}
|
||||
anim.addListener(new AnimatorListenerAdapter() {
|
||||
private boolean mCancelled;
|
||||
|
||||
public void onAnimationCancel(Animator animation) {
|
||||
mCancelled = true;
|
||||
}
|
||||
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
updateSwipeProgressFromOffset(animView, canBeDismissed);
|
||||
mDismissPendingMap.remove(animView);
|
||||
if (!mCancelled) {
|
||||
mCallback.onChildDismissed(animView);
|
||||
}
|
||||
if (endAction != null) {
|
||||
endAction.run();
|
||||
}
|
||||
if (!mDisableHwLayers) {
|
||||
animView.setLayerType(View.LAYER_TYPE_NONE, null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
prepareDismissAnimation(animView, anim);
|
||||
mDismissPendingMap.put(animView, anim);
|
||||
anim.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to update the dismiss animation.
|
||||
*/
|
||||
protected void prepareDismissAnimation(View view, Animator anim) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
public void snapChild(final View animView, final float targetLeft, float velocity) {
|
||||
final boolean canBeDismissed = mCallback.canChildBeDismissed(animView);
|
||||
AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {
|
||||
public void onAnimationUpdate(ValueAnimator animation) {
|
||||
onTranslationUpdate(animView, (float) animation.getAnimatedValue(), canBeDismissed);
|
||||
}
|
||||
};
|
||||
|
||||
Animator anim = getViewTranslationAnimator(animView, targetLeft, updateListener);
|
||||
if (anim == null) {
|
||||
return;
|
||||
}
|
||||
int duration = SNAP_ANIM_LEN;
|
||||
anim.setDuration(duration);
|
||||
anim.addListener(new AnimatorListenerAdapter() {
|
||||
public void onAnimationEnd(Animator animator) {
|
||||
mSnappingChild = false;
|
||||
updateSwipeProgressFromOffset(animView, canBeDismissed);
|
||||
mCallback.onChildSnappedBack(animView, targetLeft);
|
||||
}
|
||||
});
|
||||
prepareSnapBackAnimation(animView, anim);
|
||||
mSnappingChild = true;
|
||||
anim.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to update the snap back animation.
|
||||
*/
|
||||
protected void prepareSnapBackAnimation(View view, Animator anim) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when there's a down event.
|
||||
*/
|
||||
public void onDownUpdate(View currView) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on a move event.
|
||||
*/
|
||||
protected void onMoveUpdate(View view, float totalTranslation, float delta) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Called in {@link AnimatorUpdateListener#onAnimationUpdate(ValueAnimator)} when the current
|
||||
* view is being animated to dismiss or snap.
|
||||
*/
|
||||
public void onTranslationUpdate(View animView, float value, boolean canBeDismissed) {
|
||||
updateSwipeProgressFromOffset(animView, canBeDismissed, value);
|
||||
}
|
||||
|
||||
private void snapChildInstantly(final View view) {
|
||||
final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
|
||||
setTranslation(view, 0);
|
||||
updateSwipeProgressFromOffset(view, canAnimViewBeDismissed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a view is updated to be non-dismissable, if the view was being dismissed before
|
||||
* the update this will handle snapping it back into place.
|
||||
*
|
||||
* @param view the view to snap if necessary.
|
||||
* @param animate whether to animate the snap or not.
|
||||
* @param targetLeft the target to snap to.
|
||||
*/
|
||||
public void snapChildIfNeeded(final View view, boolean animate, float targetLeft) {
|
||||
if ((mDragging && mCurrView == view) || mSnappingChild) {
|
||||
return;
|
||||
}
|
||||
boolean needToSnap = false;
|
||||
Animator dismissPendingAnim = mDismissPendingMap.get(view);
|
||||
if (dismissPendingAnim != null) {
|
||||
needToSnap = true;
|
||||
dismissPendingAnim.cancel();
|
||||
} else if (getTranslation(view) != 0) {
|
||||
needToSnap = true;
|
||||
}
|
||||
if (needToSnap) {
|
||||
if (animate) {
|
||||
snapChild(view, targetLeft, 0.0f /* velocity */);
|
||||
} else {
|
||||
snapChildInstantly(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean onTouchEvent(MotionEvent ev) {
|
||||
if (mLongPressSent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!mDragging) {
|
||||
if (mCallback.getChildAtPosition(ev) != null) {
|
||||
|
||||
// We are dragging directly over a card, make sure that we also catch the gesture
|
||||
// even if nobody else wants the touch event.
|
||||
onInterceptTouchEvent(ev);
|
||||
return true;
|
||||
} else {
|
||||
|
||||
// We are not doing anything, make sure the long press callback
|
||||
// is not still ticking like a bomb waiting to go off.
|
||||
removeLongPressCallback();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
mVelocityTracker.addMovement(ev);
|
||||
final int action = ev.getAction();
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_OUTSIDE:
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (mCurrView != null) {
|
||||
float delta = getPos(ev) - mInitialTouchPos;
|
||||
float absDelta = Math.abs(delta);
|
||||
if (absDelta >= getFalsingThreshold()) {
|
||||
mTouchAboveFalsingThreshold = true;
|
||||
}
|
||||
// don't let items that can't be dismissed be dragged more than
|
||||
// maxScrollDistance
|
||||
if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) {
|
||||
float size = getSize(mCurrView);
|
||||
float maxScrollDistance = 0.25f * size;
|
||||
if (absDelta >= size) {
|
||||
delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
|
||||
} else {
|
||||
delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2));
|
||||
}
|
||||
}
|
||||
|
||||
setTranslation(mCurrView, mTranslation + delta);
|
||||
updateSwipeProgressFromOffset(mCurrView, mCanCurrViewBeDimissed);
|
||||
onMoveUpdate(mCurrView, mTranslation + delta, delta);
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
if (mCurrView == null) {
|
||||
break;
|
||||
}
|
||||
mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, getMaxVelocity());
|
||||
float velocity = getVelocity(mVelocityTracker);
|
||||
|
||||
if (!handleUpEvent(ev, mCurrView, velocity, getTranslation(mCurrView))) {
|
||||
if (isDismissGesture(ev)) {
|
||||
// flingadingy
|
||||
dismissChild(mCurrView, velocity,
|
||||
!swipedFastEnough() /* useAccelerateInterpolator */);
|
||||
} else {
|
||||
// snappity
|
||||
mCallback.onDragCancelled(mCurrView);
|
||||
snapChild(mCurrView, 0 /* leftTarget */, velocity);
|
||||
}
|
||||
mCurrView = null;
|
||||
}
|
||||
mDragging = false;
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private int getFalsingThreshold() {
|
||||
float factor = mCallback.getFalsingThresholdFactor();
|
||||
return (int) (mFalsingThreshold * factor);
|
||||
}
|
||||
|
||||
private float getMaxVelocity() {
|
||||
return MAX_DISMISS_VELOCITY * mDensityScale;
|
||||
}
|
||||
|
||||
protected float getEscapeVelocity() {
|
||||
return getUnscaledEscapeVelocity() * mDensityScale;
|
||||
}
|
||||
|
||||
protected float getUnscaledEscapeVelocity() {
|
||||
return SWIPE_ESCAPE_VELOCITY;
|
||||
}
|
||||
|
||||
protected long getMaxEscapeAnimDuration() {
|
||||
return MAX_ESCAPE_ANIMATION_DURATION;
|
||||
}
|
||||
|
||||
protected boolean swipedFarEnough() {
|
||||
float translation = getTranslation(mCurrView);
|
||||
return DISMISS_IF_SWIPED_FAR_ENOUGH && Math.abs(translation) > 0.4 * getSize(mCurrView);
|
||||
}
|
||||
|
||||
protected boolean isDismissGesture(MotionEvent ev) {
|
||||
boolean falsingDetected = mCallback.isAntiFalsingNeeded() && !mTouchAboveFalsingThreshold;
|
||||
return !falsingDetected && (swipedFastEnough() || swipedFarEnough())
|
||||
&& ev.getActionMasked() == MotionEvent.ACTION_UP
|
||||
&& mCallback.canChildBeDismissed(mCurrView);
|
||||
}
|
||||
|
||||
protected boolean swipedFastEnough() {
|
||||
float velocity = getVelocity(mVelocityTracker);
|
||||
float translation = getTranslation(mCurrView);
|
||||
boolean ret = (Math.abs(velocity) > getEscapeVelocity())
|
||||
&& (velocity > 0) == (translation > 0);
|
||||
return ret;
|
||||
}
|
||||
|
||||
protected boolean handleUpEvent(MotionEvent ev, View animView, float velocity,
|
||||
float translation) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
View getChildAtPosition(MotionEvent ev);
|
||||
|
||||
boolean canChildBeDismissed(View v);
|
||||
|
||||
boolean isAntiFalsingNeeded();
|
||||
|
||||
void onBeginDrag(View v);
|
||||
|
||||
void onChildDismissed(View v);
|
||||
|
||||
void onDragCancelled(View v);
|
||||
|
||||
/**
|
||||
* Called when the child is snapped to a position.
|
||||
*
|
||||
* @param animView the view that was snapped.
|
||||
* @param targetLeft the left position the view was snapped to.
|
||||
*/
|
||||
void onChildSnappedBack(View animView, float targetLeft);
|
||||
|
||||
/**
|
||||
* Updates the swipe progress on a child.
|
||||
*
|
||||
* @return if true, prevents the default alpha fading.
|
||||
*/
|
||||
boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress);
|
||||
|
||||
/**
|
||||
* @return The factor the falsing threshold should be multiplied with
|
||||
*/
|
||||
float getFalsingThresholdFactor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent to View.OnLongClickListener with coordinates
|
||||
*/
|
||||
public interface LongPressListener {
|
||||
/**
|
||||
* Equivalent to {@link View.OnLongClickListener#onLongClick(View)} with coordinates
|
||||
* @return whether the longpress was handled
|
||||
*/
|
||||
boolean onLongPress(View v, int x, int y);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.launcher3.touch;
|
||||
|
||||
/**
|
||||
* Utility methods for overscroll damping and related effect.
|
||||
*/
|
||||
public class OverScroll {
|
||||
|
||||
private static final float OVERSCROLL_DAMP_FACTOR = 0.07f;
|
||||
|
||||
/**
|
||||
* This curve determines how the effect of scrolling over the limits of the page diminishes
|
||||
* as the user pulls further and further from the bounds
|
||||
*
|
||||
* @param f The percentage of how much the user has overscrolled.
|
||||
* @return A transformed percentage based on the influence curve.
|
||||
*/
|
||||
private static float overScrollInfluenceCurve(float f) {
|
||||
f -= 1.0f;
|
||||
return f * f * f + 1.0f;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param amount The original amount overscrolled.
|
||||
* @param max The maximum amount that the View can overscroll.
|
||||
* @return The dampened overscroll amount.
|
||||
*/
|
||||
public static int dampedScroll(float amount, int max) {
|
||||
if (Float.compare(amount, 0) == 0) return 0;
|
||||
|
||||
float f = amount / max;
|
||||
f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f)));
|
||||
|
||||
// Clamp this factor, f, to -1 < f < 1
|
||||
if (Math.abs(f) >= 1) {
|
||||
f /= Math.abs(f);
|
||||
}
|
||||
|
||||
return Math.round(OVERSCROLL_DAMP_FACTOR * f * max);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,25 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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.launcher3.touch;
|
||||
|
||||
import static android.view.MotionEvent.INVALID_POINTER_ID;
|
||||
import android.content.Context;
|
||||
import android.graphics.PointF;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewConfiguration;
|
||||
@@ -9,18 +27,20 @@ import android.view.animation.Interpolator;
|
||||
|
||||
/**
|
||||
* One dimensional scroll/drag/swipe gesture detector.
|
||||
*
|
||||
* Definition of swipe is different from android system in that this detector handles
|
||||
* 'swipe to dismiss', 'swiping up/down a container' but also keeps scrolling state before
|
||||
* swipe action happens
|
||||
*/
|
||||
public class SwipeDetector {
|
||||
|
||||
private static final boolean DBG = false;
|
||||
private static final String TAG = "SwipeDetector";
|
||||
|
||||
private final float mTouchSlop;
|
||||
|
||||
private int mScrollConditions;
|
||||
public static final int DIRECTION_UP = 1 << 0;
|
||||
public static final int DIRECTION_DOWN = 1 << 1;
|
||||
public static final int DIRECTION_BOTH = DIRECTION_DOWN | DIRECTION_UP;
|
||||
public static final int DIRECTION_POSITIVE = 1 << 0;
|
||||
public static final int DIRECTION_NEGATIVE = 1 << 1;
|
||||
public static final int DIRECTION_BOTH = DIRECTION_NEGATIVE | DIRECTION_POSITIVE;
|
||||
|
||||
private static final float ANIMATION_DURATION = 1200;
|
||||
private static final float FAST_FLING_PX_MS = 10;
|
||||
@@ -47,6 +67,42 @@ public class SwipeDetector {
|
||||
SETTLING // onDragEnd
|
||||
}
|
||||
|
||||
public static abstract class Direction {
|
||||
|
||||
abstract float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint);
|
||||
|
||||
/**
|
||||
* Distance in pixels a touch can wander before we think the user is scrolling.
|
||||
*/
|
||||
abstract float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos);
|
||||
}
|
||||
|
||||
public static final Direction VERTICAL = new Direction() {
|
||||
|
||||
@Override
|
||||
float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint) {
|
||||
return ev.getY(pointerIndex) - refPoint.y;
|
||||
}
|
||||
|
||||
@Override
|
||||
float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos) {
|
||||
return Math.abs(ev.getX(pointerIndex) - downPos.x);
|
||||
}
|
||||
};
|
||||
|
||||
public static final Direction HORIZONTAL = new Direction() {
|
||||
|
||||
@Override
|
||||
float getDisplacement(MotionEvent ev, int pointerIndex, PointF refPoint) {
|
||||
return ev.getX(pointerIndex) - refPoint.x;
|
||||
}
|
||||
|
||||
@Override
|
||||
float getActiveTouchSlop(MotionEvent ev, int pointerIndex, PointF downPos) {
|
||||
return Math.abs(ev.getY(pointerIndex) - downPos.y);
|
||||
}
|
||||
};
|
||||
|
||||
//------------------- ScrollState transition diagram -----------------------------------
|
||||
//
|
||||
// IDLE -> (mDisplacement > mTouchSlop) -> DRAGGING
|
||||
@@ -93,28 +149,24 @@ public class SwipeDetector {
|
||||
return mState == ScrollState.DRAGGING;
|
||||
}
|
||||
|
||||
private float mDownX;
|
||||
private float mDownY;
|
||||
private final PointF mDownPos = new PointF();
|
||||
private final PointF mLastPos = new PointF();
|
||||
private final Direction mDir;
|
||||
|
||||
private final float mTouchSlop;
|
||||
|
||||
/* Client of this gesture detector can register a callback. */
|
||||
private final Listener mListener;
|
||||
|
||||
private float mLastY;
|
||||
private long mCurrentMillis;
|
||||
|
||||
private float mVelocity;
|
||||
private float mLastDisplacementX;
|
||||
private float mLastDisplacementY;
|
||||
private float mDisplacementY;
|
||||
private float mDisplacementX;
|
||||
private float mLastDisplacement;
|
||||
private float mDisplacement;
|
||||
|
||||
private float mSubtractDisplacement;
|
||||
private boolean mIgnoreSlopWhenSettling;
|
||||
|
||||
/* Client of this gesture detector can register a callback. */
|
||||
private Listener mListener;
|
||||
|
||||
public void setListener(Listener l) {
|
||||
mListener = l;
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onDragStart(boolean start);
|
||||
|
||||
@@ -123,8 +175,15 @@ public class SwipeDetector {
|
||||
void onDragEnd(float velocity, boolean fling);
|
||||
}
|
||||
|
||||
public SwipeDetector(Context context) {
|
||||
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
|
||||
public SwipeDetector(@NonNull Context context, @NonNull Listener l, @NonNull Direction dir) {
|
||||
this(ViewConfiguration.get(context).getScaledTouchSlop(), l, dir);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected SwipeDetector(float touchSlope, @NonNull Listener l, @NonNull Direction dir) {
|
||||
mTouchSlop = touchSlope;
|
||||
mListener = l;
|
||||
mDir = dir;
|
||||
}
|
||||
|
||||
public void setDetectableScrollConditions(int scrollDirectionFlags, boolean ignoreSlop) {
|
||||
@@ -132,21 +191,16 @@ public class SwipeDetector {
|
||||
mIgnoreSlopWhenSettling = ignoreSlop;
|
||||
}
|
||||
|
||||
private boolean shouldScrollStart() {
|
||||
// reject cases where the slop condition is not met.
|
||||
if (Math.abs(mDisplacementY) < mTouchSlop) {
|
||||
private boolean shouldScrollStart(MotionEvent ev, int pointerIndex) {
|
||||
// reject cases where the angle or slop condition is not met.
|
||||
if (Math.max(mDir.getActiveTouchSlop(ev, pointerIndex, mDownPos), mTouchSlop)
|
||||
> Math.abs(mDisplacement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// reject cases where the angle condition is not met.
|
||||
float deltaY = Math.abs(mDisplacementY);
|
||||
float deltaX = Math.max(Math.abs(mDisplacementX), 1);
|
||||
if (deltaX > deltaY) {
|
||||
return false;
|
||||
}
|
||||
// Check if the client is interested in scroll in current direction.
|
||||
if (((mScrollConditions & DIRECTION_DOWN) > 0 && mDisplacementY > 0) ||
|
||||
((mScrollConditions & DIRECTION_UP) > 0 && mDisplacementY < 0)) {
|
||||
if (((mScrollConditions & DIRECTION_NEGATIVE) > 0 && mDisplacement > 0) ||
|
||||
((mScrollConditions & DIRECTION_POSITIVE) > 0 && mDisplacement < 0)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -155,12 +209,11 @@ public class SwipeDetector {
|
||||
public boolean onTouchEvent(MotionEvent ev) {
|
||||
switch (ev.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
mDownX = ev.getX();
|
||||
mDownY = ev.getY();
|
||||
mActivePointerId = ev.getPointerId(0);
|
||||
mLastDisplacementX = 0;
|
||||
mLastDisplacementY = 0;
|
||||
mDisplacementY = 0;
|
||||
mDownPos.set(ev.getX(), ev.getY());
|
||||
mLastPos.set(mDownPos);
|
||||
mLastDisplacement = 0;
|
||||
mDisplacement = 0;
|
||||
mVelocity = 0;
|
||||
|
||||
if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
|
||||
@@ -169,13 +222,14 @@ public class SwipeDetector {
|
||||
break;
|
||||
//case MotionEvent.ACTION_POINTER_DOWN:
|
||||
case MotionEvent.ACTION_POINTER_UP:
|
||||
int ptrIdx = (ev.getActionIndex() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
|
||||
MotionEvent.ACTION_POINTER_INDEX_SHIFT;
|
||||
int ptrIdx = ev.getActionIndex();
|
||||
int ptrId = ev.getPointerId(ptrIdx);
|
||||
if (ptrId == mActivePointerId) {
|
||||
final int newPointerIdx = ptrIdx == 0 ? 1 : 0;
|
||||
mDownX = ev.getX(newPointerIdx) - mLastDisplacementX;
|
||||
mDownY = ev.getY(newPointerIdx) - mLastDisplacementY;
|
||||
mDownPos.set(
|
||||
ev.getX(newPointerIdx) - (mLastPos.x - mDownPos.x),
|
||||
ev.getY(newPointerIdx) - (mLastPos.y - mDownPos.y));
|
||||
mLastPos.set(ev.getX(newPointerIdx), ev.getY(newPointerIdx));
|
||||
mActivePointerId = ev.getPointerId(newPointerIdx);
|
||||
}
|
||||
break;
|
||||
@@ -184,18 +238,18 @@ public class SwipeDetector {
|
||||
if (pointerIndex == INVALID_POINTER_ID) {
|
||||
break;
|
||||
}
|
||||
mDisplacementX = ev.getX(pointerIndex) - mDownX;
|
||||
mDisplacementY = ev.getY(pointerIndex) - mDownY;
|
||||
|
||||
computeVelocity(ev);
|
||||
mDisplacement = mDir.getDisplacement(ev, pointerIndex, mDownPos);
|
||||
computeVelocity(mDir.getDisplacement(ev, pointerIndex, mLastPos),
|
||||
ev.getEventTime());
|
||||
|
||||
// handle state and listener calls.
|
||||
if (mState != ScrollState.DRAGGING && shouldScrollStart()) {
|
||||
if (mState != ScrollState.DRAGGING && shouldScrollStart(ev, pointerIndex)) {
|
||||
setState(ScrollState.DRAGGING);
|
||||
}
|
||||
if (mState == ScrollState.DRAGGING) {
|
||||
reportDragging();
|
||||
}
|
||||
mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));
|
||||
break;
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
case MotionEvent.ACTION_UP:
|
||||
@@ -205,16 +259,8 @@ public class SwipeDetector {
|
||||
}
|
||||
break;
|
||||
default:
|
||||
//TODO: add multi finger tracking by tracking active pointer.
|
||||
break;
|
||||
}
|
||||
// Do house keeping.
|
||||
mLastDisplacementX = mDisplacementX;
|
||||
mLastDisplacementY = mDisplacementY;
|
||||
int pointerIndex = ev.findPointerIndex(mActivePointerId);
|
||||
if (pointerIndex != INVALID_POINTER_ID) {
|
||||
mLastY = ev.getY(pointerIndex);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -234,7 +280,7 @@ public class SwipeDetector {
|
||||
if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) {
|
||||
mSubtractDisplacement = 0;
|
||||
}
|
||||
if (mDisplacementY > 0) {
|
||||
if (mDisplacement > 0) {
|
||||
mSubtractDisplacement = mTouchSlop;
|
||||
} else {
|
||||
mSubtractDisplacement = -mTouchSlop;
|
||||
@@ -242,14 +288,14 @@ public class SwipeDetector {
|
||||
}
|
||||
|
||||
private boolean reportDragging() {
|
||||
float delta = mDisplacementY - mLastDisplacementY;
|
||||
if (delta != 0) {
|
||||
if (mDisplacement != mLastDisplacement) {
|
||||
if (DBG) {
|
||||
Log.d(TAG, String.format("onDrag disp=%.1f, velocity=%.1f",
|
||||
mDisplacementY, mVelocity));
|
||||
mDisplacement, mVelocity));
|
||||
}
|
||||
|
||||
return mListener.onDrag(mDisplacementY - mSubtractDisplacement, mVelocity);
|
||||
mLastDisplacement = mDisplacement;
|
||||
return mListener.onDrag(mDisplacement - mSubtractDisplacement, mVelocity);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -257,19 +303,15 @@ public class SwipeDetector {
|
||||
private void reportDragEnd() {
|
||||
if (DBG) {
|
||||
Log.d(TAG, String.format("onScrollEnd disp=%.1f, velocity=%.1f",
|
||||
mDisplacementY, mVelocity));
|
||||
mDisplacement, mVelocity));
|
||||
}
|
||||
mListener.onDragEnd(mVelocity, Math.abs(mVelocity) > RELEASE_VELOCITY_PX_MS);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the damped velocity using the two motion events and the previous velocity.
|
||||
* Computes the damped velocity.
|
||||
*/
|
||||
private float computeVelocity(MotionEvent to) {
|
||||
return computeVelocity(to.getY() - mLastY, to.getEventTime());
|
||||
}
|
||||
|
||||
public float computeVelocity(float delta, long currentMillis) {
|
||||
long previousMillis = mCurrentMillis;
|
||||
mCurrentMillis = currentMillis;
|
||||
@@ -299,7 +341,7 @@ public class SwipeDetector {
|
||||
return (1.0f - alpha) * from + alpha * to;
|
||||
}
|
||||
|
||||
public long calculateDuration(float velocity, float progressNeeded) {
|
||||
public static long calculateDuration(float velocity, float progressNeeded) {
|
||||
// TODO: make these values constants after tuning.
|
||||
float velocityDivisor = Math.max(2f, Math.abs(0.5f * velocity));
|
||||
float travelDistance = Math.max(0.2f, progressNeeded);
|
||||
|
||||
@@ -87,8 +87,7 @@ public class WidgetsBottomSheet extends AbstractFloatingView implements Insettab
|
||||
AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
|
||||
mScrollInterpolator = new SwipeDetector.ScrollInterpolator();
|
||||
mInsets = new Rect();
|
||||
mSwipeDetector = new SwipeDetector(context);
|
||||
mSwipeDetector.setListener(this);
|
||||
mSwipeDetector = new SwipeDetector(context, this, SwipeDetector.VERTICAL);
|
||||
mGradientBackground = (GradientView) mLauncher.findViewById(R.id.gradient_bg);
|
||||
}
|
||||
|
||||
@@ -283,12 +282,12 @@ public class WidgetsBottomSheet extends AbstractFloatingView implements Insettab
|
||||
public void onDragEnd(float velocity, boolean fling) {
|
||||
if ((fling && velocity > 0) || getTranslationY() > (mTranslationYRange) / 2) {
|
||||
mScrollInterpolator.setVelocityAtZero(velocity);
|
||||
mOpenCloseAnimator.setDuration(mSwipeDetector.calculateDuration(velocity,
|
||||
mOpenCloseAnimator.setDuration(SwipeDetector.calculateDuration(velocity,
|
||||
(mTranslationYClosed - getTranslationY()) / mTranslationYRange));
|
||||
close(true);
|
||||
} else {
|
||||
mIsOpen = false;
|
||||
mOpenCloseAnimator.setDuration(mSwipeDetector.calculateDuration(velocity,
|
||||
mOpenCloseAnimator.setDuration(SwipeDetector.calculateDuration(velocity,
|
||||
(getTranslationY() - mTranslationYOpen) / mTranslationYRange));
|
||||
open(true);
|
||||
}
|
||||
@@ -302,7 +301,7 @@ public class WidgetsBottomSheet extends AbstractFloatingView implements Insettab
|
||||
@Override
|
||||
public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
|
||||
int directionsToDetectScroll = mSwipeDetector.isIdleState() ?
|
||||
SwipeDetector.DIRECTION_DOWN : 0;
|
||||
SwipeDetector.DIRECTION_NEGATIVE : 0;
|
||||
mSwipeDetector.setDetectableScrollConditions(
|
||||
directionsToDetectScroll, false);
|
||||
mSwipeDetector.onTouchEvent(ev);
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
package com.android.launcher3.touch;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.filters.SmallTest;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
@@ -33,11 +32,12 @@ import org.mockito.MockitoAnnotations;
|
||||
|
||||
import static org.mockito.Matchers.anyBoolean;
|
||||
import static org.mockito.Matchers.anyFloat;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class SwipeDetectorTest{
|
||||
public class SwipeDetectorTest {
|
||||
|
||||
private static final String TAG = SwipeDetectorTest.class.getSimpleName();
|
||||
public static void L(String s, Object... parts) {
|
||||
@@ -54,28 +54,47 @@ public class SwipeDetectorTest{
|
||||
@Before
|
||||
public void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
Context context = InstrumentationRegistry.getTargetContext();
|
||||
mDetector = new SwipeDetector(context);
|
||||
mGenerator = new TouchEventGenerator(new TouchEventGenerator.Listener() {
|
||||
@Override
|
||||
public void onTouchEvent(MotionEvent event) {
|
||||
mDetector.onTouchEvent(event);
|
||||
}
|
||||
});
|
||||
mDetector.setListener(mMockListener);
|
||||
|
||||
mDetector = new SwipeDetector(mTouchSlop, mMockListener, SwipeDetector.VERTICAL);
|
||||
mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_BOTH, false);
|
||||
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
|
||||
mTouchSlop = ViewConfiguration.get(InstrumentationRegistry.getTargetContext())
|
||||
.getScaledTouchSlop();
|
||||
L("mTouchSlop=", mTouchSlop);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDragStart() throws Exception {
|
||||
public void testDragStart_vertical() throws Exception {
|
||||
mGenerator.put(0, 100, 100);
|
||||
mGenerator.move(0, 100, 100 + mTouchSlop);
|
||||
// TODO: actually calculate the following parameters and do exact value checks.
|
||||
verify(mMockListener).onDragStart(anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDragStart_failed() throws Exception {
|
||||
mGenerator.put(0, 100, 100);
|
||||
mGenerator.move(0, 100 + mTouchSlop, 100);
|
||||
// TODO: actually calculate the following parameters and do exact value checks.
|
||||
verify(mMockListener, never()).onDragStart(anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDragStart_horizontal() throws Exception {
|
||||
mDetector = new SwipeDetector(mTouchSlop, mMockListener, SwipeDetector.HORIZONTAL);
|
||||
mDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_BOTH, false);
|
||||
|
||||
mGenerator.put(0, 100, 100);
|
||||
mGenerator.move(0, 100 + mTouchSlop, 100);
|
||||
// TODO: actually calculate the following parameters and do exact value checks.
|
||||
verify(mMockListener).onDragStart(anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDrag() throws Exception {
|
||||
mGenerator.put(0, 100, 100);
|
||||
|
||||
Reference in New Issue
Block a user