[Magnifier - 1] Initial implementation and wiring
* implementation of a magnifier which can be attached to any view * important APIs: ** show(float centerXOnScreen, float centerYOnScreen, float scale) ** dismiss() * smart offset => shows below if there is no space above * controlled by boolean flag (easy to turn off) * attached the magnifier to Editor's handles * vertically snaps to the middle of the line containing the selection * horizontally snaps to the offset of the character where the selection starts/ends Bug: 66657373 Test: bit FrameworksCoreTests:android.widget.TextViewActivityTest Test: bit CtsWidgetTestCases:android.widget.cts.TextViewTest Test: manual test that shows the magnifier working Change-Id: I1d4616b8bb1210d869ac47dca137ea9636355250
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
184
core/java/com/android/internal/widget/Magnifier.java
Normal file
184
core/java/com/android/internal/widget/Magnifier.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
27
core/res/res/layout/magnifier.xml
Normal file
27
core/res/res/layout/magnifier.xml
Normal file
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user