From b1436b0ae7e5a4e5a8644e4c11bd7f7d338ba7a4 Mon Sep 17 00:00:00 2001 From: Vinit Nayak Date: Mon, 2 May 2022 11:51:01 -0700 Subject: [PATCH] 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 --- .../taskbar/NavbarButtonsViewController.java | 33 ++++- .../taskbar/RecentsHitboxExtender.java | 134 ++++++++++++++++++ .../taskbar/TaskbarDragLayerController.java | 3 +- .../TaskbarLauncherStateController.java | 1 + .../taskbar/RecentsHitboxExtenderTest.java | 125 ++++++++++++++++ 5 files changed, 293 insertions(+), 3 deletions(-) create mode 100644 quickstep/src/com/android/launcher3/taskbar/RecentsHitboxExtender.java create mode 100644 quickstep/tests/src/com/android/launcher3/taskbar/RecentsHitboxExtenderTest.java diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java index 349dd0a8f6..4758f102ab 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.Region.Op; 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 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT private BaseDragLayer 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 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT || (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 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT 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 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT 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 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT 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 0000000000..4651570a60 --- /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 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 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 3e2695c135..99c59a8a98 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java @@ -182,7 +182,8 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa */ 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 138fb991f4..1e9a6a90f0 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java @@ -427,6 +427,7 @@ import java.util.function.Supplier; // 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 0000000000..929bff3004 --- /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 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); + } +}