Merge "Implement diff haptics going into all apps" into tm-qpr-dev

This commit is contained in:
Brandon Dayauon
2023-02-09 00:45:24 +00:00
committed by Android (Google) Code Review
6 changed files with 181 additions and 9 deletions

View File

@@ -40,6 +40,7 @@
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<uses-permission android:name="android.permission.VIBRATE"/>
<!-- for rotating surface by arbitrary degree -->
<uses-permission android:name="android.permission.ROTATE_SURFACE_FLINGER" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

View File

@@ -39,6 +39,7 @@ import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.taskbar.LauncherTaskbarUIController;
import com.android.launcher3.uioverrides.QuickstepLauncher;
@@ -62,6 +63,7 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch
private static final long TRANSLATION_ANIM_MIN_DURATION_MS = 80;
private static final float TRANSLATION_ANIM_VELOCITY_DP_PER_MS = 0.8f;
private final VibratorWrapper mVibratorWrapper;
private final RecentsView mRecentsView;
private final MotionPauseDetector mMotionPauseDetector;
private final float mMotionPauseMinDisplacement;
@@ -82,6 +84,7 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch
mRecentsView = l.getOverviewPanel();
mMotionPauseDetector = new MotionPauseDetector(l);
mMotionPauseMinDisplacement = ViewConfiguration.get(l).getScaledTouchSlop();
mVibratorWrapper = VibratorWrapper.INSTANCE.get(l.getApplicationContext());
}
@Override
@@ -188,6 +191,11 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch
// need to manually set the duration to a reasonable value.
animator.setDuration(HINT_STATE.getTransitionDuration(mLauncher, true /* isToState */));
}
if (FeatureFlags.ENABLE_HAPTICS_ALL_APPS.get() &&
((mFromState == NORMAL && mToState == ALL_APPS)
|| (mFromState == ALL_APPS && mToState == NORMAL)) && isFling) {
mVibratorWrapper.vibrateForDragBump();
}
}
private void onMotionPauseDetected() {

View File

@@ -32,6 +32,7 @@ import static com.android.launcher3.util.SystemUiController.UI_STATE_ALL_APPS;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.util.FloatProperty;
import android.view.HapticFeedbackConstants;
import android.view.View;
@@ -47,17 +48,21 @@ import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.anim.PropertySetter;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.statemanager.StateManager.StateHandler;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.touch.AllAppsSwipeController;
import com.android.launcher3.util.MultiPropertyFactory;
import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.launcher3.util.Themes;
import com.android.launcher3.util.VibratorWrapper;
import com.android.launcher3.views.ScrimView;
/**
@@ -78,6 +83,8 @@ public class AllAppsTransitionController
private static final int REVERT_SWIPE_ALL_APPS_TO_HOME_ANIMATION_DURATION_MS = 200;
private static final float NAV_BAR_COLOR_FORCE_UPDATE_THRESHOLD = 0.1f;
private static final float SWIPE_DRAG_COMMIT_THRESHOLD =
1 - AllAppsSwipeController.ALL_APPS_STATE_TRANSITION_MANUAL;
public static final FloatProperty<AllAppsTransitionController> ALL_APPS_PROGRESS =
new FloatProperty<AllAppsTransitionController>("allAppsProgress") {
@@ -181,6 +188,7 @@ public class AllAppsTransitionController
private boolean mIsTablet;
private boolean mHasScaleEffect;
private final VibratorWrapper mVibratorWrapper;
public AllAppsTransitionController(Launcher l) {
mLauncher = l;
@@ -193,6 +201,7 @@ public class AllAppsTransitionController
setShiftRange(dp.allAppsShiftRange);
mLauncher.addOnDeviceProfileChangeListener(this);
mVibratorWrapper = VibratorWrapper.INSTANCE.get(mLauncher.getApplicationContext());
}
public float getShiftRange() {
@@ -311,6 +320,11 @@ public class AllAppsTransitionController
/**
* Creates an animation which updates the vertical transition progress and updates all the
* dependent UI using various animation events
*
* This method also dictates where along the progress the haptics should be played. As the user
* scrolls up from workspace or down from AllApps, a drag haptic is being played until the
* commit point where it plays a commit haptic. Where we play the haptics differs when going
* from workspace -> allApps and vice versa.
*/
@Override
public void setStateWithAnimation(LauncherState toState,
@@ -339,6 +353,20 @@ public class AllAppsTransitionController
});
}
if(FeatureFlags.ENABLE_HAPTICS_ALL_APPS.get() && config.userControlled
&& Utilities.ATLEAST_S) {
if (toState == ALL_APPS) {
builder.addOnFrameListener(
new VibrationAnimatorUpdateListener(this, mVibratorWrapper,
SWIPE_DRAG_COMMIT_THRESHOLD, 1));
} else {
builder.addOnFrameListener(
new VibrationAnimatorUpdateListener(this, mVibratorWrapper,
0, SWIPE_DRAG_COMMIT_THRESHOLD));
}
builder.addEndListener(mVibratorWrapper::cancelVibrate);
}
float targetProgress = toState.getVerticalProgress(mLauncher);
if (Float.compare(mProgress, targetProgress) == 0) {
setAlphas(toState, config, builder);
@@ -356,7 +384,7 @@ public class AllAppsTransitionController
setAlphas(toState, config, builder);
if (ALL_APPS.equals(toState) && mLauncher.isInState(NORMAL)) {
if (ALL_APPS.equals(toState) && mLauncher.isInState(NORMAL) && !(Utilities.ATLEAST_S)) {
mLauncher.getAppsView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
}
@@ -494,4 +522,45 @@ public class AllAppsTransitionController
}
}
}
/**
* This VibrationAnimatorUpdateListener class takes in four parameters, a controller, start
* threshold, end threshold, and a Vibrator wrapper. We use the progress given by the controller
* as it gives an accurate progress that dictates where the vibrator should vibrate.
* Note: once the user begins a gesture and does the commit haptic, there should not be anymore
* haptics played for that gesture.
*/
private static class VibrationAnimatorUpdateListener implements
ValueAnimator.AnimatorUpdateListener {
private final VibratorWrapper mVibratorWrapper;
private final AllAppsTransitionController mController;
private final float mStartThreshold;
private final float mEndThreshold;
private boolean mHasCommitted;
VibrationAnimatorUpdateListener(AllAppsTransitionController controller,
VibratorWrapper vibratorWrapper, float startThreshold,
float endThreshold) {
mController = controller;
mVibratorWrapper = vibratorWrapper;
mStartThreshold = startThreshold;
mEndThreshold = endThreshold;
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if (mHasCommitted) {
return;
}
float currentProgress =
AllAppsTransitionController.ALL_APPS_PROGRESS.get(mController);
if (currentProgress > mStartThreshold && currentProgress < mEndThreshold) {
mVibratorWrapper.vibrateForDragTexture();
} else if (!(currentProgress == 0 || currentProgress == 1)) {
// This check guards against committing at the location of the start of the gesture
mVibratorWrapper.vibrateForDragCommit();
mHasCommitted = true;
}
}
}
}

