diff options
5 files changed, 247 insertions, 4 deletions
diff --git a/packages/SystemUI/res/layout/navigation_layout.xml b/packages/SystemUI/res/layout/navigation_layout.xml index a621c7c0f836..53f5dfe1acbe 100644 --- a/packages/SystemUI/res/layout/navigation_layout.xml +++ b/packages/SystemUI/res/layout/navigation_layout.xml @@ -24,7 +24,7 @@ android:paddingStart="8dp" android:paddingEnd="8dp"> - <FrameLayout + <com.android.systemui.statusbar.phone.NearestTouchFrame android:id="@+id/nav_buttons" android:layout_width="match_parent" android:layout_height="match_parent"> @@ -44,7 +44,7 @@ android:orientation="horizontal" android:clipChildren="false" /> - </FrameLayout> + </com.android.systemui.statusbar.phone.NearestTouchFrame> <com.android.systemui.statusbar.policy.DeadZone android:id="@+id/deadzone" diff --git a/packages/SystemUI/res/layout/navigation_layout_rot90.xml b/packages/SystemUI/res/layout/navigation_layout_rot90.xml index bf48c7f337d7..39cdff47c5ac 100644 --- a/packages/SystemUI/res/layout/navigation_layout_rot90.xml +++ b/packages/SystemUI/res/layout/navigation_layout_rot90.xml @@ -24,7 +24,7 @@ android:paddingTop="8dp" android:paddingBottom="8dp"> - <FrameLayout + <com.android.systemui.statusbar.phone.NearestTouchFrame android:id="@+id/nav_buttons" android:layout_width="match_parent" android:layout_height="match_parent"> @@ -44,7 +44,7 @@ android:orientation="vertical" android:clipChildren="false" /> - </FrameLayout> + </com.android.systemui.statusbar.phone.NearestTouchFrame> <com.android.systemui.statusbar.policy.DeadZone android:id="@+id/deadzone" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NearestTouchFrame.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NearestTouchFrame.java new file mode 100644 index 000000000000..8bc656354012 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NearestTouchFrame.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2017 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.systemui.statusbar.phone; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Pair; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import com.android.systemui.R; + +import java.util.ArrayList; +import java.util.Comparator; + +/** + * 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 ArrayList<View> mClickableChildren = new ArrayList<>(); + private final boolean mIsActive; + private final int[] mTmpInt = new int[2]; + private final int[] mOffset = new int[2]; + private View mTouchingChild; + + public NearestTouchFrame(Context context, AttributeSet attrs) { + super(context, attrs); + mIsActive = context.getResources().getConfiguration().smallestScreenWidthDp < 600; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + mClickableChildren.clear(); + addClickableChildren(this); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + getLocationInWindow(mOffset); + } + + 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) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + mTouchingChild = findNearestChild(event); + } + if (mTouchingChild != null) { + event.offsetLocation(mTouchingChild.getWidth() / 2 - event.getX(), + mTouchingChild.getHeight() / 2 - event.getY()); + return mTouchingChild.dispatchTouchEvent(event); + } + } + return super.onTouchEvent(event); + } + + private View findNearestChild(MotionEvent event) { + return mClickableChildren.stream().map(v -> new Pair<>(distance(v, event), v)) + .min(Comparator.comparingInt(f -> f.first)).get().second; + } + + 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/src/com/android/systemui/statusbar/policy/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java index 08ea5439f6f3..65bfabd19fbb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonView.java @@ -113,6 +113,11 @@ public class KeyButtonView extends ImageView implements ButtonInterface { setBackground(mRipple); } + @Override + public boolean isClickable() { + return mCode != 0 || super.isClickable(); + } + public void setCode(int code) { mCode = code; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NearestTouchFrameTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NearestTouchFrameTest.java new file mode 100644 index 000000000000..577dc52e2290 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NearestTouchFrameTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2017 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.systemui.statusbar.phone; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.support.test.filters.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper.RunWithLooper; +import android.view.MotionEvent; +import android.view.View; + +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidTestingRunner.class) +@RunWithLooper +@SmallTest +public class NearestTouchFrameTest extends SysuiTestCase { + + private NearestTouchFrame mNearestTouchFrame; + + @Before + public void setup() { + mNearestTouchFrame = new NearestTouchFrame(mContext, null); + } + + @Test + public void testHorizontalSelection_Left() { + View left = mockViewAt(0, 0, 10, 10); + View right = mockViewAt(20, 0, 10, 10); + + mNearestTouchFrame.addView(left); + mNearestTouchFrame.addView(right); + mNearestTouchFrame.onMeasure(0, 0); + + MotionEvent ev = MotionEvent.obtain(0, 0, 0, + 12 /* x */, 5 /* y */, 0); + mNearestTouchFrame.onTouchEvent(ev); + verify(left).onTouchEvent(eq(ev)); + ev.recycle(); + } + + @Test + public void testHorizontalSelection_Right() { + View left = mockViewAt(0, 0, 10, 10); + View right = mockViewAt(20, 0, 10, 10); + + mNearestTouchFrame.addView(left); + mNearestTouchFrame.addView(right); + mNearestTouchFrame.onMeasure(0, 0); + + MotionEvent ev = MotionEvent.obtain(0, 0, 0, + 18 /* x */, 5 /* y */, 0); + mNearestTouchFrame.onTouchEvent(ev); + verify(right).onTouchEvent(eq(ev)); + ev.recycle(); + } + + @Test + public void testVerticalSelection_Top() { + View top = mockViewAt(0, 0, 10, 10); + View bottom = mockViewAt(0, 20, 10, 10); + + mNearestTouchFrame.addView(top); + mNearestTouchFrame.addView(bottom); + mNearestTouchFrame.onMeasure(0, 0); + + MotionEvent ev = MotionEvent.obtain(0, 0, 0, + 5 /* x */, 12 /* y */, 0); + mNearestTouchFrame.onTouchEvent(ev); + verify(top).onTouchEvent(eq(ev)); + ev.recycle(); + } + + @Test + public void testVerticalSelection_Bottom() { + View top = mockViewAt(0, 0, 10, 10); + View bottom = mockViewAt(0, 20, 10, 10); + + mNearestTouchFrame.addView(top); + mNearestTouchFrame.addView(bottom); + mNearestTouchFrame.onMeasure(0, 0); + + MotionEvent ev = MotionEvent.obtain(0, 0, 0, + 5 /* x */, 18 /* y */, 0); + mNearestTouchFrame.onTouchEvent(ev); + verify(bottom).onTouchEvent(eq(ev)); + ev.recycle(); + } + + private View mockViewAt(int x, int y, int width, int height) { + View v = spy(new View(mContext)); + doAnswer(invocation -> { + int[] pos = (int[]) invocation.getArguments()[0]; + pos[0] = x; + pos[1] = y; + return null; + }).when(v).getLocationInWindow(any()); + when(v.isClickable()).thenReturn(true); + + // Stupid final methods. + v.setLeft(0); + v.setRight(width); + v.setTop(0); + v.setBottom(height); + return v; + } +}
\ No newline at end of file |