Merge "feat(magnification): move mag button when window reaches the screen edge"

This commit is contained in:
TreeHugger Robot
2022-05-27 00:53:19 +00:00
committed by Android (Google) Code Review
6 changed files with 180 additions and 24 deletions

View File

@@ -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"/>

View File

@@ -1080,6 +1080,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>

View File

@@ -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);
}
}

View File

@@ -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),

View File

@@ -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);
}

View File

@@ -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(