Merge "Cache locations of where nearest child is." into sc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
7330bd4e8c
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
Reference in New Issue
Block a user