Use nearest region for all the nav buttons in 3-button folded mode

Bug: 230395757
Test: In 3-button folded mode, make sure that the touches that happen between nav buttons go to the nearest button. No regression in other modes.

Change-Id: Icb776a9a4ed4fc31d33dc3267c7053f2b0da0bfc
This commit is contained in:
Tracy Zhou
2023-12-30 20:13:15 -08:00
parent 5f3b761451
commit 9c9befae5f
9 changed files with 232 additions and 11 deletions

View File

@@ -35,7 +35,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<FrameLayout
<com.android.launcher3.taskbar.navbutton.NearestTouchFrame
android:id="@+id/navbuttons_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -62,7 +62,7 @@
android:layout_height="match_parent"
android:gravity="center_vertical"
android:layout_gravity="end"/>
</FrameLayout>
</com.android.launcher3.taskbar.navbutton.NearestTouchFrame>
<com.android.launcher3.taskbar.StashedHandleView
android:id="@+id/stashed_handle"

View File

@@ -52,7 +52,7 @@
android:elevation="@dimen/bubblebar_elevation"
/>
<FrameLayout
<com.android.launcher3.taskbar.navbutton.NearestTouchFrame
android:id="@+id/navbuttons_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -83,7 +83,7 @@
android:paddingTop="@dimen/taskbar_contextual_padding_top"
android:gravity="center_vertical"
android:layout_gravity="end"/>
</FrameLayout>
</com.android.launcher3.taskbar.navbutton.NearestTouchFrame>
<com.android.launcher3.taskbar.StashedHandleView
android:id="@+id/stashed_handle"

View File