View File

@@ -371,6 +371,8 @@ public final class FeatureFlags {
"ENABLE_LAUNCH_FROM_STAGED_APP", true,
"Enable the ability to tap a staged app during split select to launch it in full screen"
);
public static final BooleanFlag ENABLE_HAPTICS_ALL_APPS = getDebugFlag(
"ENABLE_HAPTICS_ALL_APPS", false, "Enables haptics opening/closing All apps");
public static final BooleanFlag ENABLE_FORCED_MONO_ICON = getDebugFlag(
"ENABLE_FORCED_MONO_ICON", false,

View File

@@ -129,10 +129,7 @@ public class AllAppsSwipeController extends AbstractStateChangeTouchController {
Interpolators.clampToProgress(
Interpolators.mapToProgress(EMPHASIZED_DECELERATE, 0.4f, 1f),
ALL_APPS_STATE_TRANSITION_ATOMIC, 1f);
public static final Interpolator ALL_APPS_VERTICAL_PROGRESS_MANUAL =
Interpolators.clampToProgress(
Interpolators.mapToProgress(LINEAR, ALL_APPS_STATE_TRANSITION_MANUAL, 1f),
ALL_APPS_STATE_TRANSITION_MANUAL, 1f);
public static final Interpolator ALL_APPS_VERTICAL_PROGRESS_MANUAL = LINEAR;
// --------

View File

@@ -17,7 +17,6 @@ package com.android.launcher3.util;
import static android.os.VibrationEffect.createPredefined;
import static android.provider.Settings.System.HAPTIC_FEEDBACK_ENABLED;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
@@ -28,12 +27,17 @@ import android.content.Context;
import android.database.ContentObserver;
import android.media.AudioAttributes;
import android.os.Build;
import android.os.SystemClock;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.provider.Settings;
import androidx.annotation.Nullable;
import com.android.launcher3.Utilities;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.anim.PendingAnimation;
import java.util.function.Consumer;
/**
* Wrapper around {@link Vibrator} to easily perform haptic feedback where necessary.
@@ -52,6 +56,21 @@ public class VibratorWrapper {
public static final VibrationEffect EFFECT_CLICK =
createPredefined(VibrationEffect.EFFECT_CLICK);
private static final float DRAG_TEXTURE_SCALE = 0.03f;
private static final float DRAG_COMMIT_SCALE = 0.5f;
private static final float DRAG_BUMP_SCALE = 0.4f;
private static final int DRAG_TEXTURE_EFFECT_SIZE = 200;
@Nullable
private final VibrationEffect mDragEffect;
@Nullable
private final VibrationEffect mCommitEffect;
@Nullable
private final VibrationEffect mBumpEffect;
private long mLastDragTime;
private final int mThresholdUntilNextDragCallMillis;
/**
* Haptic when entering overview.
*/
@@ -62,7 +81,7 @@ public class VibratorWrapper {
private boolean mIsHapticFeedbackEnabled;
public VibratorWrapper(Context context) {
private VibratorWrapper(Context context) {
mVibrator = context.getSystemService(Vibrator.class);
mHasVibrator = mVibrator.hasVibrator();
if (mHasVibrator) {
@@ -75,12 +94,88 @@ public class VibratorWrapper {
}
};
resolver.registerContentObserver(Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED),
false /* notifyForDescendents */, observer);
false /* notifyForDescendants */, observer);
} else {
mIsHapticFeedbackEnabled = false;
}
if (Utilities.ATLEAST_S && mVibrator.areAllPrimitivesSupported(
VibrationEffect.Composition.PRIMITIVE_LOW_TICK)) {
// Drag texture, Commit, and Bump should only be used for premium phones.
// Before using these haptics make sure check if the device can use it
VibrationEffect.Composition dragEffect = VibrationEffect.startComposition();
for (int i = 0; i < DRAG_TEXTURE_EFFECT_SIZE; i++) {
dragEffect.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_LOW_TICK, DRAG_TEXTURE_SCALE);
}
mDragEffect = dragEffect.compose();
mCommitEffect = VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_TICK, DRAG_COMMIT_SCALE).compose();
mBumpEffect = VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_LOW_TICK, DRAG_BUMP_SCALE).compose();
int primitiveDuration = mVibrator.getPrimitiveDurations(
VibrationEffect.Composition.PRIMITIVE_LOW_TICK)[0];
mThresholdUntilNextDragCallMillis =
DRAG_TEXTURE_EFFECT_SIZE * primitiveDuration + 100;
} else {
mDragEffect = null;
mCommitEffect = null;
mBumpEffect = null;
mThresholdUntilNextDragCallMillis = 0;
}
}
/**
* This is called when the user swipes to/from all apps. This is meant to be used in between
* long animation progresses so that it gives a dragging texture effect. For a better
* experience, this should be used in combination with vibrateForDragCommit().
*/
public void vibrateForDragTexture() {
if (mDragEffect == null) {
return;
}
long currentTime = SystemClock.elapsedRealtime();
long elapsedTimeSinceDrag = currentTime - mLastDragTime;
if (elapsedTimeSinceDrag >= mThresholdUntilNextDragCallMillis) {
vibrate(mDragEffect);
mLastDragTime = currentTime;
}
}
/**
* This is used when user reaches the commit threshold when swiping to/from from all apps.
*/
public void vibrateForDragCommit() {
if (mCommitEffect != null) {
vibrate(mCommitEffect);
}
// resetting dragTexture timestamp to be able to play dragTexture again
mLastDragTime = 0;
}
/**
* The bump haptic is used to be called at the end of a swipe and only if it the gesture is a
* FLING going to/from all apps. Client can just call this method elsewhere just for the
* effect.
*/
public void vibrateForDragBump() {
if (mBumpEffect != null) {
vibrate(mBumpEffect);
}
}
/**
* This should be used to cancel a haptic in case where the haptic shouldn't be vibrating. For
* example, when no animation is happening but a vibrator happens to be vibrating still. Need
* boolean parameter for {@link PendingAnimation#addEndListener(Consumer)}.
*/
public void cancelVibrate(boolean unused) {
UI_HELPER_EXECUTOR.execute(mVibrator::cancel);
// reset dragTexture timestamp to be able to play dragTexture again whenever cancelled
mLastDragTime = 0;
}
private boolean isHapticFeedbackEnabled(ContentResolver resolver) {
return Settings.System.getInt(resolver, HAPTIC_FEEDBACK_ENABLED, 0) == 1;
}