diff options
| author | 2021-02-19 19:34:53 +0000 | |
|---|---|---|
| committer | 2021-02-19 19:34:53 +0000 | |
| commit | 7330bd4e8cbe7027e1acfcb6efa7927d7b89ee49 (patch) | |
| tree | 9bce06f133fbc64662c320d0941986cd7dfbacd2 | |
| parent | 792ba79a874ee5f6dee13e1e4547d9a92ec77c67 (diff) | |
| parent | 99d189d14e925e3f7402e902b63496d3ace0f632 (diff) | |
Merge "Cache locations of where nearest child is." into sc-dev
7 files changed, 231 insertions, 38 deletions
diff --git a/packages/SystemUI/res/layout/navigation_layout.xml b/packages/SystemUI/res/layout/navigation_layout.xml index 0e576fbe2245..64c7422c27ed 100644 --- a/packages/SystemUI/res/layout/navigation_layout.xml +++ b/packages/SystemUI/res/layout/navigation_layout.xml @@ -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" diff --git a/packages/SystemUI/res/layout/navigation_layout_vertical.xml b/packages/SystemUI/res/layout/navigation_layout_vertical.xml index 4b6770042632..42e93249e95f 100644 --- a/packages/SystemUI/res/layout/navigation_layout_vertical.xml +++ b/packages/SystemUI/res/layout/navigation_layout_vertical.xml @@ -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" diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml index a1191aeacdde..6e458681bbab 100644 --- a/packages/SystemUI/res/values/attrs.xml +++ b/packages/SystemUI/res/values/attrs.xml @@ -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" /> diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index 1660deabdd72..553623702aed 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -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; diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java index c07404c2e34d..8e75bec72c15 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java @@ -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); } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/NearestTouchFrame.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/NearestTouchFrame.java index 88c8fea085fb..b1c85b5c530e 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/NearestTouchFrame.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/NearestTouchFrame.java @@ -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); - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/buttons/NearestTouchFrameTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/buttons/NearestTouchFrameTest.java index 0320103ceaa8..5c179d4e46b6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/buttons/NearestTouchFrameTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/buttons/NearestTouchFrameTest.java @@ -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 -> { |