Merge "Cache locations of where nearest child is." into sc-dev

This commit is contained in:
TreeHugger Robot
2021-02-19 19:34:53 +00:00
committed by Android (Google) Code Review
7 changed files with 231 additions and 38 deletions

View File

@@ -30,7 +30,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false">
android:clipToPadding="false"
systemui:isVertical="false">
<LinearLayout
android:id="@+id/ends_group"

View File

@@ -30,7 +30,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false">
android:clipToPadding="false"
systemui:isVertical="true">
<com.android.systemui.navigationbar.buttons.ReverseLinearLayout
android:id="@+id/ends_group"

View File

@@ -143,6 +143,8 @@
<attr name="handleColor" format="color" />
<attr name="scrimColor" format="color" />
<attr name="isVertical" format="boolean" />
<!-- Used display CarrierText in Keyguard or QS Footer -->
<declare-styleable name="CarrierText">
<attr name="allCaps" format="boolean" />

View File

@@ -1124,7 +1124,7 @@ public class NavigationBar implements View.OnAttachStateChangeListener,
}
// If an incoming call is ringing, HOME is totally disabled.
// (The user is already on the InCallUI at this point,
// and his ONLY options are to answer or reject the call.)
// and their ONLY options are to answer or reject the call.)
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mHomeBlockedThisTouch = false;

View File

@@ -76,6 +76,7 @@ import com.android.systemui.navigationbar.buttons.ContextualButton;
import com.android.systemui.navigationbar.buttons.ContextualButtonGroup;
import com.android.systemui.navigationbar.buttons.DeadZone;
import com.android.systemui.navigationbar.buttons.KeyButtonDrawable;
import com.android.systemui.navigationbar.buttons.NearestTouchFrame;
import com.android.systemui.navigationbar.buttons.RotationContextButton;
import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
import com.android.systemui.navigationbar.gestural.FloatingRotationButton;
@@ -97,6 +98,8 @@ import com.android.wm.shell.legacysplitscreen.LegacySplitScreen;
import com.android.wm.shell.pip.Pip;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
public class NavigationBarView extends FrameLayout implements
@@ -129,6 +132,7 @@ public class NavigationBarView extends FrameLayout implements
private final Region mTmpRegion = new Region();
private final int[] mTmpPosition = new int[2];
private Rect mTmpBounds = new Rect();
private Map<View, Rect> mButtonFullTouchableRegions = new HashMap<>();
private KeyButtonDrawable mBackIcon;
private KeyButtonDrawable mHomeDefaultIcon;
@@ -973,9 +977,18 @@ public class NavigationBarView extends FrameLayout implements
getButtonLocations(true /* includeFloatingRotationButton */, true /* inScreen */));
}
private void updateButtonTouchRegionCache() {
FrameLayout navBarLayout = mIsVertical
? mNavigationInflaterView.mVertical
: mNavigationInflaterView.mHorizontal;
mButtonFullTouchableRegions = ((NearestTouchFrame) navBarLayout
.findViewById(R.id.nav_buttons)).getFullTouchableChildRegions();
}
private Region getButtonLocations(boolean includeFloatingRotationButton,
boolean inScreenSpace) {
mTmpRegion.setEmpty();
updateButtonTouchRegionCache();
updateButtonLocation(getBackButton(), inScreenSpace);
updateButtonLocation(getHomeButton(), inScreenSpace);
updateButtonLocation(getRecentsButton(), inScreenSpace);
@@ -999,6 +1012,12 @@ public class NavigationBarView extends FrameLayout implements
if (view == null || !button.isVisible()) {
return;
}
// If the button is tappable from perspective of NearestTouchFrame, then we'll
// include the regions where the tap is valid instead of just the button layout location
if (mButtonFullTouchableRegions.containsKey(view)) {
mTmpRegion.op(mButtonFullTouchableRegions.get(view), Op.UNION);
return;
}
updateButtonLocation(view, inScreenSpace);
}

View File

