| /* |
| * Copyright (C) 2019 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.accessibility; |
| |
| import static android.view.WindowInsets.Type.systemGestures; |
| import static android.view.WindowManager.LayoutParams; |
| |
| import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP; |
| |
| import android.animation.ObjectAnimator; |
| import android.animation.PropertyValuesHolder; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.UiContext; |
| import android.content.Context; |
| import android.content.pm.ActivityInfo; |
| import android.content.res.Resources; |
| import android.graphics.Insets; |
| import android.graphics.Matrix; |
| import android.graphics.PixelFormat; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.graphics.Region; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.RemoteException; |
| import android.util.Log; |
| import android.util.Range; |
| import android.view.Choreographer; |
| import android.view.Display; |
| import android.view.Gravity; |
| import android.view.IWindow; |
| import android.view.IWindowSession; |
| import android.view.LayoutInflater; |
| import android.view.MotionEvent; |
| import android.view.Surface; |
| import android.view.SurfaceControl; |
| 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 com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.graphics.SfVsyncFrameCallbackProvider; |
| import com.android.systemui.R; |
| import com.android.systemui.model.SysUiState; |
| import com.android.systemui.shared.system.WindowManagerWrapper; |
| |
| import java.io.PrintWriter; |
| import java.text.NumberFormat; |
| import java.util.Collections; |
| import java.util.Locale; |
| |
| /** |
| * Class to handle adding and removing a window magnification. |
| */ |
| class WindowMagnificationController implements View.OnTouchListener, SurfaceHolder.Callback, |
| MirrorWindowControl.MirrorWindowDelegate, MagnificationGestureDetector.OnGestureListener { |
| |
| private static final String TAG = "WindowMagnificationController"; |
| // Delay to avoid updating state description too frequently. |
| private static final int UPDATE_STATE_DESCRIPTION_DELAY_MS = 100; |
| // It should be consistent with the value defined in WindowMagnificationGestureHandler. |
| 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; |
| private Rect mWindowBounds; |
| private final int mDisplayId; |
| @Surface.Rotation |
| @VisibleForTesting |
| int mRotation; |
| private final Rect mMagnificationFrame = new Rect(); |
| private final SurfaceControl.Transaction mTransaction; |
| |
| private final WindowManager mWm; |
| |
| private float mScale; |
| |
| private final Rect mTmpRect = new Rect(); |
| private final Rect mMirrorViewBounds = new Rect(); |
| private final Rect mSourceBounds = new Rect(); |
| |
| // The root of the mirrored content |
| private SurfaceControl mMirrorSurface; |
| |
| private View mDragView; |
| private View mLeftDrag; |
| private View mTopDrag; |
| private View mRightDrag; |
| private View mBottomDrag; |
| |
| @NonNull |
| private final WindowMagnifierCallback mWindowMagnifierCallback; |
| |
| private final View.OnLayoutChangeListener mMirrorViewLayoutChangeListener; |
| private final View.OnLayoutChangeListener mMirrorSurfaceViewLayoutChangeListener; |
| private final Runnable mMirrorViewRunnable; |
| private final Runnable mUpdateStateDescriptionRunnable; |
| private final Runnable mWindowInsetChangeRunnable; |
| private View mMirrorView; |
| private SurfaceView mMirrorSurfaceView; |
| private int mMirrorSurfaceMargin; |
| private int mBorderDragSize; |
| private int mDragViewSize; |
| private int mOuterBorderSize; |
| // 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. |
| private int mSystemGestureTop = -1; |
| |
| private final SfVsyncFrameCallbackProvider mSfVsyncFrameProvider; |
| private final MagnificationGestureDetector mGestureDetector; |
| private final int mBounceEffectDuration; |
| private Choreographer.FrameCallback mMirrorViewGeometryVsyncCallback; |
| private Locale mLocale; |
| private NumberFormat mPercentFormat; |
| private float mBounceEffectAnimationScale; |
| private SysUiState mSysUiState; |
| // Set it to true when the view is overlapped with the gesture insets at the bottom. |
| private boolean mOverlapWithGestureInsets; |
| |
| @Nullable |
| private MirrorWindowControl mMirrorWindowControl; |
| |
| WindowMagnificationController(@UiContext Context context, @NonNull Handler handler, |
| SfVsyncFrameCallbackProvider sfVsyncFrameProvider, |
| MirrorWindowControl mirrorWindowControl, SurfaceControl.Transaction transaction, |
| @NonNull WindowMagnifierCallback callback, SysUiState sysUiState) { |
| mContext = context; |
| mHandler = handler; |
| mSfVsyncFrameProvider = sfVsyncFrameProvider; |
| mWindowMagnifierCallback = callback; |
| mSysUiState = sysUiState; |
| |
| final Display display = mContext.getDisplay(); |
| mDisplayId = mContext.getDisplayId(); |
| mRotation = display.getRotation(); |
| |
| mWm = context.getSystemService(WindowManager.class); |
| mWindowBounds = mWm.getCurrentWindowMetrics().getBounds(); |
| |
| mResources = mContext.getResources(); |
| mScale = mResources.getInteger(R.integer.magnification_default_scale); |
| mBounceEffectDuration = mResources.getInteger( |
| com.android.internal.R.integer.config_shortAnimTime); |
| updateDimensions(); |
| setInitialStartBounds(); |
| computeBounceAnimationScale(); |
| |
| mMirrorWindowControl = mirrorWindowControl; |
| if (mMirrorWindowControl != null) { |
| mMirrorWindowControl.setWindowDelegate(this); |
| } |
| mTransaction = transaction; |
| mGestureDetector = |
| new MagnificationGestureDetector(mContext, handler, this); |
| |
| // Initialize listeners. |
| mMirrorViewRunnable = () -> { |
| if (mMirrorView != null) { |
| final Rect oldViewBounds = new Rect(mMirrorViewBounds); |
| mMirrorView.getBoundsOnScreen(mMirrorViewBounds); |
| if (oldViewBounds.width() != mMirrorViewBounds.width() |
| || oldViewBounds.height() != mMirrorViewBounds.height()) { |
| mMirrorView.setSystemGestureExclusionRects(Collections.singletonList( |
| new Rect(0, 0, mMirrorViewBounds.width(), mMirrorViewBounds.height()))); |
| } |
| updateSystemUIStateIfNeeded(); |
| mWindowMagnifierCallback.onWindowMagnifierBoundsChanged( |
| mDisplayId, mMirrorViewBounds); |
| } |
| }; |
| mMirrorViewLayoutChangeListener = |
| (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { |
| if (!mHandler.hasCallbacks(mMirrorViewRunnable)) { |
| mHandler.post(mMirrorViewRunnable); |
| } |
| }; |
| |
| mMirrorSurfaceViewLayoutChangeListener = |
| (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) |
| -> applyTapExcludeRegion(); |
| |
| mMirrorViewGeometryVsyncCallback = |
| l -> { |
| if (isWindowVisible() && mMirrorSurface != null) { |
| calculateSourceBounds(mMagnificationFrame, mScale); |
| // The final destination for the magnification surface should be at 0,0 |
| // since the ViewRootImpl's position will change |
| mTmpRect.set(0, 0, mMagnificationFrame.width(), |
| mMagnificationFrame.height()); |
| mTransaction.setGeometry(mMirrorSurface, mSourceBounds, mTmpRect, |
| Surface.ROTATION_0).apply(); |
| mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId, mSourceBounds); |
| } |
| }; |
| mUpdateStateDescriptionRunnable = () -> { |
| if (isWindowVisible()) { |
| mMirrorView.setStateDescription(formatStateDescription(mScale)); |
| } |
| }; |
| mWindowInsetChangeRunnable = this::onWindowInsetChanged; |
| } |
| |
| private void updateDimensions() { |
| mMirrorSurfaceMargin = mResources.getDimensionPixelSize( |
| 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); |
| } |
| |
| private void computeBounceAnimationScale() { |
| final float windowWidth = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin; |
| final float visibleWindowWidth = windowWidth - 2 * mOuterBorderSize; |
| final float animationScaleMax = windowWidth / visibleWindowWidth; |
| mBounceEffectAnimationScale = Math.min(animationScaleMax, ANIMATION_BOUNCE_EFFECT_SCALE); |
| } |
| |
| private boolean updateSystemGestureInsetsTop() { |
| final WindowMetrics windowMetrics = mWm.getCurrentWindowMetrics(); |
| final Insets insets = windowMetrics.getWindowInsets().getInsets(systemGestures()); |
| final int gestureTop = |
| insets.bottom != 0 ? windowMetrics.getBounds().bottom - insets.bottom : -1; |
| if (gestureTop != mSystemGestureTop) { |
| mSystemGestureTop = gestureTop; |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Deletes the magnification window. |
| */ |
| void deleteWindowMagnification() { |
| if (mMirrorSurface != null) { |
| mTransaction.remove(mMirrorSurface).apply(); |
| mMirrorSurface = null; |
| } |
| |
| if (mMirrorSurfaceView != null) { |
| mMirrorSurfaceView.removeOnLayoutChangeListener(mMirrorSurfaceViewLayoutChangeListener); |
| } |
| |
| if (mMirrorView != null) { |
| mHandler.removeCallbacks(mMirrorViewRunnable); |
| mMirrorView.removeOnLayoutChangeListener(mMirrorViewLayoutChangeListener); |
| mWm.removeView(mMirrorView); |
| mMirrorView = null; |
| } |
| |
| if (mMirrorWindowControl != null) { |
| mMirrorWindowControl.destroyControl(); |
| } |
| mMirrorViewBounds.setEmpty(); |
| updateSystemUIStateIfNeeded(); |
| } |
| |
| /** |
| * Called when the configuration has changed, and it updates window magnification UI. |
| * |
| * @param configDiff a bit mask of the differences between the configurations |
| */ |
| void onConfigurationChanged(int configDiff) { |
| if ((configDiff & ActivityInfo.CONFIG_DENSITY) != 0) { |
| updateDimensions(); |
| computeBounceAnimationScale(); |
| if (isWindowVisible()) { |
| deleteWindowMagnification(); |
| enableWindowMagnification(Float.NaN, Float.NaN, Float.NaN); |
| } |
| } else if ((configDiff & ActivityInfo.CONFIG_ORIENTATION) != 0) { |
| onRotate(); |
| } else if ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0) { |
| updateAccessibilityWindowTitleIfNeeded(); |
| } |
| } |
| |
| private void updateSystemUIStateIfNeeded() { |
| updateSysUIState(false); |
| } |
| |
| private void updateAccessibilityWindowTitleIfNeeded() { |
| if (!isWindowVisible()) return; |
| LayoutParams params = (LayoutParams) mMirrorView.getLayoutParams(); |
| params.accessibilityTitle = getAccessibilityWindowTitle(); |
| mWm.updateViewLayout(mMirrorView, params); |
| } |
| |
| /** Handles MirrorWindow position when the device rotation changed. */ |
| private void onRotate() { |
| final Display display = mContext.getDisplay(); |
| final int oldRotation = mRotation; |
| mWindowBounds = mWm.getCurrentWindowMetrics().getBounds(); |
| |
| setMagnificationFrameBoundary(); |
| mRotation = display.getRotation(); |
| |
| if (!isWindowVisible()) { |
| return; |
| } |
| // Keep MirrorWindow position on the screen unchanged when device rotates 90° |
| // clockwise or anti-clockwise. |
| final int rotationDegree = getDegreeFromRotation(mRotation, oldRotation); |
| final Matrix matrix = new Matrix(); |
| matrix.setRotate(rotationDegree); |
| if (rotationDegree == 90) { |
| matrix.postTranslate(mWindowBounds.width(), 0); |
| } else if (rotationDegree == 270) { |
| matrix.postTranslate(0, mWindowBounds.height()); |
| } else { |
| Log.w(TAG, "Invalid rotation change. " + rotationDegree); |
| return; |
| } |
| // The rect of MirrorView is going to be transformed. |
| LayoutParams params = |
| (LayoutParams) mMirrorView.getLayoutParams(); |
| mTmpRect.set(params.x, params.y, params.x + params.width, params.y + params.height); |
| final RectF transformedRect = new RectF(mTmpRect); |
| matrix.mapRect(transformedRect); |
| moveWindowMagnifier(transformedRect.left - mTmpRect.left, |
| transformedRect.top - mTmpRect.top); |
| } |
| |
| /** Returns the rotation degree change of two {@link Surface.Rotation} */ |
| private int getDegreeFromRotation(@Surface.Rotation int newRotation, |
| @Surface.Rotation int oldRotation) { |
| final int rotationDiff = oldRotation - newRotation; |
| final int degree = (rotationDiff + 4) % 4 * 90; |
| return degree; |
| } |
| |
| private void createMirrorWindow() { |
| // The window should be the size the mirrored surface will be but also add room for the |
| // border and the drag handle. |
| int windowWidth = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin; |
| int windowHeight = mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin; |
| |
| LayoutParams params = new LayoutParams( |
| windowWidth, windowHeight, |
| LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, |
| LayoutParams.FLAG_NOT_TOUCH_MODAL |
| | LayoutParams.FLAG_NOT_FOCUSABLE, |
| PixelFormat.TRANSPARENT); |
| params.gravity = Gravity.TOP | Gravity.LEFT; |
| params.x = mMagnificationFrame.left - mMirrorSurfaceMargin; |
| params.y = mMagnificationFrame.top - mMirrorSurfaceMargin; |
| params.layoutInDisplayCutoutMode = LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; |
| params.receiveInsetsIgnoringZOrder = true; |
| params.setTitle(mContext.getString(R.string.magnification_window_title)); |
| params.accessibilityTitle = getAccessibilityWindowTitle(); |
| |
| mMirrorView = LayoutInflater.from(mContext).inflate(R.layout.window_magnifier_view, null); |
| mMirrorSurfaceView = mMirrorView.findViewById(R.id.surface_view); |
| |
| // Allow taps to go through to the mirror SurfaceView below. |
| mMirrorSurfaceView.addOnLayoutChangeListener(mMirrorSurfaceViewLayoutChangeListener); |
| |
| mMirrorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
| | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
| | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
| | View.SYSTEM_UI_FLAG_FULLSCREEN |
| | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
| | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION); |
| mMirrorView.addOnLayoutChangeListener(mMirrorViewLayoutChangeListener); |
| mMirrorView.setAccessibilityDelegate(new MirrorWindowA11yDelegate()); |
| mMirrorView.setOnApplyWindowInsetsListener((v, insets) -> { |
| if (!mHandler.hasCallbacks(mWindowInsetChangeRunnable)) { |
| mHandler.post(mWindowInsetChangeRunnable); |
| } |
| return v.onApplyWindowInsets(insets); |
| }); |
| |
| mWm.addView(mMirrorView, params); |
| |
| SurfaceHolder holder = mMirrorSurfaceView.getHolder(); |
| holder.addCallback(this); |
| holder.setFormat(PixelFormat.RGBA_8888); |
| addDragTouchListeners(); |
| } |
| |
| private void onWindowInsetChanged() { |
| if (updateSystemGestureInsetsTop()) { |
| updateSystemUIStateIfNeeded(); |
| } |
| } |
| |
| private void applyTapExcludeRegion() { |
| final Region tapExcludeRegion = calculateTapExclude(); |
| final IWindow window = IWindow.Stub.asInterface(mMirrorView.getWindowToken()); |
| try { |
| IWindowSession session = WindowManagerGlobal.getWindowSession(); |
| session.updateTapExcludeRegion(window, tapExcludeRegion); |
| } catch (RemoteException e) { |
| } |
| } |
| |
| private Region calculateTapExclude() { |
| 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()); |
| regionInsideDragBorder.op(dragArea, Region.Op.DIFFERENCE); |
| return regionInsideDragBorder; |
| } |
| |
| private String getAccessibilityWindowTitle() { |
| return mResources.getString(com.android.internal.R.string.android_system_label); |
| } |
| |
| private void showControls() { |
| if (mMirrorWindowControl != null) { |
| mMirrorWindowControl.showControl(); |
| } |
| } |
| |
| private void setInitialStartBounds() { |
| // Sets the initial frame area for the mirror and places it in the center of the display. |
| final int initSize = Math.min(mWindowBounds.width(), mWindowBounds.height()) / 2 |
| + 2 * mMirrorSurfaceMargin; |
| final int initX = mWindowBounds.width() / 2 - initSize / 2; |
| final int initY = mWindowBounds.height() / 2 - initSize / 2; |
| mMagnificationFrame.set(initX, initY, initX + initSize, initY + initSize); |
| } |
| |
| /** |
| * This is called once the surfaceView is created so the mirrored content can be placed as a |
| * child of the surfaceView. |
| */ |
| private void createMirror() { |
| mMirrorSurface = WindowManagerWrapper.getInstance().mirrorDisplay(mDisplayId); |
| if (!mMirrorSurface.isValid()) { |
| return; |
| } |
| mTransaction.show(mMirrorSurface) |
| .reparent(mMirrorSurface, mMirrorSurfaceView.getSurfaceControl()); |
| |
| modifyWindowMagnification(mTransaction); |
| } |
| |
| private void addDragTouchListeners() { |
| mDragView = mMirrorView.findViewById(R.id.drag_handle); |
| mLeftDrag = mMirrorView.findViewById(R.id.left_handle); |
| mTopDrag = mMirrorView.findViewById(R.id.top_handle); |
| mRightDrag = mMirrorView.findViewById(R.id.right_handle); |
| mBottomDrag = mMirrorView.findViewById(R.id.bottom_handle); |
| |
| mDragView.setOnTouchListener(this); |
| mLeftDrag.setOnTouchListener(this); |
| mTopDrag.setOnTouchListener(this); |
| mRightDrag.setOnTouchListener(this); |
| mBottomDrag.setOnTouchListener(this); |
| } |
| |
| /** |
| * Modifies the placement of the mirrored content when the position of mMirrorView is updated. |
| */ |
| private void modifyWindowMagnification(SurfaceControl.Transaction t) { |
| mSfVsyncFrameProvider.postFrameCallback(mMirrorViewGeometryVsyncCallback); |
| updateMirrorViewLayout(); |
| |
| } |
| |
| /** |
| * Updates the layout params of MirrorView and translates MirrorView position when the view is |
| * moved close to the screen edges. |
| */ |
| private void updateMirrorViewLayout() { |
| if (!isWindowVisible()) { |
| return; |
| } |
| final int maxMirrorViewX = mWindowBounds.width() - mMirrorView.getWidth(); |
| final int maxMirrorViewY = mWindowBounds.height() - mMirrorView.getHeight(); |
| |
| LayoutParams params = |
| (LayoutParams) mMirrorView.getLayoutParams(); |
| params.x = mMagnificationFrame.left - mMirrorSurfaceMargin; |
| params.y = mMagnificationFrame.top - mMirrorSurfaceMargin; |
| |
| // Translates MirrorView position to make MirrorSurfaceView that is inside MirrorView |
| // able to move close to the screen edges. |
| final float translationX; |
| final float translationY; |
| if (params.x < 0) { |
| translationX = Math.max(params.x, -mOuterBorderSize); |
| } else if (params.x > maxMirrorViewX) { |
| translationX = Math.min(params.x - maxMirrorViewX, mOuterBorderSize); |
| } else { |
| translationX = 0; |
| } |
| if (params.y < 0) { |
| translationY = Math.max(params.y, -mOuterBorderSize); |
| } else if (params.y > maxMirrorViewY) { |
| translationY = Math.min(params.y - maxMirrorViewY, mOuterBorderSize); |
| } else { |
| translationY = 0; |
| } |
| mMirrorView.setTranslationX(translationX); |
| mMirrorView.setTranslationY(translationY); |
| mWm.updateViewLayout(mMirrorView, params); |
| } |
| |
| @Override |
| public boolean onTouch(View v, MotionEvent event) { |
| if (v == mDragView || v == mLeftDrag || v == mTopDrag || v == mRightDrag |
| || v == mBottomDrag) { |
| return mGestureDetector.onTouch(event); |
| } |
| return false; |
| } |
| |
| public void updateSysUIStateFlag() { |
| updateSysUIState(true); |
| } |
| |
| /** |
| * Calculates the desired source bounds. This will be the area under from the center of the |
| * displayFrame, factoring in scale. |
| */ |
| private void calculateSourceBounds(Rect displayFrame, float scale) { |
| int halfWidth = displayFrame.width() / 2; |
| int halfHeight = displayFrame.height() / 2; |
| int left = displayFrame.left + (halfWidth - (int) (halfWidth / scale)); |
| int right = displayFrame.right - (halfWidth - (int) (halfWidth / scale)); |
| int top = displayFrame.top + (halfHeight - (int) (halfHeight / scale)); |
| int bottom = displayFrame.bottom - (halfHeight - (int) (halfHeight / scale)); |
| mSourceBounds.set(left, top, right, bottom); |
| } |
| |
| private void setMagnificationFrameBoundary() { |
| // Calculates width and height for magnification frame could exceed out the screen. |
| // TODO : re-calculating again when scale is changed. |
| // The half width of magnification frame. |
| final int halfWidth = mMagnificationFrame.width() / 2; |
| // The half height of magnification frame. |
| final int halfHeight = mMagnificationFrame.height() / 2; |
| // The scaled half width of magnified region. |
| final int scaledWidth = (int) (halfWidth / mScale); |
| // The scaled half height of magnified region. |
| final int scaledHeight = (int) (halfHeight / mScale); |
| final int exceededWidth = halfWidth - scaledWidth; |
| final int exceededHeight = halfHeight - scaledHeight; |
| |
| mMagnificationFrameBoundary.set(-exceededWidth, -exceededHeight, |
| mWindowBounds.width() + exceededWidth, mWindowBounds.height() + exceededHeight); |
| } |
| |
| /** |
| * Calculates and sets the real position of magnification frame based on the magnified region |
| * should be limited by the region of the display. |
| */ |
| private boolean updateMagnificationFramePosition(int xOffset, int yOffset) { |
| mTmpRect.set(mMagnificationFrame); |
| mTmpRect.offset(xOffset, yOffset); |
| |
| if (mTmpRect.left < mMagnificationFrameBoundary.left) { |
| mTmpRect.offsetTo(mMagnificationFrameBoundary.left, mTmpRect.top); |
| } else if (mTmpRect.right > mMagnificationFrameBoundary.right) { |
| final int leftOffset = mMagnificationFrameBoundary.right - mMagnificationFrame.width(); |
| mTmpRect.offsetTo(leftOffset, mTmpRect.top); |
| } |
| |
| if (mTmpRect.top < mMagnificationFrameBoundary.top) { |
| mTmpRect.offsetTo(mTmpRect.left, mMagnificationFrameBoundary.top); |
| } else if (mTmpRect.bottom > mMagnificationFrameBoundary.bottom) { |
| final int topOffset = mMagnificationFrameBoundary.bottom - mMagnificationFrame.height(); |
| mTmpRect.offsetTo(mTmpRect.left, topOffset); |
| } |
| |
| if (!mTmpRect.equals(mMagnificationFrame)) { |
| mMagnificationFrame.set(mTmpRect); |
| return true; |
| } |
| return false; |
| } |
| |
| private void updateSysUIState(boolean force) { |
| final boolean overlap = isWindowVisible() && mSystemGestureTop > 0 |
| && mMirrorViewBounds.bottom > mSystemGestureTop; |
| if (force || overlap != mOverlapWithGestureInsets) { |
| mOverlapWithGestureInsets = overlap; |
| mSysUiState.setFlag(SYSUI_STATE_MAGNIFICATION_OVERLAP, mOverlapWithGestureInsets) |
| .commitUpdate(mDisplayId); |
| } |
| } |
| |
| @Override |
| public void surfaceCreated(SurfaceHolder holder) { |
| createMirror(); |
| } |
| |
| @Override |
| public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { |
| } |
| |
| @Override |
| public void surfaceDestroyed(SurfaceHolder holder) { |
| } |
| |
| @Override |
| public void move(int xOffset, int yOffset) { |
| moveWindowMagnifier(xOffset, yOffset); |
| } |
| |
| /** |
| * Enables window magnification with specified parameters. |
| * |
| * @param scale the target scale, or {@link Float#NaN} to leave unchanged |
| * @param centerX the screen-relative X coordinate around which to center, |
| * or {@link Float#NaN} to leave unchanged. |
| * @param centerY the screen-relative Y coordinate around which to center, |
| * or {@link Float#NaN} to leave unchanged. |
| */ |
| void enableWindowMagnification(float scale, float centerX, float centerY) { |
| final float offsetX = Float.isNaN(centerX) ? 0 |
| : centerX - mMagnificationFrame.exactCenterX(); |
| final float offsetY = Float.isNaN(centerY) ? 0 |
| : centerY - mMagnificationFrame.exactCenterY(); |
| mScale = Float.isNaN(scale) ? mScale : scale; |
| |
| setMagnificationFrameBoundary(); |
| updateMagnificationFramePosition((int) offsetX, (int) offsetY); |
| if (!isWindowVisible()) { |
| createMirrorWindow(); |
| showControls(); |
| } else { |
| modifyWindowMagnification(mTransaction); |
| } |
| } |
| |
| /** |
| * Sets the scale of the magnified region if it's visible. |
| * |
| * @param scale the target scale, or {@link Float#NaN} to leave unchanged |
| */ |
| void setScale(float scale) { |
| if (!isWindowVisible() || mScale == scale) { |
| return; |
| } |
| enableWindowMagnification(scale, Float.NaN, Float.NaN); |
| mHandler.removeCallbacks(mUpdateStateDescriptionRunnable); |
| mHandler.postDelayed(mUpdateStateDescriptionRunnable, UPDATE_STATE_DESCRIPTION_DELAY_MS); |
| } |
| |
| /** |
| * Moves the window magnifier with specified offset in pixels unit. |
| * |
| * @param offsetX the amount in pixels to offset the window magnifier in the X direction, in |
| * current screen pixels. |
| * @param offsetY the amount in pixels to offset the window magnifier in the Y direction, in |
| * current screen pixels. |
| */ |
| void moveWindowMagnifier(float offsetX, float offsetY) { |
| if (mMirrorSurfaceView == null) { |
| return; |
| } |
| if (updateMagnificationFramePosition((int) offsetX, (int) offsetY)) { |
| modifyWindowMagnification(mTransaction); |
| } |
| } |
| |
| /** |
| * Gets the scale. |
| * |
| * @return {@link Float#NaN} if the window is invisible. |
| */ |
| float getScale() { |
| return isWindowVisible() ? mScale : Float.NaN; |
| } |
| |
| /** |
| * Returns the screen-relative X coordinate of the center of the magnified bounds. |
| * |
| * @return the X coordinate. {@link Float#NaN} if the window is invisible. |
| */ |
| float getCenterX() { |
| return isWindowVisible() ? mMagnificationFrame.exactCenterX() : Float.NaN; |
| } |
| |
| /** |
| * Returns the screen-relative Y coordinate of the center of the magnified bounds. |
| * |
| * @return the Y coordinate. {@link Float#NaN} if the window is invisible. |
| */ |
| float getCenterY() { |
| return isWindowVisible() ? mMagnificationFrame.exactCenterY() : Float.NaN; |
| } |
| |
| //The window is visible when it is existed. |
| private boolean isWindowVisible() { |
| return mMirrorView != null; |
| } |
| |
| private CharSequence formatStateDescription(float scale) { |
| // Cache the locale-appropriate NumberFormat. Configuration locale is guaranteed |
| // non-null, so the first time this is called we will always get the appropriate |
| // NumberFormat, then never regenerate it unless the locale changes on the fly. |
| final Locale curLocale = mContext.getResources().getConfiguration().getLocales().get(0); |
| if (!curLocale.equals(mLocale)) { |
| mLocale = curLocale; |
| mPercentFormat = NumberFormat.getPercentInstance(curLocale); |
| } |
| return mPercentFormat.format(scale); |
| } |
| |
| @Override |
| public boolean onSingleTap() { |
| animateBounceEffect(); |
| return true; |
| } |
| |
| @Override |
| public boolean onDrag(float offsetX, float offsetY) { |
| moveWindowMagnifier(offsetX, offsetY); |
| return true; |
| } |
| |
| @Override |
| public boolean onStart(float x, float y) { |
| return true; |
| } |
| |
| @Override |
| public boolean onFinish(float x, float y) { |
| return false; |
| } |
| |
| private void animateBounceEffect() { |
| final ObjectAnimator scaleAnimator = ObjectAnimator.ofPropertyValuesHolder(mMirrorView, |
| PropertyValuesHolder.ofFloat(View.SCALE_X, 1, mBounceEffectAnimationScale, 1), |
| PropertyValuesHolder.ofFloat(View.SCALE_Y, 1, mBounceEffectAnimationScale, 1)); |
| scaleAnimator.setDuration(mBounceEffectDuration); |
| scaleAnimator.start(); |
| } |
| |
| public void dump(PrintWriter pw) { |
| pw.println("WindowMagnificationController (displayId=" + mDisplayId + "):"); |
| pw.println(" mOverlapWithGestureInsets:" + mOverlapWithGestureInsets); |
| pw.println(" mScale:" + mScale); |
| pw.println(" mMirrorViewBounds:" + (isWindowVisible() ? mMirrorViewBounds : "empty")); |
| pw.println(" mSystemGestureTop:" + mSystemGestureTop); |
| } |
| |
| private class MirrorWindowA11yDelegate extends View.AccessibilityDelegate { |
| |
| @Override |
| public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { |
| super.onInitializeAccessibilityNodeInfo(host, info); |
| info.addAction( |
| new AccessibilityAction(R.id.accessibility_action_zoom_in, |
| mContext.getString(R.string.accessibility_control_zoom_in))); |
| info.addAction(new AccessibilityAction(R.id.accessibility_action_zoom_out, |
| mContext.getString(R.string.accessibility_control_zoom_out))); |
| info.addAction(new AccessibilityAction(R.id.accessibility_action_move_up, |
| mContext.getString(R.string.accessibility_control_move_up))); |
| info.addAction(new AccessibilityAction(R.id.accessibility_action_move_down, |
| mContext.getString(R.string.accessibility_control_move_down))); |
| info.addAction(new AccessibilityAction(R.id.accessibility_action_move_left, |
| mContext.getString(R.string.accessibility_control_move_left))); |
| info.addAction(new AccessibilityAction(R.id.accessibility_action_move_right, |
| mContext.getString(R.string.accessibility_control_move_right))); |
| |
| info.setContentDescription(mContext.getString(R.string.magnification_window_title)); |
| info.setStateDescription(formatStateDescription(getScale())); |
| } |
| |
| @Override |
| public boolean performAccessibilityAction(View host, int action, Bundle args) { |
| if (performA11yAction(action)) { |
| return true; |
| } |
| return super.performAccessibilityAction(host, action, args); |
| } |
| |
| private boolean performA11yAction(int action) { |
| if (action == R.id.accessibility_action_zoom_in) { |
| final float scale = mScale + A11Y_CHANGE_SCALE_DIFFERENCE; |
| mWindowMagnifierCallback.onPerformScaleAction(mDisplayId, |
| A11Y_ACTION_SCALE_RANGE.clamp(scale)); |
| } else if (action == R.id.accessibility_action_zoom_out) { |
| final float scale = mScale - A11Y_CHANGE_SCALE_DIFFERENCE; |
| mWindowMagnifierCallback.onPerformScaleAction(mDisplayId, |
| A11Y_ACTION_SCALE_RANGE.clamp(scale)); |
| } else if (action == R.id.accessibility_action_move_up) { |
| move(0, -mSourceBounds.height()); |
| } else if (action == R.id.accessibility_action_move_down) { |
| move(0, mSourceBounds.height()); |
| } else if (action == R.id.accessibility_action_move_left) { |
| move(-mSourceBounds.width(), 0); |
| } else if (action == R.id.accessibility_action_move_right) { |
| move(mSourceBounds.width(), 0); |
| } else { |
| return false; |
| } |
| mWindowMagnifierCallback.onAccessibilityActionPerformed(mDisplayId); |
| return true; |
| } |
| } |
| } |