Extend recents button hitbox on tablet
* Extends hitbox when recents is tapped
when going from taskbar to overview.
* Extended region lasts for 400ms after
the animation ends.
Fixes: 225885714
Test: Manual, added unit test
Change-Id: I8766279c1a5bf6867f8d69ddd3af2aa3565deec2
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 349dd0a..4758f10 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -16,6 +16,7 @@
package com.android.launcher3.taskbar;
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
+import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
import static com.android.launcher3.taskbar.LauncherTaskbarUIController.SYSUI_SURFACE_PROGRESS_INDEX;
import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_A11Y;
import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_BACK;
@@ -51,6 +52,7 @@
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.PaintDrawable;
import android.inputmethodservice.InputMethodService;
+import android.os.Handler;
import android.util.Property;
import android.view.Gravity;
import android.view.MotionEvent;
@@ -158,6 +160,7 @@
private BaseDragLayer<TaskbarActivityContext> mSeparateWindowParent; // Initialized in init.
private final ViewTreeObserverWrapper.OnComputeInsetsListener mSeparateWindowInsetsComputer =
this::onComputeInsetsForSeparateWindow;
+ private final RecentsHitboxExtender mHitboxExtender = new RecentsHitboxExtender();
public NavbarButtonsViewController(TaskbarActivityContext context, FrameLayout navButtonsView) {
mContext = context;
@@ -388,8 +391,7 @@
|| (flags & FLAG_KEYGUARD_VISIBLE) != 0,
VIEW_TRANSLATE_X, navButtonSize * (isRtl ? -2 : 2), 0));
-
- // home and recents buttons
+ // home button
mHomeButton = addButton(R.drawable.ic_sysbar_home, BUTTON_HOME, navContainer,
navButtonController, R.id.home);
mHomeButtonAlpha = new MultiValueAlpha(mHomeButton, NUM_ALPHA_CHANNELS);
@@ -399,8 +401,21 @@
ALPHA_INDEX_KEYGUARD_OR_DISABLE),
flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0 &&
(flags & FLAG_DISABLE_HOME) == 0));
+
+ // Recents button
View recentsButton = addButton(R.drawable.ic_sysbar_recent, BUTTON_RECENTS,
navContainer, navButtonController, R.id.recent_apps);
+ mHitboxExtender.init(recentsButton, mNavButtonsView, mContext.getDeviceProfile(),
+ () -> {
+ float[] recentsCoords = new float[2];
+ getDescendantCoordRelativeToAncestor(recentsButton, mNavButtonsView,
+ recentsCoords, false);
+ return recentsCoords;
+ }, new Handler());
+ recentsButton.setOnClickListener(v -> {
+ navButtonController.onButtonClick(BUTTON_RECENTS);
+ mHitboxExtender.onRecentsButtonClicked();
+ });
mPropertyHolders.add(new StatePropertyHolder(recentsButton,
flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0 && (flags & FLAG_DISABLE_RECENTS) == 0
&& !mContext.isNavBarKidsModeActive()));
@@ -504,6 +519,9 @@
View button = mAllButtons.get(i);
if (button.getVisibility() == View.VISIBLE) {
parent.getDescendantRectRelativeToSelf(button, mTempRect);
+ if (mHitboxExtender.extendedHitboxEnabled()) {
+ mTempRect.bottom += mContext.mDeviceProfile.getTaskbarOffsetY();
+ }
outRegion.op(mTempRect, Op.UNION);
}
}
@@ -733,6 +751,17 @@
return str.toString();
}
+ public TouchController getTouchController() {
+ return mHitboxExtender;
+ }
+
+ /**
+ * @param alignment 0 -> Taskbar, 1 -> Workspace
+ */
+ public void updateTaskbarAlignment(float alignment) {
+ mHitboxExtender.onAnimationProgressToOverview(alignment);
+ }
+
private class RotationButtonListener implements RotationButton.RotationButtonUpdatesCallback {
@Override
public void onVisibilityChanged(boolean isVisible) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/RecentsHitboxExtender.java b/quickstep/src/com/android/launcher3/taskbar/RecentsHitboxExtender.java
new file mode 100644
index 0000000..4651570
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/RecentsHitboxExtender.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2022 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.taskbar;
+
+import android.graphics.Rect;
+import android.os.Handler;
+import android.view.MotionEvent;
+import android.view.TouchDelegate;
+import android.view.View;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.util.TouchController;
+
+import java.util.function.Supplier;
+
+/**
+ * Extends the Recents touch area during the taskbar to overview animation
+ * to give user some error room when trying to quickly double tap recents button since it moves.
+ *
+ * Listens for icon alignment as our indication for the animation.
+ */
+public class RecentsHitboxExtender implements TouchController {
+
+ private static final int RECENTS_HITBOX_TIMEOUT_MS = 500;
+
+ private View mRecentsButton;
+ private View mRecentsParent;
+ private DeviceProfile mDeviceProfile;
+ private Supplier<float[]> mParentCoordSupplier;
+ private TouchDelegate mRecentsTouchDelegate;
+ /**
+ * Will be true while the animation from taskbar to overview is occurring.
+ * Lifecycle of this variable slightly extends past the animation by
+ * {@link #RECENTS_HITBOX_TIMEOUT_MS}, so can use this variable as a proxy for if
+ * the current hitbox is extended or not.
+ */
+ private boolean mAnimatingFromTaskbarToOverview;
+ private float mLastIconAlignment;
+ private final Rect mRecentsHitBox = new Rect();
+ private boolean mRecentsButtonClicked;
+ private Handler mHandler;
+ private final Runnable mRecentsHitboxResetRunnable = this::reset;
+
+ public void init(View recentsButton, View recentsParent, DeviceProfile deviceProfile,
+ Supplier<float[]> parentCoordSupplier, Handler handler) {
+ mRecentsButton = recentsButton;
+ mRecentsParent = recentsParent;
+ mDeviceProfile = deviceProfile;
+ mParentCoordSupplier = parentCoordSupplier;
+ mHandler = handler;
+ }
+
+ public void onRecentsButtonClicked() {
+ mRecentsButtonClicked = true;
+ }
+
+ /**
+ * @param progress 0 -> Taskbar, 1 -> Overview
+ */
+ public void onAnimationProgressToOverview(float progress) {
+ if (progress == 1 || progress == 0) {
+ // Done w/ animation
+ mLastIconAlignment = progress;
+ if (mAnimatingFromTaskbarToOverview) {
+ if (progress == 1) {
+ // Finished animation to workspace, remove the touch delegate shortly
+ mHandler.postDelayed(mRecentsHitboxResetRunnable, RECENTS_HITBOX_TIMEOUT_MS);
+ return;
+ } else {
+ // Went back to taskbar, reset immediately
+ mHandler.removeCallbacks(mRecentsHitboxResetRunnable);
+ reset();
+ }
+ }
+ }
+
+ if (mAnimatingFromTaskbarToOverview) {
+ return;
+ }
+
+ if (progress > 0 && mLastIconAlignment == 0 && mRecentsButtonClicked) {
+ // Starting animation, previously we were showing taskbar
+ mAnimatingFromTaskbarToOverview = true;
+ float[] recentsCoords = mParentCoordSupplier.get();
+ int x = (int) recentsCoords[0];
+ int y = (int) (recentsCoords[1]);
+ // Extend hitbox vertically by the offset amount from mDeviceProfile.getTaskbarOffsetY()
+ mRecentsHitBox.set(x, y,
+ x + mRecentsButton.getWidth(),
+ y + mRecentsButton.getHeight() + mDeviceProfile.getTaskbarOffsetY()
+ );
+ mRecentsTouchDelegate = new TouchDelegate(mRecentsHitBox, mRecentsButton);
+ mRecentsParent.setTouchDelegate(mRecentsTouchDelegate);
+ }
+ }
+
+ private void reset() {
+ mAnimatingFromTaskbarToOverview = false;
+ mRecentsButton.setTouchDelegate(null);
+ mRecentsHitBox.setEmpty();
+ mRecentsButtonClicked = false;
+ }
+
+ /**
+ * @return {@code true} if the bounds for recents touches are currently extended
+ */
+ public boolean extendedHitboxEnabled() {
+ return mAnimatingFromTaskbarToOverview;
+ }
+
+ @Override
+ public boolean onControllerTouchEvent(MotionEvent ev) {
+ return mRecentsTouchDelegate.onTouchEvent(ev);
+ }
+
+ @Override
+ public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+ return mRecentsHitBox.contains((int)ev.getX(), (int)ev.getY());
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
index 3e2695c..99c59a8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
@@ -182,7 +182,8 @@
*/
public TouchController[] getTouchControllers() {
return new TouchController[]{mActivity.getDragController(),
- mControllers.taskbarForceVisibleImmersiveController};
+ mControllers.taskbarForceVisibleImmersiveController,
+ mControllers.navbarButtonsViewController.getTouchController()};
}
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index 138fb99..1e9a6a9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -427,6 +427,7 @@
// Switch taskbar and hotseat in last frame
setTaskbarViewVisible(alignment < 1);
+ mControllers.navbarButtonsViewController.updateTaskbarAlignment(alignment);
}
private float getCurrentIconAlignmentRatioBetweenAppAndHome() {
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/RecentsHitboxExtenderTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/RecentsHitboxExtenderTest.java
new file mode 100644
index 0000000..929bff3
--- /dev/null
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/RecentsHitboxExtenderTest.java
@@ -0,0 +1,125 @@
+package com.android.launcher3.taskbar;
+
+import static android.view.MotionEvent.ACTION_DOWN;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.os.Handler;
+import android.view.MotionEvent;
+import android.view.View;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.launcher3.DeviceProfile;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.function.Supplier;
+
+@RunWith(AndroidJUnit4.class)
+public class RecentsHitboxExtenderTest {
+
+ private static final int TASKBAR_OFFSET_Y = 35;
+ private static final int BUTTON_WIDTH = 10;
+ private static final int BUTTON_HEIGHT = 10;
+
+ private final RecentsHitboxExtender mHitboxExtender = new RecentsHitboxExtender();
+ @Mock
+ View mMockRecentsButton;
+ @Mock
+ View mMockRecentsParent;
+ @Mock
+ DeviceProfile mMockDeviceProfile;
+ @Mock
+ Handler mMockHandler;
+ Context mContext;
+
+ float[] mRecentsCoords = new float[]{0,0};
+ private final Supplier<float[]> mSupplier = () -> mRecentsCoords;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+ mContext = instrumentation.getContext();
+ mHitboxExtender.init(mMockRecentsButton, mMockRecentsParent, mMockDeviceProfile, mSupplier,
+ mMockHandler);
+ when(mMockDeviceProfile.getTaskbarOffsetY()).thenReturn(TASKBAR_OFFSET_Y);
+ when(mMockRecentsButton.getContext()).thenReturn(mContext);
+ when(mMockRecentsButton.getWidth()).thenReturn(BUTTON_WIDTH);
+ when(mMockRecentsButton.getHeight()).thenReturn(BUTTON_HEIGHT);
+ }
+
+ @Test
+ public void noRecentsButtonClick_notActive() {
+ mHitboxExtender.onAnimationProgressToOverview(0);
+ mHitboxExtender.onAnimationProgressToOverview(0.5f);
+ assertFalse(mHitboxExtender.extendedHitboxEnabled());
+ }
+
+ @Test
+ public void recentsButtonClick_active() {
+ mHitboxExtender.onRecentsButtonClicked();
+ mHitboxExtender.onAnimationProgressToOverview(0);
+ mHitboxExtender.onAnimationProgressToOverview(0.5f);
+ assertTrue(mHitboxExtender.extendedHitboxEnabled());
+ }
+
+ @Test
+ public void homeToTaskbar_notActive() {
+ mHitboxExtender.onAnimationProgressToOverview(1);
+ mHitboxExtender.onAnimationProgressToOverview(0.5f);
+ assertFalse(mHitboxExtender.extendedHitboxEnabled());
+ }
+
+ @Test
+ public void animationEndReset() {
+ mHitboxExtender.onRecentsButtonClicked();
+ mHitboxExtender.onAnimationProgressToOverview(0);
+ mHitboxExtender.onAnimationProgressToOverview(0.5f);
+ assertTrue(mHitboxExtender.extendedHitboxEnabled());
+ mHitboxExtender.onAnimationProgressToOverview(1);
+ verify(mMockHandler, times(1)).postDelayed(any(), anyLong());
+ }
+
+ @Test
+ public void motionWithinHitbox() {
+ mHitboxExtender.onRecentsButtonClicked();
+ mHitboxExtender.onAnimationProgressToOverview(0);
+ mHitboxExtender.onAnimationProgressToOverview(0.5f);
+ assertTrue(mHitboxExtender.extendedHitboxEnabled());
+ // Center width, past height but w/in offset bounds
+ MotionEvent motionEvent = getMotionEvent(ACTION_DOWN,
+ BUTTON_WIDTH / 2, BUTTON_HEIGHT + TASKBAR_OFFSET_Y / 2);
+ assertTrue(mHitboxExtender.onControllerInterceptTouchEvent(motionEvent));
+ }
+
+ @Test
+ public void motionOutsideHitbox() {
+ mHitboxExtender.onRecentsButtonClicked();
+ mHitboxExtender.onAnimationProgressToOverview(0);
+ mHitboxExtender.onAnimationProgressToOverview(0.5f);
+ assertTrue(mHitboxExtender.extendedHitboxEnabled());
+ // Center width, past height and offset
+ MotionEvent motionEvent = getMotionEvent(ACTION_DOWN,
+ BUTTON_WIDTH / 2, BUTTON_HEIGHT + TASKBAR_OFFSET_Y * 2);
+ assertFalse(mHitboxExtender.onControllerInterceptTouchEvent(motionEvent));
+ }
+
+ private MotionEvent getMotionEvent(int action, int x, int y) {
+ return MotionEvent.obtain(0, 0, action, x, y, 0);
+ }
+}