Merge "[Magnifier - 1] Initial implementation and wiring"
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index d988d66..a346863 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -366,7 +366,7 @@
// 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 19b8c731..8003575 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -119,6 +119,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
mTextActionMode.invalidateContentRect();
}
}
- hideAfterDelay();
- break;
-
+ // Fall through.
case MotionEvent.ACTION_CANCEL:
hideAfterDelay();
+ dismissMagnifier();
break;
default:
@@ -4751,6 +4821,12 @@
super.onDetached();
removeHiderCallback();
}
+
+ @Override
+ @MagnifierHandleTrigger
+ protected int getMagnifierHandleTrigger() {
+ return MagnifierHandleTrigger.INSERTION;
+ }
}
@Retention(RetentionPolicy.SOURCE)
@@ -5009,12 +5085,26 @@
@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 @@
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 0000000..86e7b38
--- /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 0000000..181e5e5
--- /dev/null
+++ b/core/res/res/layout/magnifier.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/floatingToolbarPopupBackgroundDrawable">
+ <ImageView
+ android:id="@+id/magnifier_image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+</LinearLayout>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 16c8578..b3d0053 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -520,6 +520,12 @@
<dimen name="floating_toolbar_vertical_margin">8dp</dimen>
<dimen name="content_rect_bottom_clip_allowance">20dp</dimen>
+ <!-- Magnifier dimensions -->
+ <dimen name="magnifier_width">200dp</dimen>
+ <dimen name="magnifier_height">48dp</dimen>
+ <dimen name="magnifier_elevation">2dp</dimen>
+ <dimen name="magnifier_offset">42dp</dimen>
+
<dimen name="chooser_grid_padding">0dp</dimen>
<!-- Spacing around the background change frome service to non-service -->
<dimen name="chooser_service_spacing">8dp</dimen>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 0732f0d..b32daee9 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2474,6 +2474,14 @@
<java-symbol type="drawable" name="ft_avd_tooverflow_animation" />
<java-symbol type="attr" name="floatingToolbarDividerColor" />
+ <!-- Magnifier -->
+ <java-symbol type="id" name="magnifier_image" />
+ <java-symbol type="layout" name="magnifier" />
+ <java-symbol type="dimen" name="magnifier_width" />
+ <java-symbol type="dimen" name="magnifier_height" />
+ <java-symbol type="dimen" name="magnifier_elevation" />
+ <java-symbol type="dimen" name="magnifier_offset" />
+
<java-symbol type="string" name="date_picker_prev_month_button" />
<java-symbol type="string" name="date_picker_next_month_button" />
<java-symbol type="layout" name="date_picker_month_item_material" />