diff options
2 files changed, 206 insertions, 36 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java index 34f721c94ed2..128af3702c49 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/MagnificationModeSwitch.java @@ -20,8 +20,13 @@ import android.annotation.NonNull; import android.content.Context; import android.content.pm.ActivityInfo; import android.graphics.PixelFormat; +import android.graphics.PointF; import android.provider.Settings; +import android.util.MathUtils; import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; import android.view.WindowManager; import android.widget.ImageView; @@ -31,7 +36,8 @@ import com.android.systemui.R; /** * Shows/hides a {@link android.widget.ImageView} on the screen and changes the values of * {@link Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE} when the UI is toggled. - * The button UI would automatically be dismissed after displaying for a period of time. + * The button icon is movable by dragging. And the button UI would automatically be dismissed after + * displaying for a period of time. */ class MagnificationModeSwitch { @@ -41,6 +47,10 @@ class MagnificationModeSwitch { private final Context mContext; private final WindowManager mWindowManager; private final ImageView mImageView; + private final PointF mLastDown = new PointF(); + private final PointF mLastDrag = new PointF(); + private final int mTapTimeout = ViewConfiguration.getTapTimeout(); + private final int mTouchSlop; private int mMagnificationMode = Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; private final WindowManager.LayoutParams mParams; private boolean mIsVisible = false; @@ -56,13 +66,10 @@ class MagnificationModeSwitch { Context.WINDOW_SERVICE); mParams = createLayoutParams(); mImageView = imageView; + mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); applyResourcesValues(); - mImageView.setOnClickListener( - view -> { - removeButton(); - toggleMagnificationMode(); - }); mImageView.setImageResource(getIconResId(mMagnificationMode)); + mImageView.setOnTouchListener(this::onTouch); } private void applyResourcesValues() { @@ -71,13 +78,59 @@ class MagnificationModeSwitch { mImageView.setPadding(padding, padding, padding, padding); } + private boolean onTouch(View v, MotionEvent event) { + if (!mIsVisible || mImageView == null) { + return false; + } + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mImageView.setAlpha(1.0f); + mImageView.animate().cancel(); + mLastDown.set(event.getRawX(), event.getRawY()); + mLastDrag.set(event.getRawX(), event.getRawY()); + return true; + case MotionEvent.ACTION_MOVE: + // Move the button position. + moveButton(event.getRawX() - mLastDrag.x, + event.getRawY() - mLastDrag.y); + mLastDrag.set(event.getRawX(), event.getRawY()); + return true; + case MotionEvent.ACTION_UP: + // Single tap to toggle magnification mode and the button position will be reset + // after the action is performed. + final float distance = MathUtils.dist(mLastDown.x, mLastDown.y, + event.getRawX(), event.getRawY()); + if ((event.getEventTime() - event.getDownTime()) <= mTapTimeout + && distance <= mTouchSlop) { + handleSingleTap(); + } else { + showButton(mMagnificationMode); + } + return true; + case MotionEvent.ACTION_CANCEL: + showButton(mMagnificationMode); + return true; + default: + return false; + } + } + + private void moveButton(float offsetX, float offsetY) { + mParams.x -= offsetX; + mParams.y -= offsetY; + mWindowManager.updateViewLayout(mImageView, mParams); + } + void removeButton() { if (!mIsVisible) { return; } mImageView.animate().cancel(); mWindowManager.removeView(mImageView); + // Reset button status. mIsVisible = false; + mParams.x = 0; + mParams.y = 0; } void showButton(int mode) { @@ -120,6 +173,11 @@ class MagnificationModeSwitch { Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE, newMode); } + private void handleSingleTap() { + removeButton(); + toggleMagnificationMode(); + } + private static ImageView createView(Context context) { ImageView imageView = new ImageView(context); imageView.setClickable(true); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java index ee151c441b68..cdbc647f152b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/MagnificationModeSwitchTest.java @@ -18,6 +18,10 @@ package com.android.systemui.accessibility; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; +import static android.view.MotionEvent.ACTION_CANCEL; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_UP; import static com.android.systemui.accessibility.MagnificationModeSwitch.getIconResId; @@ -27,6 +31,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -35,7 +40,9 @@ import android.content.Context; import android.content.pm.ActivityInfo; import android.provider.Settings; import android.testing.AndroidTestingRunner; +import android.view.MotionEvent; import android.view.View; +import android.view.ViewConfiguration; import android.view.ViewPropertyAnimator; import android.view.WindowManager; import android.widget.ImageView; @@ -48,35 +55,38 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidTestingRunner.class) public class MagnificationModeSwitchTest extends SysuiTestCase { - @Mock - private ImageView mMockImageView; + private ImageView mSpyImageView; @Mock private WindowManager mWindowManager; @Mock private ViewPropertyAnimator mViewPropertyAnimator; private MagnificationModeSwitch mMagnificationModeSwitch; + @Captor + private ArgumentCaptor<View.OnTouchListener> mTouchListenerCaptor; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); + WindowManager wm = mContext.getSystemService(WindowManager.class); + doAnswer(invocation -> + wm.getMaximumWindowMetrics() + ).when(mWindowManager).getMaximumWindowMetrics(); mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager); + mSpyImageView = Mockito.spy(new ImageView(mContext)); + doAnswer(invocation -> null).when(mSpyImageView).setOnTouchListener( + mTouchListenerCaptor.capture()); + initMockImageViewAndAnimator(); - when(mViewPropertyAnimator.setDuration(anyLong())).thenReturn(mViewPropertyAnimator); - when(mViewPropertyAnimator.alpha(anyFloat())).thenReturn(mViewPropertyAnimator); - when(mViewPropertyAnimator.setStartDelay(anyLong())).thenReturn(mViewPropertyAnimator); - when(mViewPropertyAnimator.withEndAction(any(Runnable.class))).thenReturn( - mViewPropertyAnimator); - - when(mMockImageView.animate()).thenReturn(mViewPropertyAnimator); - - mMagnificationModeSwitch = new MagnificationModeSwitch(mContext, mMockImageView); + mMagnificationModeSwitch = new MagnificationModeSwitch(mContext, mSpyImageView); } @Test @@ -85,7 +95,7 @@ public class MagnificationModeSwitchTest extends SysuiTestCase { mMagnificationModeSwitch.removeButton(); - verify(mWindowManager).removeView(mMockImageView); + verify(mWindowManager).removeView(mSpyImageView); // First invocation is in showButton. verify(mViewPropertyAnimator, times(2)).cancel(); } @@ -94,22 +104,19 @@ public class MagnificationModeSwitchTest extends SysuiTestCase { public void showWindowModeButton_fullscreenMode_addViewAndSetImageResource() { mMagnificationModeSwitch.showButton(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW); - verify(mMockImageView).setAlpha(1.0f); - verify(mMockImageView).setImageResource( + verify(mSpyImageView).setAlpha(1.0f); + verify(mSpyImageView).setImageResource( getIconResId(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW)); - verify(mViewPropertyAnimator).cancel(); - verify(mViewPropertyAnimator).setDuration(anyLong()); - verify(mViewPropertyAnimator).setStartDelay(anyLong()); - verify(mViewPropertyAnimator).alpha(anyFloat()); + assertShowButtonAnimation(); ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class); verify(mViewPropertyAnimator).withEndAction(captor.capture()); - verify(mWindowManager).addView(eq(mMockImageView), any(WindowManager.LayoutParams.class)); + verify(mWindowManager).addView(eq(mSpyImageView), any(WindowManager.LayoutParams.class)); captor.getValue().run(); // First invocation is in showButton. verify(mViewPropertyAnimator, times(2)).cancel(); - verify(mWindowManager).removeView(mMockImageView); + verify(mWindowManager).removeView(mSpyImageView); } @Test @@ -117,26 +124,131 @@ public class MagnificationModeSwitchTest extends SysuiTestCase { mMagnificationModeSwitch.showButton(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN); mMagnificationModeSwitch.onConfigurationChanged(ActivityInfo.CONFIG_DENSITY); - verify(mMockImageView, times(2)).setImageResource( + verify(mSpyImageView, times(2)).setImageResource( getIconResId(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN)); } @Test - public void performClick_fullscreenMode_removeViewAndChangeSettingsValue() { - ArgumentCaptor<View.OnClickListener> captor = ArgumentCaptor.forClass( - View.OnClickListener.class); - verify(mMockImageView).setOnClickListener(captor.capture()); + public void performSingleTap_fullscreenMode_removeViewAndChangeSettingsValue() { mMagnificationModeSwitch.showButton(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN); + resetMockImageViewAndAnimator(); - captor.getValue().onClick(mMockImageView); + // Perform a single-tap + final View.OnTouchListener listener = mTouchListenerCaptor.getValue(); + listener.onTouch(mSpyImageView, MotionEvent.obtain( + 0, 0, ACTION_DOWN, 100, 100, 0)); + verify(mViewPropertyAnimator).cancel(); - // First invocation is in showButton. - verify(mViewPropertyAnimator, times(2)).cancel(); - verify(mMockImageView).setImageResource( + resetMockImageViewAndAnimator(); + listener.onTouch(mSpyImageView, MotionEvent.obtain( + 0, ViewConfiguration.getTapTimeout(), ACTION_UP, 100, 100, 0)); + verify(mViewPropertyAnimator).cancel(); + verify(mSpyImageView).setImageResource( getIconResId(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW)); - verify(mWindowManager).removeView(mMockImageView); + verify(mWindowManager).removeView(mSpyImageView); final int actualMode = Settings.Secure.getInt(mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE, 0); assertEquals(ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW, actualMode); } + + @Test + public void showMagnificationButton_performDragging_updateViewLayout() { + mMagnificationModeSwitch.showButton(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN); + resetMockImageViewAndAnimator(); + + // Perform dragging + final View.OnTouchListener listener = mTouchListenerCaptor.getValue(); + final int offset = ViewConfiguration.get(mContext).getScaledTouchSlop(); + final int previousMode = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE, 0); + listener.onTouch(mSpyImageView, MotionEvent.obtain( + 0, 0, ACTION_DOWN, 100, 100, 0)); + verify(mSpyImageView).setAlpha(1.0f); + verify(mViewPropertyAnimator).cancel(); + + listener.onTouch(mSpyImageView, MotionEvent.obtain( + 0, ViewConfiguration.getTapTimeout(), ACTION_MOVE, 100 + offset, 100, 0)); + verify(mWindowManager).updateViewLayout(eq(mSpyImageView), + any(WindowManager.LayoutParams.class)); + + resetMockImageViewAndAnimator(); + listener.onTouch(mSpyImageView, MotionEvent.obtain( + 0, ViewConfiguration.getTapTimeout() + 10, ACTION_UP, 100 + offset, 100, 0)); + verify(mSpyImageView).setAlpha(1.0f); + assertModeUnchanged(previousMode); + assertShowButtonAnimation(); + } + + @Test + public void performSingleTapActionCanceled_showButtonAnimation() { + mMagnificationModeSwitch.showButton(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN); + resetMockImageViewAndAnimator(); + + // Perform single tap + final View.OnTouchListener listener = mTouchListenerCaptor.getValue(); + final int previousMode = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE, 0); + listener.onTouch(mSpyImageView, MotionEvent.obtain( + 0, 0, ACTION_DOWN, 100, 100, 0)); + + resetMockImageViewAndAnimator(); + listener.onTouch(mSpyImageView, MotionEvent.obtain( + 0, ViewConfiguration.getTapTimeout(), ACTION_CANCEL, 100, 100, 0)); + verify(mSpyImageView).setAlpha(1.0f); + assertModeUnchanged(previousMode); + assertShowButtonAnimation(); + } + + @Test + public void performDraggingActionCanceled_showButtonAnimation() { + mMagnificationModeSwitch.showButton(ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN); + resetMockImageViewAndAnimator(); + + // Perform dragging + final View.OnTouchListener listener = mTouchListenerCaptor.getValue(); + final int offset = ViewConfiguration.get(mContext).getScaledTouchSlop(); + final int previousMode = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE, 0); + listener.onTouch(mSpyImageView, MotionEvent.obtain( + 0, 0, ACTION_DOWN, 100, 100, 0)); + listener.onTouch(mSpyImageView, MotionEvent.obtain( + 0, ViewConfiguration.getTapTimeout(), ACTION_MOVE, 100 + offset, 100, 0)); + + resetMockImageViewAndAnimator(); + listener.onTouch(mSpyImageView, MotionEvent.obtain( + 0, ViewConfiguration.getTapTimeout(), ACTION_CANCEL, 100 + offset, 100, 0)); + verify(mSpyImageView).setAlpha(1.0f); + assertModeUnchanged(previousMode); + assertShowButtonAnimation(); + } + + private void assertModeUnchanged(int expectedMode) { + final int actualMode = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE, 0); + assertEquals(expectedMode, actualMode); + } + + private void assertShowButtonAnimation() { + verify(mViewPropertyAnimator).cancel(); + verify(mViewPropertyAnimator).setDuration(anyLong()); + verify(mViewPropertyAnimator).setStartDelay(anyLong()); + verify(mViewPropertyAnimator).alpha(anyFloat()); + verify(mViewPropertyAnimator).start(); + } + + private void initMockImageViewAndAnimator() { + when(mViewPropertyAnimator.setDuration(anyLong())).thenReturn(mViewPropertyAnimator); + when(mViewPropertyAnimator.alpha(anyFloat())).thenReturn(mViewPropertyAnimator); + when(mViewPropertyAnimator.setStartDelay(anyLong())).thenReturn(mViewPropertyAnimator); + when(mViewPropertyAnimator.withEndAction(any(Runnable.class))).thenReturn( + mViewPropertyAnimator); + + when(mSpyImageView.animate()).thenReturn(mViewPropertyAnimator); + } + + private void resetMockImageViewAndAnimator() { + Mockito.reset(mViewPropertyAnimator); + Mockito.reset(mSpyImageView); + initMockImageViewAndAnimator(); + } } |