Loading core/java/android/widget/Editor.java +71 −8 Original line number Diff line number Diff line Loading @@ -220,7 +220,9 @@ public class Editor { private final boolean mHapticTextHandleEnabled; private final MagnifierMotionAnimator mMagnifierAnimator; @Nullable private MagnifierMotionAnimator mMagnifierAnimator; private final Runnable mUpdateMagnifierRunnable = new Runnable() { @Override public void run() { Loading Loading @@ -397,12 +399,6 @@ public class Editor { mHapticTextHandleEnabled = mTextView.getContext().getResources().getBoolean( com.android.internal.R.bool.config_enableHapticTextHandle); if (FLAG_USE_MAGNIFIER) { final Magnifier magnifier = Magnifier.createBuilderWithOldMagnifierDefaults(mTextView).build(); mMagnifierAnimator = new MagnifierMotionAnimator(magnifier); } mCursorControlEnabled = AppGlobals.getIntCoreSetting( WidgetFlags.KEY_ENABLE_CURSOR_CONTROL , 0) != 0; if (TextView.DEBUG_CURSOR) { Loading @@ -421,6 +417,63 @@ public class Editor { return mCursorControlEnabled; } // Lazy creates the magnifier animator. private MagnifierMotionAnimator getMagnifierAnimator() { if (FLAG_USE_MAGNIFIER && mMagnifierAnimator == null) { // Lazy creates the magnifier instance because it requires the text height which cannot // be measured at the time of Editor instance being created. final Magnifier.Builder builder = shouldUseNewMagnifier() ? createBuilderWithInlineMagnifierDefaults() : Magnifier.createBuilderWithOldMagnifierDefaults(mTextView); mMagnifierAnimator = new MagnifierMotionAnimator(builder.build()); } return mMagnifierAnimator; } private boolean shouldUseNewMagnifier() { // TODO: use a separate flag to enable new magnifier. return mCursorControlEnabled; } private Magnifier.Builder createBuilderWithInlineMagnifierDefaults() { final Magnifier.Builder params = new Magnifier.Builder(mTextView); // TODO: supports changing the height/width dynamically because the text height can be // dynamically changed. final Paint.FontMetrics fontMetrics = mTextView.getPaint().getFontMetrics(); final float sourceHeight = fontMetrics.descent - fontMetrics.ascent; final float zoom = 1.5f; final float widthHeightRatio = 5.5f; // Slightly increase the height to avoid tooLargeTextForMagnifier() returns true. int height = (int)(sourceHeight * zoom) + 2; int width = (int)(widthHeightRatio * height); params.setFishEyeStyle() .setSize(width, height) .setSourceSize(width, Math.round(sourceHeight)) .setElevation(0) .setInitialZoom(zoom) .setClippingEnabled(false); final Context context = mTextView.getContext(); final TypedArray a = context.obtainStyledAttributes( null, com.android.internal.R.styleable.Magnifier, com.android.internal.R.attr.magnifierStyle, 0); params.setDefaultSourceToMagnifierOffset( a.getDimensionPixelSize( com.android.internal.R.styleable.Magnifier_magnifierHorizontalOffset, 0), a.getDimensionPixelSize( com.android.internal.R.styleable.Magnifier_magnifierVerticalOffset, 0)); a.recycle(); return params.setSourceBounds( Magnifier.SOURCE_BOUND_MAX_VISIBLE, Magnifier.SOURCE_BOUND_MAX_IN_SURFACE, Magnifier.SOURCE_BOUND_MAX_VISIBLE, Magnifier.SOURCE_BOUND_MAX_IN_SURFACE); } ParcelableParcel saveInstanceState() { ParcelableParcel state = new ParcelableParcel(getClass().getClassLoader()); Parcel parcel = state.getParcel(); Loading Loading @@ -5022,7 +5075,7 @@ public class Editor { } protected final void updateMagnifier(@NonNull final MotionEvent event) { if (mMagnifierAnimator == null) { if (getMagnifierAnimator() == null) { return; } Loading @@ -5036,6 +5089,16 @@ public class Editor { mTextView.invalidateCursorPath(); suspendBlink(); if (shouldUseNewMagnifier()) { // Calculates the line bounds as the content source bounds to the magnifier. Layout layout = mTextView.getLayout(); int line = layout.getLineForOffset(getCurrentCursorOffset()); int lineLeft = (int) layout.getLineLeft(line); lineLeft += mTextView.getTotalPaddingLeft() - mTextView.getScrollX(); int lineRight = (int) layout.getLineRight(line); lineRight -= mTextView.getTotalPaddingRight() + mTextView.getScrollX(); mMagnifierAnimator.mMagnifier.setSourceHorizontalBounds(lineLeft, lineRight); } mMagnifierAnimator.show(showPosInView.x, showPosInView.y); updateHandlesVisibility(); } else { Loading core/java/android/widget/Magnifier.java +170 −36 Original line number Diff line number Diff line Loading @@ -140,6 +140,18 @@ public final class Magnifier { // The lock used to synchronize the UI and render threads when a #dismiss is performed. private final Object mDestroyLock = new Object(); // Members for new styled magnifier (Eloquent style). // Whether the magnifier is in new style. private boolean mIsFishEyeStyle; // The width of the cut region on the left edge of the pixel copy source rect. private int mLeftCutWidth = 0; // The width of the cut region on the right edge of the pixel copy source rect. private int mRightCutWidth = 0; // The horizontal bounds of the content source in pixels, relative to the view. private int mLeftBound = Integer.MIN_VALUE; private int mRightBound = Integer.MAX_VALUE; /** * Initializes a magnifier. * Loading Loading @@ -198,8 +210,14 @@ public final class Magnifier { mWindowWidth = params.mWidth; mWindowHeight = params.mHeight; mZoom = params.mZoom; mIsFishEyeStyle = params.mIsFishEyeStyle; if (params.mSourceWidth > 0 && params.mSourceHeight > 0) { mSourceWidth = params.mSourceWidth; mSourceHeight = params.mSourceHeight; } else { mSourceWidth = Math.round(mWindowWidth / mZoom); mSourceHeight = Math.round(mWindowHeight / mZoom); } mWindowElevation = params.mElevation; mWindowCornerRadius = params.mCornerRadius; mOverlay = params.mOverlay; Loading @@ -220,6 +238,18 @@ public final class Magnifier { sPixelCopyHandlerThread.start(); } /** * Sets the horizontal bounds of the source when showing the magnifier. * This is used for new style magnifier. e.g. limit the source bounds by the text line bounds. * * @param left the left of the bounds, relative to the view. * @param right the right of the bounds, relative to the view. */ void setSourceHorizontalBounds(int left, int right) { mLeftBound = left; mRightBound = right; } /** * Shows the magnifier on the screen. The method takes the coordinates of the center * of the content source going to be magnified and copied to the magnifier. The coordinates Loading Loading @@ -265,20 +295,37 @@ public final class Magnifier { obtainSurfaces(); obtainContentCoordinates(sourceCenterX, sourceCenterY); obtainWindowCoordinates(magnifierCenterX, magnifierCenterY); final int startX = mClampedCenterZoomCoords.x - mSourceWidth / 2; int startX = mClampedCenterZoomCoords.x - mSourceWidth / 2; final int startY = mClampedCenterZoomCoords.y - mSourceHeight / 2; if (mIsFishEyeStyle) { // The magnifier center is the same as source center in new style. magnifierCenterX = mClampedCenterZoomCoords.x - mViewCoordinatesInSurface[0]; magnifierCenterY = mClampedCenterZoomCoords.y - mViewCoordinatesInSurface[1]; // Gets the startX for new style, which should be bounded by the horizontal bounds. // Also calculates the left/right cut width for pixel copy. final int left = startX; final int right = startX + mSourceWidth; final int leftBound = mViewCoordinatesInSurface[0] + Math.max(0, mLeftBound); final int rightBound = mViewCoordinatesInSurface[0] + Math.min(mView.getWidth(), mRightBound); startX = Math.max(left, leftBound); mLeftCutWidth = Math.max(0, leftBound - left); mRightCutWidth = Math.max(0, right - rightBound); } obtainWindowCoordinates(magnifierCenterX, magnifierCenterY); if (sourceCenterX != mPrevShowSourceCoords.x || sourceCenterY != mPrevShowSourceCoords.y || mDirtyState) { if (mWindow == null) { synchronized (mLock) { mWindow = new InternalPopupWindow(mView.getContext(), mView.getDisplay(), mParentSurface.mSurfaceControl, mWindowWidth, mWindowHeight, mParentSurface.mSurfaceControl, mWindowWidth, mWindowHeight, mZoom, mWindowElevation, mWindowCornerRadius, mOverlay != null ? mOverlay : new ColorDrawable(Color.TRANSPARENT), Handler.getMain() /* draw the magnifier on the UI thread */, mLock, mCallback); mCallback, mIsFishEyeStyle); } } performPixelCopy(startX, startY, true /* update window position */); Loading Loading @@ -387,7 +434,7 @@ public final class Magnifier { public void setZoom(@FloatRange(from = 0f) float zoom) { Preconditions.checkArgumentPositive(zoom, "Zoom should be positive"); mZoom = zoom; mSourceWidth = Math.round(mWindowWidth / mZoom); mSourceWidth = mIsFishEyeStyle ? mWindowWidth : Math.round(mWindowWidth / mZoom); mSourceHeight = Math.round(mWindowHeight / mZoom); mDirtyState = true; } Loading Loading @@ -634,7 +681,9 @@ public final class Magnifier { resolvedBottom = Math.max(resolvedBottom, resolvedTop + mSourceHeight); // Finally compute the coordinates of the source center. mClampedCenterZoomCoords.x = Math.max(resolvedLeft + mSourceWidth / 2, Math.min( mClampedCenterZoomCoords.x = mIsFishEyeStyle ? Math.max(resolvedLeft, Math.min(zoomCenterX, resolvedRight)) : Math.max(resolvedLeft + mSourceWidth / 2, Math.min( zoomCenterX, resolvedRight - mSourceWidth / 2)); mClampedCenterZoomCoords.y = Math.max(resolvedTop + mSourceHeight / 2, Math.min( zoomCenterY, resolvedBottom - mSourceHeight / 2)); Loading Loading @@ -678,11 +727,22 @@ public final class Magnifier { // Perform the pixel copy. mPixelCopyRequestRect.set(startXInSurface, startYInSurface, startXInSurface + mSourceWidth, startXInSurface + mSourceWidth - mLeftCutWidth - mRightCutWidth, startYInSurface + mSourceHeight); mPrevStartCoordsInSurface.x = startXInSurface; mPrevStartCoordsInSurface.y = startYInSurface; mDirtyState = false; final InternalPopupWindow currentWindowInstance = mWindow; if (mPixelCopyRequestRect.width() == 0) { // If the copy rect is empty, updates an empty bitmap to the window. mWindow.updateContent( Bitmap.createBitmap(mSourceWidth, mSourceHeight, Bitmap.Config.ALPHA_8)); return; } final Bitmap bitmap = Bitmap.createBitmap(mSourceWidth, mSourceHeight, Bitmap.Config.ARGB_8888); Bitmap.createBitmap(mSourceWidth - mLeftCutWidth - mRightCutWidth, mSourceHeight, Bitmap.Config.ARGB_8888); PixelCopy.request(mContentCopySurface.mSurface, mPixelCopyRequestRect, bitmap, result -> { if (result != PixelCopy.SUCCESS) { Loading @@ -696,15 +756,25 @@ public final class Magnifier { } if (updateWindowPosition) { // TODO: pull the position update outside #performPixelCopy mWindow.setContentPositionForNextDraw(windowCoords.x, windowCoords.y); } mWindow.setContentPositionForNextDraw(windowCoords.x, windowCoords.y); } if (bitmap.getWidth() < mSourceWidth) { // When bitmap width has been cut, re-fills it with full width bitmap. // This only happens in new styled magnifier. final Bitmap newBitmap = Bitmap.createBitmap( mSourceWidth, bitmap.getHeight(), bitmap.getConfig()); final Canvas can = new Canvas(newBitmap); final Rect dstRect = new Rect(mLeftCutWidth, 0, mSourceWidth - mRightCutWidth, bitmap.getHeight()); can.drawBitmap(bitmap, null, dstRect, null); mWindow.updateContent(newBitmap); } else { mWindow.updateContent(bitmap); } } }, sPixelCopyHandlerThread.getThreadHandler()); mPrevStartCoordsInSurface.x = startXInSurface; mPrevStartCoordsInSurface.y = startYInSurface; mDirtyState = false; } private void onPixelCopyFailed() { Loading Loading @@ -790,9 +860,6 @@ public final class Magnifier { // The size of the content of the magnifier. private final int mContentWidth; private final int mContentHeight; // The size of the allocated surface. private final int mSurfaceWidth; private final int mSurfaceHeight; // The insets of the content inside the allocated surface. private final int mOffsetX; private final int mOffsetY; Loading @@ -815,9 +882,6 @@ public final class Magnifier { private final Handler mHandler; // The callback to be run after the next draw. private Callback mCallback; // The position of the magnifier content when the last draw was requested. private int mLastDrawContentPositionX; private int mLastDrawContentPositionY; // Members below describe the state of the magnifier. Reads/writes to them // have to be synchronized between the UI thread and the thread that handles Loading @@ -838,10 +902,19 @@ public final class Magnifier { // The current content of the magnifier. It is mBitmap + mOverlay, only used for testing. private Bitmap mCurrentContent; private final float mZoom; // Whether is in the new magnifier style. private boolean mIsFishEyeStyle; // The mesh matrix for the fish-eye effect. private float[] mMesh; private int mMeshWidth; private int mMeshHeight; InternalPopupWindow(final Context context, final Display display, final SurfaceControl parentSurfaceControl, final int width, final int height, final float elevation, final float cornerRadius, final Drawable overlay, final Handler handler, final Object lock, final Callback callback) { final float zoom, final float elevation, final float cornerRadius, final Drawable overlay, final Handler handler, final Object lock, final Callback callback, final boolean isFishEyeStyle) { mDisplay = display; mOverlay = overlay; mLock = lock; Loading @@ -849,15 +922,16 @@ public final class Magnifier { mContentWidth = width; mContentHeight = height; mZoom = zoom; mOffsetX = (int) (1.05f * elevation); mOffsetY = (int) (1.05f * elevation); // Setup the surface we will use for drawing the content and shadow. mSurfaceWidth = mContentWidth + 2 * mOffsetX; mSurfaceHeight = mContentHeight + 2 * mOffsetY; final int surfaceWidth = mContentWidth + 2 * mOffsetX; final int surfaceHeight = mContentHeight + 2 * mOffsetY; mSurfaceSession = new SurfaceSession(); mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession) .setFormat(PixelFormat.TRANSLUCENT) .setBufferSize(mSurfaceWidth, mSurfaceHeight) .setBufferSize(surfaceWidth, surfaceHeight) .setName("magnifier surface") .setFlags(SurfaceControl.HIDDEN) .setParent(parentSurfaceControl) Loading Loading @@ -904,6 +978,38 @@ public final class Magnifier { mHandler = handler; mMagnifierUpdater = this::doDraw; mFrameDrawScheduled = false; mIsFishEyeStyle = isFishEyeStyle; if (mIsFishEyeStyle) { createMeshMatrixForFishEyeEffect(); } } private void createMeshMatrixForFishEyeEffect() { mMeshWidth = mZoom < 1.5f ? 5 : 4; mMeshHeight = 6; final float w = mContentWidth; final float h = mContentHeight; final float dx = (w - mZoom * w * (mMeshWidth - 2) / mMeshWidth) / 2; mMesh = new float[2 * (mMeshWidth + 1) * (mMeshHeight + 1)]; for (int i = 0; i < 2 * (mMeshWidth + 1) * (mMeshHeight + 1); i += 2) { // Calculates X value. final int colIndex = i % (2 * (mMeshWidth + 1)) / 2; if (colIndex == 0) { mMesh[i] = 0; } else if (colIndex == mMeshWidth) { mMesh[i] = w; } else { mMesh[i] = (colIndex - 1) * (w - 2 * dx) / (mMeshWidth - 2) + dx; } // Calculates Y value. final int rowIndex = i / 2 / (mMeshWidth + 1); final float y0 = colIndex == 0 || colIndex == mMeshWidth ? (h - h / mZoom) / 2 : 0; final float dy = colIndex == 0 || colIndex == mMeshWidth ? h / mZoom / mMeshHeight : h / mMeshHeight; mMesh[i + 1] = y0 + rowIndex * dy; } } private RenderNode createRenderNodeForBitmap(final String name, Loading Loading @@ -1060,15 +1166,19 @@ public final class Magnifier { final RecordingCanvas canvas = mBitmapRenderNode.beginRecording(mContentWidth, mContentHeight); try { if (mIsFishEyeStyle) { canvas.drawBitmapMesh( mBitmap, mMeshWidth, mMeshHeight, mMesh, 0, null, 0, null); } else { final Rect srcRect = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); final Rect dstRect = new Rect(0, 0, mContentWidth, mContentHeight); final Paint paint = new Paint(); paint.setFilterBitmap(true); canvas.drawBitmap(mBitmap, srcRect, dstRect, paint); } } finally { mBitmapRenderNode.endRecording(); } if (mPendingWindowPositionUpdate || mFirstDraw) { // If the window has to be shown or moved, defer this until the next draw. final boolean firstDraw = mFirstDraw; Loading @@ -1094,13 +1204,14 @@ public final class Magnifier { } mTransaction.apply(); }; if (!mIsFishEyeStyle) { // The new style magnifier doesn't need the light/shadow. mRenderer.setLightCenter(mDisplay, pendingX, pendingY); } } else { callback = null; } mLastDrawContentPositionX = mWindowPositionX + mOffsetX; mLastDrawContentPositionY = mWindowPositionY + mOffsetY; mFrameDrawScheduled = false; } Loading Loading @@ -1149,6 +1260,9 @@ public final class Magnifier { private @SourceBound int mTopContentBound; private @SourceBound int mRightContentBound; private @SourceBound int mBottomContentBound; private boolean mIsFishEyeStyle; private int mSourceWidth; private int mSourceHeight; /** * Construct a new builder for {@link Magnifier} objects. Loading Loading @@ -1177,6 +1291,7 @@ public final class Magnifier { mTopContentBound = SOURCE_BOUND_MAX_VISIBLE; mRightContentBound = SOURCE_BOUND_MAX_VISIBLE; mBottomContentBound = SOURCE_BOUND_MAX_VISIBLE; mIsFishEyeStyle = false; } /** Loading Loading @@ -1338,6 +1453,25 @@ public final class Magnifier { return this; } /** * Sets the source width/height. */ @NonNull Builder setSourceSize(int width, int height) { mSourceWidth = width; mSourceHeight = height; return this; } /** * Sets the magnifier as the new fish-eye style. */ @NonNull Builder setFishEyeStyle() { mIsFishEyeStyle = true; return this; } /** * Builds a {@link Magnifier} instance based on the configuration of this {@link Builder}. */ Loading Loading
core/java/android/widget/Editor.java +71 −8 Original line number Diff line number Diff line Loading @@ -220,7 +220,9 @@ public class Editor { private final boolean mHapticTextHandleEnabled; private final MagnifierMotionAnimator mMagnifierAnimator; @Nullable private MagnifierMotionAnimator mMagnifierAnimator; private final Runnable mUpdateMagnifierRunnable = new Runnable() { @Override public void run() { Loading Loading @@ -397,12 +399,6 @@ public class Editor { mHapticTextHandleEnabled = mTextView.getContext().getResources().getBoolean( com.android.internal.R.bool.config_enableHapticTextHandle); if (FLAG_USE_MAGNIFIER) { final Magnifier magnifier = Magnifier.createBuilderWithOldMagnifierDefaults(mTextView).build(); mMagnifierAnimator = new MagnifierMotionAnimator(magnifier); } mCursorControlEnabled = AppGlobals.getIntCoreSetting( WidgetFlags.KEY_ENABLE_CURSOR_CONTROL , 0) != 0; if (TextView.DEBUG_CURSOR) { Loading @@ -421,6 +417,63 @@ public class Editor { return mCursorControlEnabled; } // Lazy creates the magnifier animator. private MagnifierMotionAnimator getMagnifierAnimator() { if (FLAG_USE_MAGNIFIER && mMagnifierAnimator == null) { // Lazy creates the magnifier instance because it requires the text height which cannot // be measured at the time of Editor instance being created. final Magnifier.Builder builder = shouldUseNewMagnifier() ? createBuilderWithInlineMagnifierDefaults() : Magnifier.createBuilderWithOldMagnifierDefaults(mTextView); mMagnifierAnimator = new MagnifierMotionAnimator(builder.build()); } return mMagnifierAnimator; } private boolean shouldUseNewMagnifier() { // TODO: use a separate flag to enable new magnifier. return mCursorControlEnabled; } private Magnifier.Builder createBuilderWithInlineMagnifierDefaults() { final Magnifier.Builder params = new Magnifier.Builder(mTextView); // TODO: supports changing the height/width dynamically because the text height can be // dynamically changed. final Paint.FontMetrics fontMetrics = mTextView.getPaint().getFontMetrics(); final float sourceHeight = fontMetrics.descent - fontMetrics.ascent; final float zoom = 1.5f; final float widthHeightRatio = 5.5f; // Slightly increase the height to avoid tooLargeTextForMagnifier() returns true. int height = (int)(sourceHeight * zoom) + 2; int width = (int)(widthHeightRatio * height); params.setFishEyeStyle() .setSize(width, height) .setSourceSize(width, Math.round(sourceHeight)) .setElevation(0) .setInitialZoom(zoom) .setClippingEnabled(false); final Context context = mTextView.getContext(); final TypedArray a = context.obtainStyledAttributes( null, com.android.internal.R.styleable.Magnifier, com.android.internal.R.attr.magnifierStyle, 0); params.setDefaultSourceToMagnifierOffset( a.getDimensionPixelSize( com.android.internal.R.styleable.Magnifier_magnifierHorizontalOffset, 0), a.getDimensionPixelSize( com.android.internal.R.styleable.Magnifier_magnifierVerticalOffset, 0)); a.recycle(); return params.setSourceBounds( Magnifier.SOURCE_BOUND_MAX_VISIBLE, Magnifier.SOURCE_BOUND_MAX_IN_SURFACE, Magnifier.SOURCE_BOUND_MAX_VISIBLE, Magnifier.SOURCE_BOUND_MAX_IN_SURFACE); } ParcelableParcel saveInstanceState() { ParcelableParcel state = new ParcelableParcel(getClass().getClassLoader()); Parcel parcel = state.getParcel(); Loading Loading @@ -5022,7 +5075,7 @@ public class Editor { } protected final void updateMagnifier(@NonNull final MotionEvent event) { if (mMagnifierAnimator == null) { if (getMagnifierAnimator() == null) { return; } Loading @@ -5036,6 +5089,16 @@ public class Editor { mTextView.invalidateCursorPath(); suspendBlink(); if (shouldUseNewMagnifier()) { // Calculates the line bounds as the content source bounds to the magnifier. Layout layout = mTextView.getLayout(); int line = layout.getLineForOffset(getCurrentCursorOffset()); int lineLeft = (int) layout.getLineLeft(line); lineLeft += mTextView.getTotalPaddingLeft() - mTextView.getScrollX(); int lineRight = (int) layout.getLineRight(line); lineRight -= mTextView.getTotalPaddingRight() + mTextView.getScrollX(); mMagnifierAnimator.mMagnifier.setSourceHorizontalBounds(lineLeft, lineRight); } mMagnifierAnimator.show(showPosInView.x, showPosInView.y); updateHandlesVisibility(); } else { Loading
core/java/android/widget/Magnifier.java +170 −36 Original line number Diff line number Diff line Loading @@ -140,6 +140,18 @@ public final class Magnifier { // The lock used to synchronize the UI and render threads when a #dismiss is performed. private final Object mDestroyLock = new Object(); // Members for new styled magnifier (Eloquent style). // Whether the magnifier is in new style. private boolean mIsFishEyeStyle; // The width of the cut region on the left edge of the pixel copy source rect. private int mLeftCutWidth = 0; // The width of the cut region on the right edge of the pixel copy source rect. private int mRightCutWidth = 0; // The horizontal bounds of the content source in pixels, relative to the view. private int mLeftBound = Integer.MIN_VALUE; private int mRightBound = Integer.MAX_VALUE; /** * Initializes a magnifier. * Loading Loading @@ -198,8 +210,14 @@ public final class Magnifier { mWindowWidth = params.mWidth; mWindowHeight = params.mHeight; mZoom = params.mZoom; mIsFishEyeStyle = params.mIsFishEyeStyle; if (params.mSourceWidth > 0 && params.mSourceHeight > 0) { mSourceWidth = params.mSourceWidth; mSourceHeight = params.mSourceHeight; } else { mSourceWidth = Math.round(mWindowWidth / mZoom); mSourceHeight = Math.round(mWindowHeight / mZoom); } mWindowElevation = params.mElevation; mWindowCornerRadius = params.mCornerRadius; mOverlay = params.mOverlay; Loading @@ -220,6 +238,18 @@ public final class Magnifier { sPixelCopyHandlerThread.start(); } /** * Sets the horizontal bounds of the source when showing the magnifier. * This is used for new style magnifier. e.g. limit the source bounds by the text line bounds. * * @param left the left of the bounds, relative to the view. * @param right the right of the bounds, relative to the view. */ void setSourceHorizontalBounds(int left, int right) { mLeftBound = left; mRightBound = right; } /** * Shows the magnifier on the screen. The method takes the coordinates of the center * of the content source going to be magnified and copied to the magnifier. The coordinates Loading Loading @@ -265,20 +295,37 @@ public final class Magnifier { obtainSurfaces(); obtainContentCoordinates(sourceCenterX, sourceCenterY); obtainWindowCoordinates(magnifierCenterX, magnifierCenterY); final int startX = mClampedCenterZoomCoords.x - mSourceWidth / 2; int startX = mClampedCenterZoomCoords.x - mSourceWidth / 2; final int startY = mClampedCenterZoomCoords.y - mSourceHeight / 2; if (mIsFishEyeStyle) { // The magnifier center is the same as source center in new style. magnifierCenterX = mClampedCenterZoomCoords.x - mViewCoordinatesInSurface[0]; magnifierCenterY = mClampedCenterZoomCoords.y - mViewCoordinatesInSurface[1]; // Gets the startX for new style, which should be bounded by the horizontal bounds. // Also calculates the left/right cut width for pixel copy. final int left = startX; final int right = startX + mSourceWidth; final int leftBound = mViewCoordinatesInSurface[0] + Math.max(0, mLeftBound); final int rightBound = mViewCoordinatesInSurface[0] + Math.min(mView.getWidth(), mRightBound); startX = Math.max(left, leftBound); mLeftCutWidth = Math.max(0, leftBound - left); mRightCutWidth = Math.max(0, right - rightBound); } obtainWindowCoordinates(magnifierCenterX, magnifierCenterY); if (sourceCenterX != mPrevShowSourceCoords.x || sourceCenterY != mPrevShowSourceCoords.y || mDirtyState) { if (mWindow == null) { synchronized (mLock) { mWindow = new InternalPopupWindow(mView.getContext(), mView.getDisplay(), mParentSurface.mSurfaceControl, mWindowWidth, mWindowHeight, mParentSurface.mSurfaceControl, mWindowWidth, mWindowHeight, mZoom, mWindowElevation, mWindowCornerRadius, mOverlay != null ? mOverlay : new ColorDrawable(Color.TRANSPARENT), Handler.getMain() /* draw the magnifier on the UI thread */, mLock, mCallback); mCallback, mIsFishEyeStyle); } } performPixelCopy(startX, startY, true /* update window position */); Loading Loading @@ -387,7 +434,7 @@ public final class Magnifier { public void setZoom(@FloatRange(from = 0f) float zoom) { Preconditions.checkArgumentPositive(zoom, "Zoom should be positive"); mZoom = zoom; mSourceWidth = Math.round(mWindowWidth / mZoom); mSourceWidth = mIsFishEyeStyle ? mWindowWidth : Math.round(mWindowWidth / mZoom); mSourceHeight = Math.round(mWindowHeight / mZoom); mDirtyState = true; } Loading Loading @@ -634,7 +681,9 @@ public final class Magnifier { resolvedBottom = Math.max(resolvedBottom, resolvedTop + mSourceHeight); // Finally compute the coordinates of the source center. mClampedCenterZoomCoords.x = Math.max(resolvedLeft + mSourceWidth / 2, Math.min( mClampedCenterZoomCoords.x = mIsFishEyeStyle ? Math.max(resolvedLeft, Math.min(zoomCenterX, resolvedRight)) : Math.max(resolvedLeft + mSourceWidth / 2, Math.min( zoomCenterX, resolvedRight - mSourceWidth / 2)); mClampedCenterZoomCoords.y = Math.max(resolvedTop + mSourceHeight / 2, Math.min( zoomCenterY, resolvedBottom - mSourceHeight / 2)); Loading Loading @@ -678,11 +727,22 @@ public final class Magnifier { // Perform the pixel copy. mPixelCopyRequestRect.set(startXInSurface, startYInSurface, startXInSurface + mSourceWidth, startXInSurface + mSourceWidth - mLeftCutWidth - mRightCutWidth, startYInSurface + mSourceHeight); mPrevStartCoordsInSurface.x = startXInSurface; mPrevStartCoordsInSurface.y = startYInSurface; mDirtyState = false; final InternalPopupWindow currentWindowInstance = mWindow; if (mPixelCopyRequestRect.width() == 0) { // If the copy rect is empty, updates an empty bitmap to the window. mWindow.updateContent( Bitmap.createBitmap(mSourceWidth, mSourceHeight, Bitmap.Config.ALPHA_8)); return; } final Bitmap bitmap = Bitmap.createBitmap(mSourceWidth, mSourceHeight, Bitmap.Config.ARGB_8888); Bitmap.createBitmap(mSourceWidth - mLeftCutWidth - mRightCutWidth, mSourceHeight, Bitmap.Config.ARGB_8888); PixelCopy.request(mContentCopySurface.mSurface, mPixelCopyRequestRect, bitmap, result -> { if (result != PixelCopy.SUCCESS) { Loading @@ -696,15 +756,25 @@ public final class Magnifier { } if (updateWindowPosition) { // TODO: pull the position update outside #performPixelCopy mWindow.setContentPositionForNextDraw(windowCoords.x, windowCoords.y); } mWindow.setContentPositionForNextDraw(windowCoords.x, windowCoords.y); } if (bitmap.getWidth() < mSourceWidth) { // When bitmap width has been cut, re-fills it with full width bitmap. // This only happens in new styled magnifier. final Bitmap newBitmap = Bitmap.createBitmap( mSourceWidth, bitmap.getHeight(), bitmap.getConfig()); final Canvas can = new Canvas(newBitmap); final Rect dstRect = new Rect(mLeftCutWidth, 0, mSourceWidth - mRightCutWidth, bitmap.getHeight()); can.drawBitmap(bitmap, null, dstRect, null); mWindow.updateContent(newBitmap); } else { mWindow.updateContent(bitmap); } } }, sPixelCopyHandlerThread.getThreadHandler()); mPrevStartCoordsInSurface.x = startXInSurface; mPrevStartCoordsInSurface.y = startYInSurface; mDirtyState = false; } private void onPixelCopyFailed() { Loading Loading @@ -790,9 +860,6 @@ public final class Magnifier { // The size of the content of the magnifier. private final int mContentWidth; private final int mContentHeight; // The size of the allocated surface. private final int mSurfaceWidth; private final int mSurfaceHeight; // The insets of the content inside the allocated surface. private final int mOffsetX; private final int mOffsetY; Loading @@ -815,9 +882,6 @@ public final class Magnifier { private final Handler mHandler; // The callback to be run after the next draw. private Callback mCallback; // The position of the magnifier content when the last draw was requested. private int mLastDrawContentPositionX; private int mLastDrawContentPositionY; // Members below describe the state of the magnifier. Reads/writes to them // have to be synchronized between the UI thread and the thread that handles Loading @@ -838,10 +902,19 @@ public final class Magnifier { // The current content of the magnifier. It is mBitmap + mOverlay, only used for testing. private Bitmap mCurrentContent; private final float mZoom; // Whether is in the new magnifier style. private boolean mIsFishEyeStyle; // The mesh matrix for the fish-eye effect. private float[] mMesh; private int mMeshWidth; private int mMeshHeight; InternalPopupWindow(final Context context, final Display display, final SurfaceControl parentSurfaceControl, final int width, final int height, final float elevation, final float cornerRadius, final Drawable overlay, final Handler handler, final Object lock, final Callback callback) { final float zoom, final float elevation, final float cornerRadius, final Drawable overlay, final Handler handler, final Object lock, final Callback callback, final boolean isFishEyeStyle) { mDisplay = display; mOverlay = overlay; mLock = lock; Loading @@ -849,15 +922,16 @@ public final class Magnifier { mContentWidth = width; mContentHeight = height; mZoom = zoom; mOffsetX = (int) (1.05f * elevation); mOffsetY = (int) (1.05f * elevation); // Setup the surface we will use for drawing the content and shadow. mSurfaceWidth = mContentWidth + 2 * mOffsetX; mSurfaceHeight = mContentHeight + 2 * mOffsetY; final int surfaceWidth = mContentWidth + 2 * mOffsetX; final int surfaceHeight = mContentHeight + 2 * mOffsetY; mSurfaceSession = new SurfaceSession(); mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession) .setFormat(PixelFormat.TRANSLUCENT) .setBufferSize(mSurfaceWidth, mSurfaceHeight) .setBufferSize(surfaceWidth, surfaceHeight) .setName("magnifier surface") .setFlags(SurfaceControl.HIDDEN) .setParent(parentSurfaceControl) Loading Loading @@ -904,6 +978,38 @@ public final class Magnifier { mHandler = handler; mMagnifierUpdater = this::doDraw; mFrameDrawScheduled = false; mIsFishEyeStyle = isFishEyeStyle; if (mIsFishEyeStyle) { createMeshMatrixForFishEyeEffect(); } } private void createMeshMatrixForFishEyeEffect() { mMeshWidth = mZoom < 1.5f ? 5 : 4; mMeshHeight = 6; final float w = mContentWidth; final float h = mContentHeight; final float dx = (w - mZoom * w * (mMeshWidth - 2) / mMeshWidth) / 2; mMesh = new float[2 * (mMeshWidth + 1) * (mMeshHeight + 1)]; for (int i = 0; i < 2 * (mMeshWidth + 1) * (mMeshHeight + 1); i += 2) { // Calculates X value. final int colIndex = i % (2 * (mMeshWidth + 1)) / 2; if (colIndex == 0) { mMesh[i] = 0; } else if (colIndex == mMeshWidth) { mMesh[i] = w; } else { mMesh[i] = (colIndex - 1) * (w - 2 * dx) / (mMeshWidth - 2) + dx; } // Calculates Y value. final int rowIndex = i / 2 / (mMeshWidth + 1); final float y0 = colIndex == 0 || colIndex == mMeshWidth ? (h - h / mZoom) / 2 : 0; final float dy = colIndex == 0 || colIndex == mMeshWidth ? h / mZoom / mMeshHeight : h / mMeshHeight; mMesh[i + 1] = y0 + rowIndex * dy; } } private RenderNode createRenderNodeForBitmap(final String name, Loading Loading @@ -1060,15 +1166,19 @@ public final class Magnifier { final RecordingCanvas canvas = mBitmapRenderNode.beginRecording(mContentWidth, mContentHeight); try { if (mIsFishEyeStyle) { canvas.drawBitmapMesh( mBitmap, mMeshWidth, mMeshHeight, mMesh, 0, null, 0, null); } else { final Rect srcRect = new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); final Rect dstRect = new Rect(0, 0, mContentWidth, mContentHeight); final Paint paint = new Paint(); paint.setFilterBitmap(true); canvas.drawBitmap(mBitmap, srcRect, dstRect, paint); } } finally { mBitmapRenderNode.endRecording(); } if (mPendingWindowPositionUpdate || mFirstDraw) { // If the window has to be shown or moved, defer this until the next draw. final boolean firstDraw = mFirstDraw; Loading @@ -1094,13 +1204,14 @@ public final class Magnifier { } mTransaction.apply(); }; if (!mIsFishEyeStyle) { // The new style magnifier doesn't need the light/shadow. mRenderer.setLightCenter(mDisplay, pendingX, pendingY); } } else { callback = null; } mLastDrawContentPositionX = mWindowPositionX + mOffsetX; mLastDrawContentPositionY = mWindowPositionY + mOffsetY; mFrameDrawScheduled = false; } Loading Loading @@ -1149,6 +1260,9 @@ public final class Magnifier { private @SourceBound int mTopContentBound; private @SourceBound int mRightContentBound; private @SourceBound int mBottomContentBound; private boolean mIsFishEyeStyle; private int mSourceWidth; private int mSourceHeight; /** * Construct a new builder for {@link Magnifier} objects. Loading Loading @@ -1177,6 +1291,7 @@ public final class Magnifier { mTopContentBound = SOURCE_BOUND_MAX_VISIBLE; mRightContentBound = SOURCE_BOUND_MAX_VISIBLE; mBottomContentBound = SOURCE_BOUND_MAX_VISIBLE; mIsFishEyeStyle = false; } /** Loading Loading @@ -1338,6 +1453,25 @@ public final class Magnifier { return this; } /** * Sets the source width/height. */ @NonNull Builder setSourceSize(int width, int height) { mSourceWidth = width; mSourceHeight = height; return this; } /** * Sets the magnifier as the new fish-eye style. */ @NonNull Builder setFishEyeStyle() { mIsFishEyeStyle = true; return this; } /** * Builds a {@link Magnifier} instance based on the configuration of this {@link Builder}. */ Loading