@@ -18,8 +18,9 @@ package com.android.systemui.navigationbar.buttons;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
@@ -27,8 +28,13 @@ import android.widget.FrameLayout;
import androidx.annotation.VisibleForTesting;
import com.android.systemui.R;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Redirects touches that aren't handled by any child view to the nearest
@@ -36,11 +42,32 @@ import java.util.Comparator;
*/
public class NearestTouchFrame extends FrameLayout {
private final ArrayList<View> mClickableChildren = new ArrayList<>();
private final List<View> mClickableChildren = new ArrayList<>();
private final List<View> mAttachedChildren = new ArrayList<>();
private final boolean mIsActive;
private final int[] mTmpInt = new int[2];
private final int[] mOffset = new int[2];
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 leftTopIndex = 0;
if (mIsVertical) {
// Compare view bound's "top" values
leftTopIndex = 1;
}
view1.getLocationInWindow(mTmpInt);
int startingCoordView1 = mTmpInt[leftTopIndex] - mOffset[leftTopIndex];
view2.getLocationInWindow(mTmpInt);
int startingCoordView2 = mTmpInt[leftTopIndex] - mOffset[leftTopIndex];
return startingCoordView1 - startingCoordView2;
};
public NearestTouchFrame(Context context, AttributeSet attrs) {
this(context, attrs, context.getResources().getConfiguration());
@@ -50,13 +77,20 @@ public class NearestTouchFrame extends FrameLayout {
NearestTouchFrame(Context context, AttributeSet attrs, Configuration c) {
super(context, attrs);
mIsActive = c.smallestScreenWidthDp < 600;
int[] attrsArray = new int[] {R.attr.isVertical};
TypedArray ta = context.obtainStyledAttributes(attrs, attrsArray);
mIsVertical = ta.getBoolean(0, false);
ta.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mClickableChildren.clear();
mAttachedChildren.clear();
mTouchableRegions.clear();
addClickableChildren(this);
cacheClosestChildLocations();
}
@Override
@@ -65,6 +99,85 @@ public class NearestTouchFrame extends FrameLayout {
getLocationInWindow(mOffset);
}
/**
* 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);
}
}
@VisibleForTesting
void setIsVertical(boolean isVertical) {
mIsVertical = isVertical;
}
private Rect getChildsBounds(View child) {
child.getLocationInWindow(mTmpInt);
int left = mTmpInt[0] - mOffset[0];
int top = mTmpInt[1] - mOffset[1];
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++) {
@@ -77,47 +190,45 @@ public class NearestTouchFrame extends FrameLayout {
}
}
/**
* @return A Map where the key is the view object of the button and the value
* is the Rect where that button will receive a touch event if pressed. This Rect will
* usually be larger than the layout bounds for the button.
* The Rect is in screen coordinates.
*/
public Map<View, Rect> getFullTouchableChildRegions() {
Map<View, Rect> fullTouchRegions = new HashMap<>(mTouchableRegions.size());
getLocationOnScreen(mTmpInt);
for (Map.Entry<View, Rect> entry : mTouchableRegions.entrySet()) {
View child = entry.getKey();
Rect screenRegion = new Rect(entry.getValue());
screenRegion.offset(mTmpInt[0], mTmpInt[1]);
fullTouchRegions.put(child, screenRegion);
}
return fullTouchRegions;
}
@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 = findNearestChild(event);
mTouchingChild = mClickableChildren
.stream()
.filter(View::isAttachedToWindow)
.filter(view -> mTouchableRegions.get(view).contains(x, y))
.findFirst()
.orElse(null);
}
if (mTouchingChild != null) {
event.offsetLocation(mTouchingChild.getWidth() / 2 - event.getX(),
mTouchingChild.getHeight() / 2 - event.getY());
event.offsetLocation(mTouchingChild.getWidth() / 2 - x,
mTouchingChild.getHeight() / 2 - y);
return mTouchingChild.getVisibility() == VISIBLE
&& mTouchingChild.dispatchTouchEvent(event);
}
}
return super.onTouchEvent(event);
}
private View findNearestChild(MotionEvent event) {
if (mClickableChildren.isEmpty()) {
return null;
}
return mClickableChildren
.stream()
.filter(View::isAttachedToWindow)
.map(v -> new Pair<>(distance(v, event), v))
.min(Comparator.comparingInt(f -> f.first))
.map(data -> data.second)
.orElse(null);
}
private int distance(View v, MotionEvent event) {
v.getLocationInWindow(mTmpInt);
int left = mTmpInt[0] - mOffset[0];
int top = mTmpInt[1] - mOffset[1];
int right = left + v.getWidth();
int bottom = top + v.getHeight();
int x = Math.min(Math.abs(left - (int) event.getX()),
Math.abs((int) event.getX() - right));
int y = Math.min(Math.abs(top - (int) event.getY()),
Math.abs((int) event.getY() - bottom));
return Math.max(x, y);
}
}

