Merge "Make LockPatternView dot hit area circular." into tm-dev

This commit is contained in:
TreeHugger Robot
2022-03-09 18:21:53 +00:00
committed by Android (Google) Code Review
4 changed files with 288 additions and 72 deletions

View File

@@ -45,6 +45,7 @@ import android.util.AttributeSet;
import android.util.IntArray;
import android.util.Log;
import android.util.SparseArray;
import android.util.TypedValue;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.RenderNodeAnimator;
@@ -82,10 +83,12 @@ public class LockPatternView extends View {
private static final int DOT_ACTIVATION_DURATION_MILLIS = 50;
private static final int DOT_RADIUS_INCREASE_DURATION_MILLIS = 96;
private static final int DOT_RADIUS_DECREASE_DURATION_MILLIS = 192;
private static final float MIN_DOT_HIT_FACTOR = 0.2f;
private final CellState[][] mCellStates;
private final int mDotSize;
private final int mDotSizeActivated;
private final float mDotHitFactor;
private final int mPathWidth;
private boolean mDrawingProfilingStarted = false;
@@ -143,12 +146,11 @@ public class LockPatternView extends View {
private boolean mPatternInProgress = false;
private boolean mFadePattern = true;
private float mHitFactor = 0.6f;
@UnsupportedAppUsage
private float mSquareWidth;
@UnsupportedAppUsage
private float mSquareHeight;
private float mDotHitRadius;
private final LinearGradient mFadeOutGradientShader;
private final Path mCurrentPath = new Path();
@@ -164,8 +166,7 @@ public class LockPatternView extends View {
private final Interpolator mFastOutSlowInInterpolator;
private final Interpolator mLinearOutSlowInInterpolator;
private PatternExploreByTouchHelper mExploreByTouchHelper;
private AudioManager mAudioManager;
private final PatternExploreByTouchHelper mExploreByTouchHelper;
private Drawable mSelectedDrawable;
private Drawable mNotSelectedDrawable;
@@ -349,6 +350,9 @@ public class LockPatternView extends View {
mDotSize = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_size);
mDotSizeActivated = getResources().getDimensionPixelSize(
R.dimen.lock_pattern_dot_size_activated);
TypedValue outValue = new TypedValue();
getResources().getValue(R.dimen.lock_pattern_dot_hit_factor, outValue, true);
mDotHitFactor = Math.max(Math.min(outValue.getFloat(), 1f), MIN_DOT_HIT_FACTOR);
mUseLockPatternDrawable = getResources().getBoolean(R.bool.use_lock_pattern_drawable);
if (mUseLockPatternDrawable) {
@@ -375,7 +379,6 @@ public class LockPatternView extends View {
AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in);
mExploreByTouchHelper = new PatternExploreByTouchHelper(this);
setAccessibilityDelegate(mExploreByTouchHelper);
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
int fadeAwayGradientWidth = getResources().getDimensionPixelSize(
R.dimen.lock_pattern_fade_away_gradient_width);
@@ -679,6 +682,7 @@ public class LockPatternView extends View {
final int height = h - mPaddingTop - mPaddingBottom;
mSquareHeight = height / 3.0f;
mExploreByTouchHelper.invalidateRoot();
mDotHitRadius = Math.min(mSquareHeight / 2, mSquareWidth / 2) * mDotHitFactor;
if (mUseLockPatternDrawable) {
mNotSelectedDrawable.setBounds(mPaddingLeft, mPaddingTop, width, height);
@@ -890,63 +894,30 @@ public class LockPatternView extends View {
return set;
}
// helper method to find which cell a point maps to
@Nullable
private Cell checkForNewHit(float x, float y) {
final int rowHit = getRowHit(y);
if (rowHit < 0) {
return null;
Cell cellHit = detectCellHit(x, y);
if (cellHit != null && !mPatternDrawLookup[cellHit.row][cellHit.column]) {
return cellHit;
}
final int columnHit = getColumnHit(x);
if (columnHit < 0) {
return null;
}
if (mPatternDrawLookup[rowHit][columnHit]) {
return null;
}
return Cell.of(rowHit, columnHit);
return null;
}
/**
* Helper method to find the row that y falls into.
* @param y The y coordinate
* @return The row that y falls in, or -1 if it falls in no row.
*/
private int getRowHit(float y) {
final float squareHeight = mSquareHeight;
float hitSize = squareHeight * mHitFactor;
float offset = mPaddingTop + (squareHeight - hitSize) / 2f;
for (int i = 0; i < 3; i++) {
final float hitTop = offset + squareHeight * i;
if (y >= hitTop && y <= hitTop + hitSize) {
return i;
/** Helper method to find which cell a point maps to. */
@Nullable
private Cell detectCellHit(float x, float y) {
final float hitRadiusSquared = mDotHitRadius * mDotHitRadius;
for (int row = 0; row < 3; row++) {
for (int column = 0; column < 3; column++) {
float centerY = getCenterYForRow(row);
float centerX = getCenterXForColumn(column);
if ((x - centerX) * (x - centerX) + (y - centerY) * (y - centerY)
< hitRadiusSquared) {
return Cell.of(row, column);
}
}
}
return -1;
}
/**
* Helper method to find the column x fallis into.
* @param x The x coordinate.
* @return The column that x falls in, or -1 if it falls in no column.
*/
private int getColumnHit(float x) {
final float squareWidth = mSquareWidth;
float hitSize = squareWidth * mHitFactor;
float offset = mPaddingLeft + (squareWidth - hitSize) / 2f;
for (int i = 0; i < 3; i++) {
final float hitLeft = offset + squareWidth * i;
if (x >= hitLeft && x <= hitLeft + hitSize) {
return i;
}
}
return -1;
return null;
}
@Override
@@ -1553,8 +1524,7 @@ public class LockPatternView extends View {
protected int getVirtualViewAt(float x, float y) {
// This must use the same hit logic for the screen to ensure consistency whether
// accessibility is on or off.
int id = getVirtualViewIdForHit(x, y);
return id;
return getVirtualViewIdForHit(x, y);
}
@Override
@@ -1670,12 +1640,11 @@ public class LockPatternView extends View {
final int col = ordinal % 3;
float centerX = getCenterXForColumn(col);
float centerY = getCenterYForRow(row);
float cellheight = mSquareHeight * mHitFactor * 0.5f;
float cellwidth = mSquareWidth * mHitFactor * 0.5f;
bounds.left = (int) (centerX - cellwidth);
bounds.right = (int) (centerX + cellwidth);
bounds.top = (int) (centerY - cellheight);
bounds.bottom = (int) (centerY + cellheight);
float cellHitRadius = mDotHitRadius;
bounds.left = (int) (centerX - cellHitRadius);
bounds.right = (int) (centerX + cellHitRadius);
bounds.top = (int) (centerY - cellHitRadius);
bounds.bottom = (int) (centerY + cellHitRadius);
return bounds;
}
@@ -1694,16 +1663,12 @@ public class LockPatternView extends View {
* @return VIRTUAL_BASE_VIEW_ID+id or 0 if no view was hit
*/
private int getVirtualViewIdForHit(float x, float y) {
final int rowHit = getRowHit(y);
if (rowHit < 0) {
Cell cellHit = detectCellHit(x, y);
if (cellHit == null) {
return ExploreByTouchHelper.INVALID_ID;
}
final int columnHit = getColumnHit(x);
if (columnHit < 0) {
return ExploreByTouchHelper.INVALID_ID;
}
boolean dotAvailable = mPatternDrawLookup[rowHit][columnHit];
int dotId = (rowHit * 3 + columnHit) + VIRTUAL_BASE_VIEW_ID;
boolean dotAvailable = mPatternDrawLookup[cellHit.row][cellHit.column];
int dotId = (cellHit.row * 3 + cellHit.column) + VIRTUAL_BASE_VIEW_ID;
int view = dotAvailable ? dotId : ExploreByTouchHelper.INVALID_ID;
if (DEBUG_A11Y) Log.v(TAG, "getVirtualViewIdForHit(" + x + "," + y + ") => "
+ view + "avail =" + dotAvailable);

View File

@@ -668,6 +668,9 @@
<dimen name="lock_pattern_dot_line_width">22dp</dimen>
<dimen name="lock_pattern_dot_size">14dp</dimen>
<dimen name="lock_pattern_dot_size_activated">30dp</dimen>
<!-- How much of the cell space is classified as hit areas [0..1] where 1 means that hit area is
a circle with diameter equals to cell minimum side min(width, height). -->
<item type="dimen" format="float" name="lock_pattern_dot_hit_factor">0.6</item>
<!-- Width of a gradient applied to a lock pattern line while its disappearing animation. -->
<dimen name="lock_pattern_fade_away_gradient_width">8dp</dimen>

View File

@@ -1325,6 +1325,7 @@
<java-symbol type="dimen" name="lock_pattern_dot_line_width" />
<java-symbol type="dimen" name="lock_pattern_dot_size" />
<java-symbol type="dimen" name="lock_pattern_dot_size_activated" />
<java-symbol type="dimen" name="lock_pattern_dot_hit_factor" />
<java-symbol type="dimen" name="lock_pattern_fade_away_gradient_width" />
<java-symbol type="drawable" name="clock_dial" />
<java-symbol type="drawable" name="clock_hand_hour" />

View File

@@ -0,0 +1,247 @@
/*
* Copyright (C) 2022 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.internal.widget;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import android.content.Context;
import androidx.test.annotation.UiThreadTest;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toolbar;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.rule.UiThreadTestRule;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import com.android.internal.R;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
@RunWith(Parameterized.class)
@SmallTest
public class LockPatternViewTest {
@Rule
public UiThreadTestRule uiThreadTestRule = new UiThreadTestRule();
private final int mViewSize;
private final float mDefaultError;
private final float mDot1x;
private final float mDot1y;
private final float mDot2x;
private final float mDot2y;
private final float mDot3x;
private final float mDot3y;
private final float mDot5x;
private final float mDot5y;
private final float mDot7x;
private final float mDot7y;
private final float mDot9x;
private final float mDot9y;
private Context mContext;
private LockPatternView mLockPatternView;
@Mock
private LockPatternView.OnPatternListener mPatternListener;
@Captor
private ArgumentCaptor<List<LockPatternView.Cell>> mCellsArgumentCaptor;
public LockPatternViewTest(int viewSize) {
mViewSize = viewSize;
float cellSize = viewSize / 3f;
mDefaultError = cellSize * 0.2f;
mDot1x = cellSize / 2f;
mDot1y = cellSize / 2f;
mDot2x = cellSize + mDot1x;
mDot2y = mDot1y;
mDot3x = cellSize + mDot2x;
mDot3y = mDot1y;
// dot4 is skipped as redundant
mDot5x = cellSize + mDot1x;
mDot5y = cellSize + mDot1y;
// dot6 is skipped as redundant
mDot7x = mDot1x;
mDot7y = cellSize * 2 + mDot1y;
// dot8 is skipped as redundant
mDot9x = cellSize * 2 + mDot7x;
mDot9y = mDot7y;
}
@Parameterized.Parameters
public static Collection primeNumbers() {
return Arrays.asList(192, 512, 768, 1024);
}
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mContext = InstrumentationRegistry.getContext();
mLockPatternView = new LockPatternView(mContext, null);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(mViewSize,
View.MeasureSpec.EXACTLY);
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(mViewSize,
View.MeasureSpec.EXACTLY);
mLockPatternView.measure(widthMeasureSpec, heightMeasureSpec);
mLockPatternView.layout(0, 0, mLockPatternView.getMeasuredWidth(),
mLockPatternView.getMeasuredHeight());
}
@UiThreadTest
@Test
public void downStartsPattern() {
mLockPatternView.setOnPatternListener(mPatternListener);
mLockPatternView.onTouchEvent(
MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, mDot1x, mDot1y, 1));
verify(mPatternListener).onPatternStart();
}
@UiThreadTest
@Test
public void up_completesPattern() {
mLockPatternView.setOnPatternListener(mPatternListener);
mLockPatternView.onTouchEvent(
MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, mDot1x, mDot1y, 1));
mLockPatternView.onTouchEvent(
MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, mDot1x, mDot1y, 1));
verify(mPatternListener).onPatternDetected(any());
}
@UiThreadTest
@Test
public void moveToDot_hitsDot() {
mLockPatternView.setOnPatternListener(mPatternListener);
mLockPatternView.onTouchEvent(
MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1f, 1f, 1));
mLockPatternView.onTouchEvent(
MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, mDot1x, mDot1y, 1));
verify(mPatternListener).onPatternStart();
}
@UiThreadTest
@Test
public void moveOutside_doesNotHitsDot() {
mLockPatternView.setOnPatternListener(mPatternListener);
mLockPatternView.onTouchEvent(
MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1f, 1f, 1));
mLockPatternView.onTouchEvent(
MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, 2f, 2f, 1));
verify(mPatternListener, never()).onPatternStart();
}
@UiThreadTest
@Test
public void moveAlongTwoDots_hitsTwo() {
mLockPatternView.setOnPatternListener(mPatternListener);
mLockPatternView.onTouchEvent(
MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1f, 1f, 1));
makeMove(mDot1x, mDot1y, mDot2x, mDot2y, 6);
mLockPatternView.onTouchEvent(
MotionEvent.obtain(0, 3, MotionEvent.ACTION_UP, mDot2x, mDot2y, 1));
verify(mPatternListener).onPatternDetected(mCellsArgumentCaptor.capture());
List<LockPatternView.Cell> patternCells = mCellsArgumentCaptor.getValue();
assertThat(patternCells, hasSize(2));
assertThat(patternCells,
contains(LockPatternView.Cell.of(0, 0), LockPatternView.Cell.of(0, 1)));
}
@UiThreadTest
@Test
public void moveAlongTwoDotsDiagonally_hitsTwo() {
mLockPatternView.setOnPatternListener(mPatternListener);
mLockPatternView.onTouchEvent(
MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1f, 1f, 1));
makeMove(mDot1x, mDot1y, mDot5x, mDot5y, 6);
mLockPatternView.onTouchEvent(
MotionEvent.obtain(0, 3, MotionEvent.ACTION_UP, mDot5x, mDot5y, 1));
verify(mPatternListener).onPatternDetected(mCellsArgumentCaptor.capture());
List<LockPatternView.Cell> patternCells = mCellsArgumentCaptor.getValue();
assertThat(patternCells, hasSize(2));
assertThat(patternCells,
contains(LockPatternView.Cell.of(0, 0), LockPatternView.Cell.of(1, 1)));
}
@UiThreadTest
@Test
public void moveAlongZPattern_hitsDots() {
mLockPatternView.setOnPatternListener(mPatternListener);
mLockPatternView.onTouchEvent(
MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1f, 1f, 1));
makeMove(mDot1x, mDot1y, mDot3x + mDefaultError, mDot3y, 10);
makeMove(mDot3x - mDefaultError, mDot3y, mDot7x, mDot7y, 10);
makeMove(mDot7x, mDot7y - mDefaultError, mDot9x, mDot9y - mDefaultError, 10);
mLockPatternView.onTouchEvent(
MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, mViewSize - mDefaultError,
mViewSize - mDefaultError, 1));
verify(mPatternListener).onPatternDetected(mCellsArgumentCaptor.capture());
List<LockPatternView.Cell> patternCells = mCellsArgumentCaptor.getValue();
assertThat(patternCells, hasSize(7));
assertThat(patternCells,
contains(LockPatternView.Cell.of(0, 0),
LockPatternView.Cell.of(0, 1),
LockPatternView.Cell.of(0, 2),
LockPatternView.Cell.of(1, 1),
LockPatternView.Cell.of(2, 0),
LockPatternView.Cell.of(2, 1),
LockPatternView.Cell.of(2, 2)));
}
private void makeMove(float xFrom, float yFrom, float xTo, float yTo, int numberOfSteps) {
for (int i = 0; i < numberOfSteps; i++) {
float progress = i / (numberOfSteps - 1f);
float rest = 1f - progress;
mLockPatternView.onTouchEvent(
MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE,
/* x= */ xFrom * rest + xTo * progress,
/* y= */ yFrom * rest + yTo * progress,
1));
}
}
}