diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index d988d664027f6..a3468638ba9a4 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -366,7 +366,7 @@ public final class ViewRootImpl implements ViewParent, // These can be accessed by any thread, must be protected with a lock. // Surface can never be reassigned or cleared (use Surface.clear()). - final Surface mSurface = new Surface(); + public final Surface mSurface = new Surface(); boolean mAdded; boolean mAddedTouchMode; diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 19b8c73132aa1..8003575e28d80 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -119,6 +119,7 @@ import com.android.internal.util.ArrayUtils; import com.android.internal.util.GrowingArrayUtils; import com.android.internal.util.Preconditions; import com.android.internal.widget.EditableInputConnection; +import com.android.internal.widget.Magnifier; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -138,6 +139,9 @@ import java.util.List; public class Editor { private static final String TAG = "Editor"; private static final boolean DEBUG_UNDO = false; + // Specifies whether to use or not the magnifier when pressing the insertion or selection + // handles. + private static final boolean FLAG_USE_MAGNIFIER = false; static final int BLINK = 500; private static final int DRAG_SHADOW_MAX_TEXT_LENGTH = 20; @@ -161,6 +165,17 @@ public class Editor { private static final int MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT = 11; private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 100; + private static final float MAGNIFIER_ZOOM = 1.5f; + @IntDef({MagnifierHandleTrigger.SELECTION_START, + MagnifierHandleTrigger.SELECTION_END, + MagnifierHandleTrigger.INSERTION}) + @Retention(RetentionPolicy.SOURCE) + private @interface MagnifierHandleTrigger { + int INSERTION = 0; + int SELECTION_START = 1; + int SELECTION_END = 2; + } + // Each Editor manages its own undo stack. private final UndoManager mUndoManager = new UndoManager(); private UndoOwner mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this); @@ -179,6 +194,8 @@ public class Editor { private final boolean mHapticTextHandleEnabled; + private final Magnifier mMagnifier; + // Used to highlight a word when it is corrected by the IME private CorrectionHighlighter mCorrectionHighlighter; @@ -325,6 +342,8 @@ public class Editor { mProcessTextIntentActionsHandler = new ProcessTextIntentActionsHandler(this); mHapticTextHandleEnabled = mTextView.getContext().getResources().getBoolean( com.android.internal.R.bool.config_enableHapticTextHandle); + + mMagnifier = FLAG_USE_MAGNIFIER ? new Magnifier(mTextView) : null; } ParcelableParcel saveInstanceState() { @@ -4353,6 +4372,9 @@ public class Editor { protected abstract void updatePosition(float x, float y, boolean fromTouchScreen); + @MagnifierHandleTrigger + protected abstract int getMagnifierHandleTrigger(); + protected boolean isAtRtlRun(@NonNull Layout layout, int offset) { return layout.isRtlCharAt(offset); } @@ -4490,6 +4512,53 @@ public class Editor { return 0; } + protected final void showMagnifier() { + if (mMagnifier == null) { + return; + } + + final int trigger = getMagnifierHandleTrigger(); + final int offset; + switch (trigger) { + case MagnifierHandleTrigger.INSERTION: // Fall through. + case MagnifierHandleTrigger.SELECTION_START: + offset = mTextView.getSelectionStart(); + break; + case MagnifierHandleTrigger.SELECTION_END: + offset = mTextView.getSelectionEnd(); + break; + default: + offset = -1; + break; + } + + if (offset == -1) { + dismissMagnifier(); + } + + final Layout layout = mTextView.getLayout(); + final int lineNumber = layout.getLineForOffset(offset); + // Horizontally snap to character offset. + final float xPosInView = getHorizontal(mTextView.getLayout(), offset); + // Vertically snap to middle of current line. + final float yPosInView = (mTextView.getLayout().getLineTop(lineNumber) + + mTextView.getLayout().getLineBottom(lineNumber)) / 2.0f; + final int[] coordinatesOnScreen = new int[2]; + mTextView.getLocationOnScreen(coordinatesOnScreen); + final float centerXOnScreen = xPosInView + mTextView.getTotalPaddingLeft() + - mTextView.getScrollX() + coordinatesOnScreen[0]; + final float centerYOnScreen = yPosInView + mTextView.getTotalPaddingTop() + - mTextView.getScrollY() + coordinatesOnScreen[1]; + + mMagnifier.show(centerXOnScreen, centerYOnScreen, MAGNIFIER_ZOOM); + } + + protected final void dismissMagnifier() { + if (mMagnifier != null) { + mMagnifier.dismiss(); + } + } + @Override public boolean onTouchEvent(MotionEvent ev) { updateFloatingToolbarVisibility(ev); @@ -4542,10 +4611,7 @@ public class Editor { case MotionEvent.ACTION_UP: filterOnTouchUp(ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)); - mIsDragging = false; - updateDrawable(); - break; - + // Fall through. case MotionEvent.ACTION_CANCEL: mIsDragging = false; updateDrawable(); @@ -4671,6 +4737,11 @@ public class Editor { case MotionEvent.ACTION_DOWN: mDownPositionX = ev.getRawX(); mDownPositionY = ev.getRawY(); + showMagnifier(); + break; + + case MotionEvent.ACTION_MOVE: + showMagnifier(); break; case MotionEvent.ACTION_UP: @@ -4696,11 +4767,10 @@ public class Editor { mTextActionMode.invalidateContentRect(); } } - hideAfterDelay(); - break; - + // Fall through. case MotionEvent.ACTION_CANCEL: hideAfterDelay(); + dismissMagnifier(); break; default: @@ -4751,6 +4821,12 @@ public class Editor { super.onDetached(); removeHiderCallback(); } + + @Override + @MagnifierHandleTrigger + protected int getMagnifierHandleTrigger() { + return MagnifierHandleTrigger.INSERTION; + } } @Retention(RetentionPolicy.SOURCE) @@ -5009,12 +5085,26 @@ public class Editor { @Override public boolean onTouchEvent(MotionEvent event) { boolean superResult = super.onTouchEvent(event); - if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { - // Reset the touch word offset and x value when the user - // re-engages the handle. - mTouchWordDelta = 0.0f; - mPrevX = UNSET_X_VALUE; + + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + // Reset the touch word offset and x value when the user + // re-engages the handle. + mTouchWordDelta = 0.0f; + mPrevX = UNSET_X_VALUE; + showMagnifier(); + break; + + case MotionEvent.ACTION_MOVE: + showMagnifier(); + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + dismissMagnifier(); + break; } + return superResult; } @@ -5110,6 +5200,13 @@ public class Editor { return isRtlChar == isRtlParagraph ? primaryOffset : secondaryOffset; } } + + @MagnifierHandleTrigger + protected int getMagnifierHandleTrigger() { + return isStartHandle() + ? MagnifierHandleTrigger.SELECTION_START + : MagnifierHandleTrigger.SELECTION_END; + } } private int getCurrentLineAdjustedForSlop(Layout layout, int prevLine, float y) { diff --git a/core/java/com/android/internal/widget/Magnifier.java b/core/java/com/android/internal/widget/Magnifier.java new file mode 100644 index 0000000000000..86e7b38a8bc88 --- /dev/null +++ b/core/java/com/android/internal/widget/Magnifier.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2017 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.internal.widget; + +import android.annotation.FloatRange; +import android.annotation.NonNull; +import android.annotation.UiThread; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Point; +import android.graphics.Rect; +import android.os.Handler; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.PixelCopy; +import android.view.View; +import android.view.ViewRootImpl; +import android.widget.ImageView; +import android.widget.PopupWindow; + +import com.android.internal.R; +import com.android.internal.util.Preconditions; + +/** + * Android magnifier widget. Can be used by any view which is attached to window. + */ +public final class Magnifier { + private static final String LOG_TAG = "magnifier"; + // The view for which this magnifier is attached. + private final View mView; + // The window containing the magnifier. + private final PopupWindow mWindow; + // The center coordinates of the window containing the magnifier. + private final Point mWindowCoords = new Point(); + // The width of the window containing the magnifier. + private final int mWindowWidth; + // The height of the window containing the magnifier. + private final int mWindowHeight; + // The bitmap used to display the contents of the magnifier. + private final Bitmap mBitmap; + // The center coordinates of the content that is to be magnified. + private final Point mCenterZoomCoords = new Point(); + // The callback of the pixel copy request will be invoked on this Handler when + // the copy is finished. + private final Handler mPixelCopyHandler = Handler.getMain(); + + /** + * Initializes a magnifier. + * + * @param view the view for which this magnifier is attached + */ + @UiThread + public Magnifier(@NonNull View view) { + mView = Preconditions.checkNotNull(view); + final Context context = mView.getContext(); + final View content = LayoutInflater.from(context).inflate(R.layout.magnifier, null); + mWindowWidth = context.getResources().getDimensionPixelSize(R.dimen.magnifier_width); + mWindowHeight = context.getResources().getDimensionPixelSize(R.dimen.magnifier_height); + final float elevation = context.getResources().getDimension(R.dimen.magnifier_elevation); + + mWindow = new PopupWindow(context); + mWindow.setContentView(content); + mWindow.setWidth(mWindowWidth); + mWindow.setHeight(mWindowHeight); + mWindow.setElevation(elevation); + mWindow.setTouchable(false); + mWindow.setBackgroundDrawable(null); + + mBitmap = Bitmap.createBitmap(mWindowWidth, mWindowHeight, Bitmap.Config.ARGB_8888); + getImageView().setImageBitmap(mBitmap); + } + + /** + * Shows the magnifier on the screen. + * + * @param centerXOnScreen horizontal coordinate of the center point of the magnifier source + * @param centerYOnScreen vertical coordinate of the center point of the magnifier source + * @param scale the scale at which the magnifier zooms on the source content + */ + public void show(@FloatRange(from=0) float centerXOnScreen, + @FloatRange(from=0) float centerYOnScreen, + @FloatRange(from=1, to=10) float scale) { + maybeResizeBitmap(scale); + configureCoordinates(centerXOnScreen, centerYOnScreen); + performPixelCopy(); + + if (mWindow.isShowing()) { + mWindow.update(mWindowCoords.x, mWindowCoords.y, mWindow.getWidth(), + mWindow.getHeight()); + } else { + mWindow.showAtLocation(mView.getRootView(), Gravity.NO_GRAVITY, + mWindowCoords.x, mWindowCoords.y); + } + } + + /** + * Dismisses the magnifier from the screen. + */ + public void dismiss() { + mWindow.dismiss(); + } + + /** + * @return the height of the magnifier window. + */ + public int getHeight() { + return mWindowHeight; + } + + /** + * @return the width of the magnifier window. + */ + public int getWidth() { + return mWindowWidth; + } + + private void maybeResizeBitmap(float scale) { + final int bitmapWidth = (int) (mWindowWidth / scale); + final int bitmapHeight = (int) (mWindowHeight / scale); + if (mBitmap.getWidth() != bitmapWidth || mBitmap.getHeight() != bitmapHeight) { + mBitmap.reconfigure(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); + getImageView().setImageBitmap(mBitmap); + } + } + + private void configureCoordinates(float posXOnScreen, float posYOnScreen) { + mCenterZoomCoords.x = (int) posXOnScreen; + mCenterZoomCoords.y = (int) posYOnScreen; + + final int verticalMagnifierOffset = mView.getContext().getResources().getDimensionPixelSize( + R.dimen.magnifier_offset); + final int availableTopSpace = (mCenterZoomCoords.y - mWindowHeight / 2) + - verticalMagnifierOffset - (mBitmap.getHeight() / 2); + + mWindowCoords.x = mCenterZoomCoords.x - mWindowWidth / 2; + mWindowCoords.y = mCenterZoomCoords.y - mWindowHeight / 2 + + verticalMagnifierOffset * (availableTopSpace > 0 ? -1 : 1); + } + + private void performPixelCopy() { + int startX = mCenterZoomCoords.x - mBitmap.getWidth() / 2; + // Clamp startX value to avoid distorting the rendering of the magnifier content. + if (startX < 0) { + startX = 0; + } else if (startX + mBitmap.getWidth() > mView.getWidth()) { + startX = mView.getWidth() - mBitmap.getWidth(); + } + + final int startY = mCenterZoomCoords.y - mBitmap.getHeight() / 2; + final ViewRootImpl viewRootImpl = mView.getViewRootImpl(); + + if (viewRootImpl != null && viewRootImpl.mSurface != null + && viewRootImpl.mSurface.isValid()) { + PixelCopy.request( + viewRootImpl.mSurface, + new Rect(startX, startY, startX + mBitmap.getWidth(), + startY + mBitmap.getHeight()), + mBitmap, + result -> getImageView().invalidate(), + mPixelCopyHandler); + } else { + Log.d(LOG_TAG, "Could not perform PixelCopy request"); + } + } + + private ImageView getImageView() { + return mWindow.getContentView().findViewById(R.id.magnifier_image); + } +} diff --git a/core/res/res/layout/magnifier.xml b/core/res/res/layout/magnifier.xml new file mode 100644 index 0000000000000..181e5e54bb002 --- /dev/null +++ b/core/res/res/layout/magnifier.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 16c857894d054..b3d00535ecf85 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -520,6 +520,12 @@ 8dp 20dp + + 200dp + 48dp + 2dp + 42dp + 0dp 8dp diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 0732f0d0bcfdf..b32daee9a3b9c 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2474,6 +2474,14 @@ + + + + + + + +