Introduces haptic feedback to launcher overview
Haptics introduced at the key moments:
- Task scroll in overview or quick switch, trigger when a new task comes
to the center of the screen;
- Task scroll in overview when overscroll animation is triggered;
- Task dismissed in overview;
There is also a configured min gap between two scroll haptics set to
20ms to prevent fast scrolls from creating a chain of cancelled effects.
Fix: 182382085
Test: manual
Change-Id: I43c0f8c879a06f317e8a660240dafb7f7abe79f7
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index eb3f94c..e4ee50a 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -108,6 +108,8 @@
// relative scroll position unchanged in updateCurrentPageScroll. Cleared when snapping to a
// page.
protected int mCurrentPageScrollDiff;
+ // The current page the PagedView is scrolling over on it's way to the destination page.
+ protected int mCurrentScrollOverPage;
@ViewDebug.ExportedProperty(category = "launcher")
protected int mNextPage = INVALID_PAGE;
@@ -180,6 +182,7 @@
mScroller = new OverScroller(context, SCROLL);
mCurrentPage = 0;
+ mCurrentScrollOverPage = 0;
final ViewConfiguration configuration = ViewConfiguration.get(context);
mTouchSlop = configuration.getScaledTouchSlop();
@@ -437,6 +440,7 @@
}
int prevPage = overridePrevPage != INVALID_PAGE ? overridePrevPage : mCurrentPage;
mCurrentPage = validateNewPage(currentPage);
+ mCurrentScrollOverPage = mCurrentPage;
updateCurrentPageScroll();
notifyPageSwitchListener(prevPage);
invalidate();
@@ -557,9 +561,11 @@
if (newPos < mMinScroll && oldPos >= mMinScroll) {
mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity());
mScroller.abortAnimation();
+ onEdgeAbsorbingScroll();
} else if (newPos > mMaxScroll && oldPos <= mMaxScroll) {
mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity());
mScroller.abortAnimation();
+ onEdgeAbsorbingScroll();
}
}
@@ -577,6 +583,7 @@
sendScrollAccessibilityEvent();
int prevPage = mCurrentPage;
mCurrentPage = validateNewPage(mNextPage);
+ mCurrentScrollOverPage = mCurrentPage;
mNextPage = INVALID_PAGE;
notifyPageSwitchListener(prevPage);
@@ -837,6 +844,7 @@
public void onViewRemoved(View child) {
super.onViewRemoved(child);
mCurrentPage = validateNewPage(mCurrentPage);
+ mCurrentScrollOverPage = mCurrentPage;
dispatchPageCountChanged();
}
@@ -1408,6 +1416,20 @@
protected void onNotSnappingToPageInFreeScroll() { }
+ /**
+ * Called when the view edges absorb part of the scroll. Subclasses can override this
+ * to provide custom behavior during animation.
+ */
+ protected void onEdgeAbsorbingScroll() {
+ }
+
+ /**
+ * Called when the current page closest to the center of the screen changes as part of the
+ * scroll. Subclasses can override this to provide custom behavior during scroll.
+ */
+ protected void onScrollOverPageChanged() {
+ }
+
protected boolean shouldFlingForVelocity(int velocity) {
float threshold = mAllowEasyFling ? mEasyFlingThresholdVelocity : mFlingThresholdVelocity;
return Math.abs(velocity) > threshold;
@@ -1692,6 +1714,15 @@
}
@Override
+ protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+ int newDestinationPage = getDestinationPage();
+ if (newDestinationPage >= 0 && newDestinationPage != mCurrentScrollOverPage) {
+ mCurrentScrollOverPage = newDestinationPage;
+ onScrollOverPageChanged();
+ }
+ }
+
+ @Override
public CharSequence getAccessibilityClassName() {
// Some accessibility services have special logic for ScrollView. Since we provide same
// accessibility info as ScrollView, inform the service to handle use the same way.
diff --git a/src/com/android/launcher3/util/VibratorWrapper.java b/src/com/android/launcher3/util/VibratorWrapper.java
deleted file mode 100644
index b0defd4..0000000
--- a/src/com/android/launcher3/util/VibratorWrapper.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright (C) 2019 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.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;
-
-import android.annotation.TargetApi;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.os.Build;
-import android.os.VibrationEffect;
-import android.os.Vibrator;
-import android.provider.Settings;
-
-/**
- * Wrapper around {@link Vibrator} to easily perform haptic feedback where necessary.
- */
-@TargetApi(Build.VERSION_CODES.Q)
-public class VibratorWrapper {
-
- public static final MainThreadInitializedObject<VibratorWrapper> INSTANCE =
- new MainThreadInitializedObject<>(VibratorWrapper::new);
-
- public static final VibrationEffect EFFECT_CLICK =
- createPredefined(VibrationEffect.EFFECT_CLICK);
-
- /**
- * Haptic when entering overview.
- */
- public static final VibrationEffect OVERVIEW_HAPTIC = EFFECT_CLICK;
-
- private final Vibrator mVibrator;
- private final boolean mHasVibrator;
-
- private boolean mIsHapticFeedbackEnabled;
-
- public VibratorWrapper(Context context) {
- mVibrator = context.getSystemService(Vibrator.class);
- mHasVibrator = mVibrator.hasVibrator();
- if (mHasVibrator) {
- final ContentResolver resolver = context.getContentResolver();
- mIsHapticFeedbackEnabled = isHapticFeedbackEnabled(resolver);
- final ContentObserver observer = new ContentObserver(MAIN_EXECUTOR.getHandler()) {
- @Override
- public void onChange(boolean selfChange) {
- mIsHapticFeedbackEnabled = isHapticFeedbackEnabled(resolver);
- }
- };
- resolver.registerContentObserver(Settings.System.getUriFor(HAPTIC_FEEDBACK_ENABLED),
- false /* notifyForDescendents */, observer);
- } else {
- mIsHapticFeedbackEnabled = false;
- }
- }
-
- private boolean isHapticFeedbackEnabled(ContentResolver resolver) {
- return Settings.System.getInt(resolver, HAPTIC_FEEDBACK_ENABLED, 0) == 1;
- }
-
- /** Vibrates with the given effect if haptic feedback is available and enabled. */
- public void vibrate(VibrationEffect vibrationEffect) {
- if (mHasVibrator && mIsHapticFeedbackEnabled) {
- UI_HELPER_EXECUTOR.execute(() -> mVibrator.vibrate(vibrationEffect));
- }
- }
-}