Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1630b874 authored by Jason Monk's avatar Jason Monk Committed by android-build-merger
Browse files

Merge "Make it easier to click on phone nav buttons" into oc-dr1-dev am: 7d08ca6e am: 29b1a86a

am: 8a367113

Change-Id: I78a1446dd91cf19db9a866e12d4dccebac7bf684
parents fc1a8ac8 8a367113
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -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"
+2 −2
Original line number Diff line number Diff line
@@ -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"
+108 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}
+5 −0
Original line number Diff line number Diff line
@@ -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;
    }
+130 −0
Original line number Diff line number Diff line
/*
 * 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