feat(magnification): move mag button when window reaches the screen edge
Wait to move it until drag is done, if they are dragging by the handle. But if it is moving via two-finger drag or triple-tap, update the drag handle immediately in real-time. Add tests to verify applyTapExcludeRegion() respects the drag handle position. This required fixing a bug in earlier code where it was adding extra padding around the drag handle. Now we only exclude the tap area of the handle, no padding. Bug: b/205904754 Test: unit and manual: 1. Drag the magnification window to the right edge of the screen. The button should swap to the left. 2. Tap the button. It should bounce the frame. 3. Drag back to the center. The button should return to the right. Change-Id: I04af438ad62359ac31ba12aee0a1739e25068937
This commit is contained in:
@@ -77,8 +77,7 @@
|
||||
android:layout_height="@dimen/magnification_drag_view_size"
|
||||
android:layout_margin="@dimen/magnification_inner_border_margin"
|
||||
android:layout_gravity="right|bottom"
|
||||
android:paddingEnd="@dimen/magnifier_drag_handle_padding"
|
||||
android:paddingBottom="@dimen/magnifier_drag_handle_padding"
|
||||
android:padding="@dimen/magnifier_drag_handle_padding"
|
||||
android:scaleType="center"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="@drawable/ic_move_magnification"/>
|
||||
|
||||
@@ -1036,6 +1036,9 @@
|
||||
<!-- The extra padding to show the whole outer border -->
|
||||
<dimen name="magnifier_drag_handle_padding">3dp</dimen>
|
||||
<dimen name="magnification_max_frame_size">300dp</dimen>
|
||||
<!-- How far from the right edge of the screen you need to drag the window before the button
|
||||
repositions to the other side. -->
|
||||
<dimen name="magnification_button_reposition_threshold_from_edge">32dp</dimen>
|
||||
|
||||
<!-- Home Controls -->
|
||||
<dimen name="controls_header_menu_size">48dp</dimen>
|
||||
|
||||
@@ -28,6 +28,7 @@ import android.hardware.display.DisplayManager;
|
||||
import android.os.Handler;
|
||||
import android.view.Display;
|
||||
import android.view.SurfaceControl;
|
||||
import android.view.WindowManagerGlobal;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
import android.view.accessibility.IRemoteMagnificationAnimationCallback;
|
||||
import android.view.accessibility.IWindowMagnificationConnection;
|
||||
@@ -89,9 +90,14 @@ public class WindowMagnification extends CoreStartable implements WindowMagnifie
|
||||
TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, /* options */ null);
|
||||
return new WindowMagnificationController(
|
||||
windowContext,
|
||||
mHandler, new WindowMagnificationAnimationController(windowContext),
|
||||
new SfVsyncFrameCallbackProvider(), null,
|
||||
new SurfaceControl.Transaction(), mWindowMagnifierCallback, mSysUiState);
|
||||
mHandler,
|
||||
new WindowMagnificationAnimationController(windowContext),
|
||||
new SfVsyncFrameCallbackProvider(),
|
||||
null,
|
||||
new SurfaceControl.Transaction(),
|
||||
mWindowMagnifierCallback,
|
||||
mSysUiState,
|
||||
WindowManagerGlobal::getWindowSession);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,11 +57,11 @@ import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.view.WindowManagerGlobal;
|
||||
import android.view.WindowMetrics;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
|
||||
import android.view.accessibility.IRemoteMagnificationAnimationCallback;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.core.math.MathUtils;
|
||||
|
||||
@@ -75,6 +75,7 @@ import java.io.PrintWriter;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Class to handle adding and removing a window magnification.
|
||||
@@ -92,6 +93,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold
|
||||
private static final Range<Float> A11Y_ACTION_SCALE_RANGE = new Range<>(2.0f, 8.0f);
|
||||
private static final float A11Y_CHANGE_SCALE_DIFFERENCE = 1.0f;
|
||||
private static final float ANIMATION_BOUNCE_EFFECT_SCALE = 1.05f;
|
||||
|
||||
private final Context mContext;
|
||||
private final Resources mResources;
|
||||
private final Handler mHandler;
|
||||
@@ -163,8 +165,13 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold
|
||||
private SurfaceView mMirrorSurfaceView;
|
||||
private int mMirrorSurfaceMargin;
|
||||
private int mBorderDragSize;
|
||||
private int mDragViewSize;
|
||||
private int mOuterBorderSize;
|
||||
|
||||
/**
|
||||
* How far from the right edge of the screen you need to drag the window before the button
|
||||
* repositions to the other side.
|
||||
*/
|
||||
private int mButtonRepositionThresholdFromEdge;
|
||||
// The boundary of magnification frame.
|
||||
private final Rect mMagnificationFrameBoundary = new Rect();
|
||||
// The top Y of the system gesture rect at the bottom. Set to -1 if it is invalid.
|
||||
@@ -172,6 +179,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold
|
||||
private int mMinWindowSize;
|
||||
|
||||
private final WindowMagnificationAnimationController mAnimationController;
|
||||
private final Supplier<IWindowSession> mGlobalWindowSessionSupplier;
|
||||
private final SfVsyncFrameCallbackProvider mSfVsyncFrameProvider;
|
||||
private final MagnificationGestureDetector mGestureDetector;
|
||||
private final int mBounceEffectDuration;
|
||||
@@ -182,18 +190,25 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold
|
||||
private SysUiState mSysUiState;
|
||||
// Set it to true when the view is overlapped with the gesture insets at the bottom.
|
||||
private boolean mOverlapWithGestureInsets;
|
||||
private boolean mIsDragging;
|
||||
|
||||
@Nullable
|
||||
private MirrorWindowControl mMirrorWindowControl;
|
||||
|
||||
WindowMagnificationController(@UiContext Context context, @NonNull Handler handler,
|
||||
WindowMagnificationController(
|
||||
@UiContext Context context,
|
||||
@NonNull Handler handler,
|
||||
@NonNull WindowMagnificationAnimationController animationController,
|
||||
SfVsyncFrameCallbackProvider sfVsyncFrameProvider,
|
||||
MirrorWindowControl mirrorWindowControl, SurfaceControl.Transaction transaction,
|
||||
@NonNull WindowMagnifierCallback callback, SysUiState sysUiState) {
|
||||
MirrorWindowControl mirrorWindowControl,
|
||||
SurfaceControl.Transaction transaction,
|
||||
@NonNull WindowMagnifierCallback callback,
|
||||
SysUiState sysUiState,
|
||||
@NonNull Supplier<IWindowSession> globalWindowSessionSupplier) {
|
||||
mContext = context;
|
||||
mHandler = handler;
|
||||
mAnimationController = animationController;
|
||||
mGlobalWindowSessionSupplier = globalWindowSessionSupplier;
|
||||
mAnimationController.setWindowMagnificationController(this);
|
||||
mSfVsyncFrameProvider = sfVsyncFrameProvider;
|
||||
mWindowMagnifierCallback = callback;
|
||||
@@ -249,8 +264,8 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold
|
||||
};
|
||||
|
||||
mMirrorSurfaceViewLayoutChangeListener =
|
||||
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom)
|
||||
-> applyTapExcludeRegion();
|
||||
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
|
||||
mMirrorView.post(this::applyTapExcludeRegion);
|
||||
|
||||
mMirrorViewGeometryVsyncCallback =
|
||||
l -> {
|
||||
@@ -283,10 +298,11 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold
|
||||
R.dimen.magnification_mirror_surface_margin);
|
||||
mBorderDragSize = mResources.getDimensionPixelSize(
|
||||
R.dimen.magnification_border_drag_size);
|
||||
mDragViewSize = mResources.getDimensionPixelSize(
|
||||
R.dimen.magnification_drag_view_size);
|
||||
mOuterBorderSize = mResources.getDimensionPixelSize(
|
||||
R.dimen.magnification_outer_border_margin);
|
||||
mButtonRepositionThresholdFromEdge =
|
||||
mResources.getDimensionPixelSize(
|
||||
R.dimen.magnification_button_reposition_threshold_from_edge);
|
||||
mMinWindowSize = mResources.getDimensionPixelSize(
|
||||
com.android.internal.R.dimen.accessibility_window_magnifier_min_size);
|
||||
}
|
||||
@@ -551,10 +567,13 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold
|
||||
}
|
||||
|
||||
private void applyTapExcludeRegion() {
|
||||
// Sometimes this can get posted and run after deleteWindowMagnification() is called.
|
||||
if (mMirrorView == null) return;
|
||||
|
||||
final Region tapExcludeRegion = calculateTapExclude();
|
||||
final IWindow window = IWindow.Stub.asInterface(mMirrorView.getWindowToken());
|
||||
try {
|
||||
IWindowSession session = WindowManagerGlobal.getWindowSession();
|
||||
IWindowSession session = mGlobalWindowSessionSupplier.get();
|
||||
session.updateTapExcludeRegion(window, tapExcludeRegion);
|
||||
} catch (RemoteException e) {
|
||||
}
|
||||
@@ -564,9 +583,9 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold
|
||||
Region regionInsideDragBorder = new Region(mBorderDragSize, mBorderDragSize,
|
||||
mMirrorView.getWidth() - mBorderDragSize,
|
||||
mMirrorView.getHeight() - mBorderDragSize);
|
||||
Rect dragArea = new Rect(mMirrorView.getWidth() - mDragViewSize - mBorderDragSize,
|
||||
mMirrorView.getHeight() - mDragViewSize - mBorderDragSize,
|
||||
mMirrorView.getWidth(), mMirrorView.getHeight());
|
||||
Rect dragArea = new Rect();
|
||||
mDragView.getHitRect(dragArea);
|
||||
|
||||
regionInsideDragBorder.op(dragArea, Region.Op.DIFFERENCE);
|
||||
return regionInsideDragBorder;
|
||||
}
|
||||
@@ -713,6 +732,12 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold
|
||||
mMirrorView.setTranslationX(translationX);
|
||||
mMirrorView.setTranslationY(translationY);
|
||||
mWm.updateViewLayout(mMirrorView, params);
|
||||
|
||||
// If they are not dragging the handle, we can move the drag handle immediately without
|
||||
// disruption. But if they are dragging it, we avoid moving until the end of the drag.
|
||||
if (!mIsDragging) {
|
||||
mMirrorView.post(this::maybeRepositionButton);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1060,14 +1085,40 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold
|
||||
|
||||
@Override
|
||||
public boolean onStart(float x, float y) {
|
||||
mIsDragging = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onFinish(float x, float y) {
|
||||
maybeRepositionButton();
|
||||
mIsDragging = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Moves the button to the opposite edge if the frame is against the edge of the screen. */
|
||||
private void maybeRepositionButton() {
|
||||
if (mMirrorView == null) return;
|
||||
|
||||
final float screenEdgeX = mWindowBounds.right - mButtonRepositionThresholdFromEdge;
|
||||
final FrameLayout.LayoutParams layoutParams =
|
||||
(FrameLayout.LayoutParams) mDragView.getLayoutParams();
|
||||
|
||||
mMirrorView.getBoundsOnScreen(mTmpRect);
|
||||
|
||||
final int newGravity;
|
||||
if (mTmpRect.right >= screenEdgeX) {
|
||||
newGravity = Gravity.BOTTOM | Gravity.LEFT;
|
||||
} else {
|
||||
newGravity = Gravity.BOTTOM | Gravity.RIGHT;
|
||||
}
|
||||
if (newGravity != layoutParams.gravity) {
|
||||
layoutParams.gravity = newGravity;
|
||||
mDragView.setLayoutParams(layoutParams);
|
||||
mDragView.post(this::applyTapExcludeRegion);
|
||||
}
|
||||
}
|
||||
|
||||
private void animateBounceEffect() {
|
||||
final ObjectAnimator scaleAnimator = ObjectAnimator.ofPropertyValuesHolder(mMirrorView,
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_X, 1, mBounceEffectAnimationScale, 1),
|
||||
|
||||
@@ -38,6 +38,7 @@ import android.testing.AndroidTestingRunner;
|
||||
import android.view.SurfaceControl;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.view.WindowManagerGlobal;
|
||||
import android.view.accessibility.IRemoteMagnificationAnimationCallback;
|
||||
import android.view.animation.AccelerateInterpolator;
|
||||
|
||||
@@ -761,8 +762,16 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase {
|
||||
SfVsyncFrameCallbackProvider sfVsyncFrameProvider,
|
||||
MirrorWindowControl mirrorWindowControl, SurfaceControl.Transaction transaction,
|
||||
WindowMagnifierCallback callback, SysUiState sysUiState) {
|
||||
super(context, handler, animationController, sfVsyncFrameProvider, mirrorWindowControl,
|
||||
transaction, callback, sysUiState);
|
||||
super(
|
||||
context,
|
||||
handler,
|
||||
animationController,
|
||||
sfVsyncFrameProvider,
|
||||
mirrorWindowControl,
|
||||
transaction,
|
||||
callback,
|
||||
sysUiState,
|
||||
WindowManagerGlobal::getWindowSession);
|
||||
mSpyController = Mockito.mock(WindowMagnificationController.class);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.hasItems;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -53,18 +54,23 @@ import android.content.res.Resources;
|
||||
import android.graphics.Insets;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Region;
|
||||
import android.graphics.RegionIterator;
|
||||
import android.os.Handler;
|
||||
import android.os.RemoteException;
|
||||
import android.os.SystemClock;
|
||||
import android.testing.AndroidTestingRunner;
|
||||
import android.testing.TestableLooper;
|
||||
import android.testing.TestableResources;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Display;
|
||||
import android.view.IWindowSession;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceControl;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
import android.view.WindowManagerGlobal;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.view.accessibility.IRemoteMagnificationAnimationCallback;
|
||||
|
||||
@@ -120,9 +126,10 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
|
||||
private WindowMagnificationController mWindowMagnificationController;
|
||||
private Instrumentation mInstrumentation;
|
||||
private final ValueAnimator mValueAnimator = ValueAnimator.ofFloat(0, 1.0f).setDuration(0);
|
||||
private IWindowSession mWindowSessionSpy;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
public void setUp() throws RemoteException {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mContext = Mockito.spy(getContext());
|
||||
mHandler = new FakeHandler(TestableLooper.get(this).getLooper());
|
||||
@@ -130,6 +137,8 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
|
||||
final WindowManager wm = mContext.getSystemService(WindowManager.class);
|
||||
mWindowManager = spy(new TestableWindowManager(wm));
|
||||
|
||||
mWindowSessionSpy = spy(WindowManagerGlobal.getWindowSession());
|
||||
|
||||
mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager);
|
||||
doAnswer(invocation -> {
|
||||
FrameCallback callback = invocation.getArgument(0);
|
||||
@@ -142,9 +151,17 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
|
||||
mResources = getContext().getOrCreateTestableResources().getResources();
|
||||
mWindowMagnificationAnimationController = new WindowMagnificationAnimationController(
|
||||
mContext, mValueAnimator);
|
||||
mWindowMagnificationController = new WindowMagnificationController(mContext,
|
||||
mHandler, mWindowMagnificationAnimationController, mSfVsyncFrameProvider,
|
||||
mMirrorWindowControl, mTransaction, mWindowMagnifierCallback, mSysUiState);
|
||||
mWindowMagnificationController =
|
||||
new WindowMagnificationController(
|
||||
mContext,
|
||||
mHandler,
|
||||
mWindowMagnificationAnimationController,
|
||||
mSfVsyncFrameProvider,
|
||||
mMirrorWindowControl,
|
||||
mTransaction,
|
||||
mWindowMagnifierCallback,
|
||||
mSysUiState,
|
||||
() -> mWindowSessionSpy);
|
||||
|
||||
verify(mMirrorWindowControl).setWindowDelegate(
|
||||
any(MirrorWindowControl.MirrorWindowDelegate.class));
|
||||
@@ -715,6 +732,77 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
|
||||
ReferenceTestUtils.waitForCondition(() -> hasMagnificationOverlapFlag());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void moveWindowMagnificationToRightEdge_dragHandleMovesToLeftAndUpdatesTapExcludeRegion()
|
||||
throws RemoteException {
|
||||
final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
|
||||
setSystemGestureInsets();
|
||||
mInstrumentation.runOnMainSync(
|
||||
() -> {
|
||||
mWindowMagnificationController.enableWindowMagnificationInternal(
|
||||
Float.NaN, Float.NaN, Float.NaN);
|
||||
});
|
||||
|
||||
mInstrumentation.runOnMainSync(
|
||||
() -> {
|
||||
mWindowMagnificationController.moveWindowMagnifier(bounds.width(), 0);
|
||||
});
|
||||
|
||||
// Wait for Region updated.
|
||||
waitForIdleSync();
|
||||
|
||||
final ArgumentCaptor<Region> tapExcludeRegionCapturer =
|
||||
ArgumentCaptor.forClass(Region.class);
|
||||
verify(mWindowSessionSpy, times(2))
|
||||
.updateTapExcludeRegion(any(), tapExcludeRegionCapturer.capture());
|
||||
Region tapExcludeRegion = tapExcludeRegionCapturer.getValue();
|
||||
RegionIterator iterator = new RegionIterator(tapExcludeRegion);
|
||||
|
||||
final Rect topRect = new Rect();
|
||||
final Rect bottomRect = new Rect();
|
||||
assertTrue(iterator.next(topRect));
|
||||
assertTrue(iterator.next(bottomRect));
|
||||
assertFalse(iterator.next(new Rect()));
|
||||
|
||||
assertEquals(topRect.right, bottomRect.right);
|
||||
assertNotEquals(topRect.left, bottomRect.left);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void moveWindowMagnificationToLeftEdge_dragHandleMovesToRightAndUpdatesTapExcludeRegion()
|
||||
throws RemoteException {
|
||||
final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
|
||||
setSystemGestureInsets();
|
||||
mInstrumentation.runOnMainSync(
|
||||
() -> {
|
||||
mWindowMagnificationController.enableWindowMagnificationInternal(
|
||||
Float.NaN, Float.NaN, Float.NaN);
|
||||
});
|
||||
|
||||
mInstrumentation.runOnMainSync(
|
||||
() -> {
|
||||
mWindowMagnificationController.moveWindowMagnifier(-bounds.width(), 0);
|
||||
});
|
||||
|
||||
// Wait for Region updated.
|
||||
waitForIdleSync();
|
||||
|
||||
final ArgumentCaptor<Region> tapExcludeRegionCapturer =
|
||||
ArgumentCaptor.forClass(Region.class);
|
||||
verify(mWindowSessionSpy).updateTapExcludeRegion(any(), tapExcludeRegionCapturer.capture());
|
||||
Region tapExcludeRegion = tapExcludeRegionCapturer.getValue();
|
||||
RegionIterator iterator = new RegionIterator(tapExcludeRegion);
|
||||
|
||||
final Rect topRect = new Rect();
|
||||
final Rect bottomRect = new Rect();
|
||||
assertTrue(iterator.next(topRect));
|
||||
assertTrue(iterator.next(bottomRect));
|
||||
assertFalse(iterator.next(new Rect()));
|
||||
|
||||
assertEquals(topRect.left, bottomRect.left);
|
||||
assertNotEquals(topRect.right, bottomRect.right);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setMinimumWindowSize_enabled_expectedWindowSize() {
|
||||
final int minimumWindowSize = mResources.getDimensionPixelSize(
|
||||
|
||||
Reference in New Issue
Block a user