View File

@@ -16,6 +16,7 @@
package com.android.systemui.navigationbar.buttons;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
@@ -25,6 +26,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper.RunWithLooper;
import android.view.MotionEvent;
@@ -39,6 +41,8 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Map;
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
@SmallTest
@@ -51,6 +55,7 @@ public class NearestTouchFrameTest extends SysuiTestCase {
Configuration c = new Configuration(mContext.getResources().getConfiguration());
c.smallestScreenWidthDp = 500;
mNearestTouchFrame = new NearestTouchFrame(mContext, null, c);
mNearestTouchFrame.layout(0, 0, 100, 100);
}
@Test
@@ -146,7 +151,7 @@ public class NearestTouchFrameTest extends SysuiTestCase {
public void testVerticalSelection_Top() {
View top = mockViewAt(0, 0, 10, 10);
View bottom = mockViewAt(0, 20, 10, 10);
mNearestTouchFrame.setIsVertical(true);
mNearestTouchFrame.addView(top);
mNearestTouchFrame.addView(bottom);
mNearestTouchFrame.onMeasure(0, 0);
@@ -162,7 +167,7 @@ public class NearestTouchFrameTest extends SysuiTestCase {
public void testVerticalSelection_Bottom() {
View top = mockViewAt(0, 0, 10, 10);
View bottom = mockViewAt(0, 20, 10, 10);
mNearestTouchFrame.setIsVertical(true);
mNearestTouchFrame.addView(top);
mNearestTouchFrame.addView(bottom);
mNearestTouchFrame.onMeasure(0, 0);
@@ -187,6 +192,60 @@ public class NearestTouchFrameTest extends SysuiTestCase {
ev.recycle();
}
@Test
public void testViewMiddleChildNotAttachedCrash() {
View view1 = mockViewAt(0, 20, 10, 10);
View view2 = mockViewAt(11, 20, 10, 10);
View view3 = mockViewAt(21, 20, 10, 10);
when(view2.isAttachedToWindow()).thenReturn(false);
mNearestTouchFrame.addView(view1);
mNearestTouchFrame.addView(view2);
mNearestTouchFrame.addView(view3);
mNearestTouchFrame.onMeasure(0, 0);
MotionEvent ev = MotionEvent.obtain(0, 0, 0, 5 /* x */, 18 /* y */, 0);
mNearestTouchFrame.onTouchEvent(ev);
verify(view2, never()).onTouchEvent(eq(ev));
ev.recycle();
}
@Test
public void testCachedRegionsSplit_horizontal() {
View left = mockViewAt(0, 0, 5, 20);
View right = mockViewAt(15, 0, 5, 20);
mNearestTouchFrame.layout(0, 0, 20, 20);
mNearestTouchFrame.addView(left);
mNearestTouchFrame.addView(right);
mNearestTouchFrame.onMeasure(0, 0);
Map<View, Rect> childRegions = mNearestTouchFrame.getFullTouchableChildRegions();
assertEquals(2, childRegions.size());
Rect leftRegion = childRegions.get(left);
Rect rightRegion = childRegions.get(right);
assertEquals(new Rect(0, 0, 9, 20), leftRegion);
assertEquals(new Rect(10, 0, 20, 20), rightRegion);
}
@Test
public void testCachedRegionsSplit_vertical() {
View top = mockViewAt(0, 0, 20, 5);
View bottom = mockViewAt(0, 15, 20, 5);
mNearestTouchFrame.layout(0, 0, 20, 20);
mNearestTouchFrame.setIsVertical(true);
mNearestTouchFrame.addView(top);
mNearestTouchFrame.addView(bottom);
mNearestTouchFrame.onMeasure(0, 0);
Map<View, Rect> childRegions = mNearestTouchFrame.getFullTouchableChildRegions();
assertEquals(2, childRegions.size());
Rect topRegion = childRegions.get(top);
Rect bottomRegion = childRegions.get(bottom);
assertEquals(new Rect(0, 0, 20, 9), topRegion);
assertEquals(new Rect(0, 10, 20, 20), bottomRegion);
}
private View mockViewAt(int x, int y, int width, int height) {
View v = spy(new View(mContext));
doAnswer(invocation -> {