@@ -28,6 +28,7 @@ import android.widget.FrameLayout;
import androidx.annotation.Nullable;
import com.android.launcher3.R;
import com.android.launcher3.taskbar.navbutton.NearestTouchFrame;
/**
* Controller for managing buttons and status icons in taskbar in a desktop environment.
@@ -43,7 +44,7 @@ public class DesktopNavbarButtonsViewController extends NavbarButtonsViewControl
private TaskbarControllers mControllers;
public DesktopNavbarButtonsViewController(TaskbarActivityContext context,
@Nullable Context navigationBarPanelContext, FrameLayout navButtonsView) {
@Nullable Context navigationBarPanelContext, NearestTouchFrame navButtonsView) {
super(context, navigationBarPanelContext, navButtonsView);
mContext = context;
mNavButtonsView = navButtonsView;

View File

@@ -91,6 +91,7 @@ import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarButton;
import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory;
import com.android.launcher3.taskbar.navbutton.NavButtonLayoutFactory.NavButtonLayoutter;
import com.android.launcher3.taskbar.navbutton.NearestTouchFrame;
import com.android.launcher3.util.DimensionUtils;
import com.android.launcher3.util.MultiPropertyFactory.MultiProperty;
import com.android.launcher3.util.MultiValueAlpha;
@@ -151,7 +152,7 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT
private final TaskbarActivityContext mContext;
private final @Nullable Context mNavigationBarPanelContext;
private final WindowManagerProxy mWindowManagerProxy;
private final FrameLayout mNavButtonsView;
private final NearestTouchFrame mNavButtonsView;
private final LinearLayout mNavButtonContainer;
// Used for IME+A11Y buttons
private final ViewGroup mEndContextualContainer;
@@ -208,7 +209,7 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT
private ImageView mRecentsButton;
public NavbarButtonsViewController(TaskbarActivityContext context,
@Nullable Context navigationBarPanelContext, FrameLayout navButtonsView) {
@Nullable Context navigationBarPanelContext, NearestTouchFrame navButtonsView) {
mContext = context;
mNavigationBarPanelContext = navigationBarPanelContext;
mWindowManagerProxy = WindowManagerProxy.INSTANCE.get(mContext);
@@ -517,6 +518,10 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT
return (mState & FLAG_IME_VISIBLE) != 0;
}
public boolean isImeRenderingNavButtons() {
return mIsImeRenderingNavButtons;
}
/**
* Returns true if the home button is disabled
*/
@@ -1003,6 +1008,8 @@ public class NavbarButtonsViewController implements TaskbarControllers.LoggableT
+ mOnTaskbarBackgroundNavButtonColorOverride.value);
pw.println(prefix + "\t\tmOnBackgroundNavButtonColorOverrideMultiplier="
+ mOnBackgroundNavButtonColorOverrideMultiplier.value);
mNavButtonsView.dumpLogs(prefix + "\t", pw);
}
private static String getStateString(int flags) {

View File

@@ -104,6 +104,7 @@ import com.android.launcher3.taskbar.bubbles.BubbleDismissController;
import com.android.launcher3.taskbar.bubbles.BubbleDragController;
import com.android.launcher3.taskbar.bubbles.BubbleStashController;
import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController;
import com.android.launcher3.taskbar.navbutton.NearestTouchFrame;
import com.android.launcher3.taskbar.overlay.TaskbarOverlayController;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
@@ -236,7 +237,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
mDragLayer = (TaskbarDragLayer) mLayoutInflater.inflate(taskbarLayout, null, false);
TaskbarView taskbarView = mDragLayer.findViewById(R.id.taskbar_view);
TaskbarScrimView taskbarScrimView = mDragLayer.findViewById(R.id.taskbar_scrim);
FrameLayout navButtonsView = mDragLayer.findViewById(R.id.navbuttons_view);
NearestTouchFrame navButtonsView = mDragLayer.findViewById(R.id.navbuttons_view);
StashedHandleView stashedHandleView = mDragLayer.findViewById(R.id.stashed_handle);
BubbleBarView bubbleBarView = mDragLayer.findViewById(R.id.taskbar_bubbles);
StashedHandleView bubbleHandleView = mDragLayer.findViewById(R.id.stashed_bubble_handle);

View File

@@ -309,7 +309,12 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas
controllers.bubbleControllers.isPresent &&
controllers.bubbleControllers.get().bubbleBarViewController.isBubbleBarVisible()
var insetsIsTouchableRegion = true
if (context.dragLayer.alpha < AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD) {
if (context.isPhoneButtonNavMode &&
(!controllers.navbarButtonsViewController.isImeVisible
|| !controllers.navbarButtonsViewController.isImeRenderingNavButtons)) {
insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME)
insetsIsTouchableRegion = false
} else if (context.dragLayer.alpha < AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD) {
// Let touches pass through us.
insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION)
debugTouchableRegion.lastSetTouchableReason = "Taskbar is invisible"

View File

@@ -56,7 +56,7 @@ class NavButtonLayoutFactory {
*/
fun getUiLayoutter(
deviceProfile: DeviceProfile,
navButtonsView: FrameLayout,
navButtonsView: NearestTouchFrame,
imeSwitcher: ImageView?,
rotationButton: RotationButton?,
a11yButton: ImageView?,
@@ -78,6 +78,7 @@ class NavButtonLayoutFactory {
return when {
isPhoneNavMode -> {
if (!deviceProfile.isLandscape) {
navButtonsView.setIsVertical(false)
PhonePortraitNavLayoutter(
resources,
navButtonContainer,
@@ -88,6 +89,7 @@ class NavButtonLayoutFactory {
a11yButton
)
} else if (surfaceRotation == ROTATION_90) {
navButtonsView.setIsVertical(true)
PhoneLandscapeNavLayoutter(
resources,
navButtonContainer,
@@ -98,6 +100,7 @@ class NavButtonLayoutFactory {
a11yButton
)
} else {
navButtonsView.setIsVertical(true)
PhoneSeascapeNavLayoutter(
resources,
navButtonContainer,

View File

@@ -0,0 +1,204 @@
/*
* Copyright (C) 2024 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.navbutton;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Redirects touches that aren't handled by any child view to the nearest
* clickable child. Only takes effect on <sw600dp.
*/
public class NearestTouchFrame extends FrameLayout {
private final List<View> mClickableChildren = new ArrayList<>();
private final List<View> mAttachedChildren = new ArrayList<>();
private final boolean mIsActive;
private boolean mIsVertical;
private View mTouchingChild;
private final Map<View, Rect> mTouchableRegions = new HashMap<>();
/**
* Used to sort all child views either by their left position or their top position,
* depending on if this layout is used horizontally or vertically, respectively
*/
private final Comparator<View> mChildRegionComparator =
(view1, view2) -> {
int startingCoordView1 = mIsVertical ? view1.getTop() : view1.getLeft();
int startingCoordView2 = mIsVertical ? view2.getTop() : view2.getLeft();
return startingCoordView1 - startingCoordView2;
};
public NearestTouchFrame(Context context, AttributeSet attrs) {
this(context, attrs, context.getResources().getConfiguration());
}
public NearestTouchFrame(Context context, AttributeSet attrs, Configuration c) {
super(context, attrs);
mIsActive = c.smallestScreenWidthDp < 600;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mClickableChildren.clear();
mAttachedChildren.clear();
mTouchableRegions.clear();
addClickableChildren(this);
cacheClosestChildLocations();
}
/**
* Populates {@link #mTouchableRegions} with the regions where each clickable child is the
* closest for a given point on this layout.
*/
private void cacheClosestChildLocations() {
if (getWidth() == 0 || getHeight() == 0) {
return;
}
// Sort by either top or left depending on mIsVertical, then take out all children
// that are not attached to window
mClickableChildren.sort(mChildRegionComparator);
mClickableChildren.stream()
.filter(View::isAttachedToWindow)
.forEachOrdered(mAttachedChildren::add);
// Cache bounds of children
// Mark coordinates where the actual child layout resides in this frame's window
for (int i = 0; i < mAttachedChildren.size(); i++) {
View child = mAttachedChildren.get(i);
if (!child.isAttachedToWindow()) {
continue;
}
Rect childRegion = getChildsBounds(child);
// We compute closest child from this child to the previous one
if (i == 0) {
// First child, nothing to the left/top of it
if (mIsVertical) {
childRegion.top = 0;
} else {
childRegion.left = 0;
}
mTouchableRegions.put(child, childRegion);
continue;
}
View previousChild = mAttachedChildren.get(i - 1);
Rect previousChildBounds = mTouchableRegions.get(previousChild);
int midPoint;
if (mIsVertical) {
int distance = childRegion.top - previousChildBounds.bottom;
midPoint = distance / 2;
childRegion.top -= midPoint;
previousChildBounds.bottom += midPoint - ((distance % 2) == 0 ? 1 : 0);
} else {
int distance = childRegion.left - previousChildBounds.right;
midPoint = distance / 2;
childRegion.left -= midPoint;
previousChildBounds.right += midPoint - ((distance % 2) == 0 ? 1 : 0);
}
if (i == mClickableChildren.size() - 1) {
// Last child, nothing to right/bottom of it
if (mIsVertical) {
childRegion.bottom = getHeight();
} else {
childRegion.right = getWidth();
}
}
mTouchableRegions.put(child, childRegion);
}
}
void setIsVertical(boolean isVertical) {
mIsVertical = isVertical;
}
private Rect getChildsBounds(View child) {
int left = child.getLeft();
int top = child.getTop();
int right = left + child.getWidth();
int bottom = top + child.getHeight();
return new Rect(left, top, right, bottom);
}
private void addClickableChildren(ViewGroup group) {
final int N = group.getChildCount();
for (int i = 0; i < N; i++) {
View child = group.getChildAt(i);
if (child.isClickable()) {
mClickableChildren.add(child);
} else if (child instanceof ViewGroup) {
addClickableChildren((ViewGroup) child);
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mIsActive) {
int x = (int) event.getX();
int y = (int) event.getY();
if (event.getAction() == MotionEvent.ACTION_DOWN) {
mTouchingChild = mClickableChildren
.stream()
.filter(View::isAttachedToWindow)
.filter(view -> mTouchableRegions.get(view).contains(x, y))
.findFirst()
.orElse(null);
}
if (mTouchingChild != null) {
// Translate the touch event to the view center of the touching child.
event.offsetLocation(mTouchingChild.getWidth() / 2 - x,
mTouchingChild.getHeight() / 2 - y);
return mTouchingChild.getVisibility() == VISIBLE
&& mTouchingChild.dispatchTouchEvent(event);
}
}
return super.onTouchEvent(event);
}
public void dumpLogs(String prefix, PrintWriter pw) {
pw.println(prefix + "NearestTouchFrame:");
pw.println(String.format("%s\tmIsVertical=%s", prefix, mIsVertical));
pw.println(String.format("%s\tmTouchingChild=%s", prefix, mTouchingChild));
pw.println(String.format("%s\tmTouchableRegions=%s", prefix,
mTouchableRegions.keySet().stream()
.map(key -> key + "=" + mTouchableRegions.get(key))
.collect(Collectors.joining(", ", "{", "}"))));
}
}

View File

@@ -27,7 +27,7 @@ import org.mockito.kotlin.whenever
class NavButtonLayoutFactoryTest {
private val mockDeviceProfile: DeviceProfile = mock()
private val mockParentButtonContainer: FrameLayout = mock()
private val mockParentButtonContainer: NearestTouchFrame = mock()
private val mockNavLayout: LinearLayout = mock()
private val mockStartContextualLayout: ViewGroup = mock()
private val mockEndContextualLayout: ViewGroup = mock()