解决EditText编辑框选中不显示放大镜问题
前提概要:在做项目的时候, 有两个lunch 版本, 一个版本,在编辑框选中后没有放大镜功能, 另一个版本却是没有放大镜这个功能,但是两个代码都是差不多的,因此很是疑惑.因此找寻源码看如何实现的.
https://developer.android.com/:放大镜微件
Android 9(API 级别 28)及更高版本中提供放大镜微件,这是一个虚拟放大镜,通过代表镜头的叠加窗格来显示所选视图的放大副本。此功能可改善文本插入和选择方面的用户体验:将放大镜应用于文本时,用户可以通过查看窗格内随其手指移动的放大文本来精确定位光标或选择手柄。下面的图 1 展示了放大镜帮助选择文本的方式。放大镜 API 并非与文本关联,并且此微件可用于各种用例,例如读取小号字体或者放大地图上看不太清楚的地名。
放大镜已与 TextView、EditText 或 WebView 等平台微件集成。它可在各种应用上提供一致的文本操纵体验。该微件附带一个简单的 API,可用于根据应用的上下文放大任何 View。
从这段话,可以知道, Android 9 及以上版本的TextView、EditText 或 WebView 等平台微件集成,默认都是实现这个功能. 从一开始就怀疑是不是xml的布局文件有问题,但是仔细对比了两个版本 的代码,没有什么区别,都是一样的.确实让人摸不着头脑.因此去查看源码实现的地方,然后加log复现查看
源码
布局 EditText 是由 frameworks/base/core/java/android/widget/EditText.java 进行实现的,EditText.java 继承 TextView.
public class EditText extends TextView {}
在 TextView 查看,可以看到底下这个方法,可以知道是创建了一个Editor,应该所有的操作都是在这里去实现的.
/**
* An Editor should be created as soon as any of the editable-specific fields (grouped
* inside the Editor object) is assigned to a non-default value.
* This method will create the Editor if needed.
*
* A standard TextView (as well as buttons, checkboxes...) should not qualify and hence will
* have a null Editor, unlike an EditText. Inconsistent in-between states will have an
* Editor for backward compatibility, as soon as one of these fields is assigned.
*
* Also note that for performance reasons, the mEditor is created when needed, but not
* reset when no more edit-specific fields are needed.
*/
private void createEditorIfNeeded() {
if (mEditor == null) {
mEditor = new Editor(this);
}
}
先看看他的构造方法:
Editor(TextView textView) {
// 这个textView 应该就是布局文件对应的编辑框组件
mTextView = textView;
// Synchronize the filter list, which places the undo input filter at the end.
mTextView.setFilters(mTextView.getFilters());
mProcessTextIntentActionsHandler = new ProcessTextIntentActionsHandler(this);
mHapticTextHandleEnabled = mTextView.getContext().getResources().getBoolean(
com.android.internal.R.bool.config_enableHapticTextHandle);
if (FLAG_USE_MAGNIFIER) {
// 注意看这个函数 从名字上看跟放大镜有关系,
mMagnifierAnimator = new MagnifierMotionAnimator(new Magnifier(mTextView));
}
}
注意看new MagnifierMotionAnimator(new Magnifier(mTextView));
这个方法.
首先先new 一个 Magnifier, 参数是当前的编辑框组件,然后把自己当做参数,再去new MagnifierMotionAnimator 这个.
Magnifier 这个单词就是放大镜,所以我们找到了地方
可以看看他的构造方法:
/**
* Initializes a magnifier.
*
* @param view the view for which this magnifier is attached
*/
public Magnifier(@NonNull View view) {
mView = Preconditions.checkNotNull(view);
final Context context = mView.getContext();
mWindowWidth = context.getResources().getDimensionPixelSize(R.dimen.magnifier_width);
mWindowHeight = context.getResources().getDimensionPixelSize(R.dimen.magnifier_height);
mWindowElevation = context.getResources().getDimension(R.dimen.magnifier_elevation);
mWindowCornerRadius = getDeviceDefaultDialogCornerRadius();
mZoom = context.getResources().getFloat(R.dimen.magnifier_zoom_scale);
mBitmapWidth = Math.round(mWindowWidth / mZoom);
mBitmapHeight = Math.round(mWindowHeight / mZoom);
// The view's surface coordinates will not be updated until the magnifier is first shown.
mViewCoordinatesInSurface = new int[2];
}
在来看看Editor.java 的一些方法
private final MagnifierMotionAnimator mMagnifierAnimator;
private final Runnable mUpdateMagnifierRunnable = new Runnable() {
@Override
public void run() {
mMagnifierAnimator.update();
}
};
// Update the magnifier contents whenever anything in the view hierarchy is updated.
// Note: this only captures UI thread-visible changes, so it's a known issue that an animating
// VectorDrawable or Ripple animation will not trigger capture, since they're owned by
// RenderThread.
private final ViewTreeObserver.OnDrawListener mMagnifierOnDrawListener =
new ViewTreeObserver.OnDrawListener() {
@Override
public void onDraw() {
if (mMagnifierAnimator != null) {
// Posting the method will ensure that updating the magnifier contents will
// happen right after the rendering of the current frame.
mTextView.post(mUpdateMagnifierRunnable);
}
}
};
从备注可以看到这个是放大镜进行Draw操作的方法,可以看到核心就是mMagnifierAnimator.update();
这个方法. 进而查找 mMagnifierAnimator 这个定义的地方 可以看到这个就是Editor.java 构造函数进行赋值的.
再回到 现象,放大镜都是有手进行触摸屏幕触发的,应该这个时候有onTouchEvent 事件发生.但是在这个Editor.java文件中,有很多 此方法, 但是从实现的代码可以看到只有InsertionHandleView 这个有放大镜的操作, 底下是他的实现方法:
@Override
public boolean onTouchEvent(MotionEvent ev) {
final boolean result = super.onTouchEvent(ev);
Log.d(TAG,"xxxxxxx ev.getActionMasked(): " +ev.getActionMasked());
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mDownPositionX = ev.getRawX();
mDownPositionY = ev.getRawY();
updateMagnifier(ev);
break;
case MotionEvent.ACTION_MOVE:
updateMagnifier(ev);
break;
case MotionEvent.ACTION_UP:
if (!offsetHasBeenChanged()) {
final float deltaX = mDownPositionX - ev.getRawX();
final float deltaY = mDownPositionY - ev.getRawY();
final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
final ViewConfiguration viewConfiguration = ViewConfiguration.get(
mTextView.getContext());
final int touchSlop = viewConfiguration.getScaledTouchSlop();
if (distanceSquared < touchSlop * touchSlop) {
// Tapping on the handle toggles the insertion action mode.
if (mTextActionMode != null) {
stopTextActionMode();
} else {
startInsertionActionMode();
}
}
} else {
if (mTextActionMode != null) {
mTextActionMode.invalidateContentRect();
}
}
// Fall through.
case MotionEvent.ACTION_CANCEL:
hideAfterDelay();
dismissMagnifier();
break;
default:
break;
}
return result;
}
按压屏幕都是有 ACTION_DOWN ,ACTION_MOVE, ACTION_UP 这些事件. 主要看updateMagnifier(ev)
这个方法, 翻译过来就是更新放大镜.
protected final void updateMagnifier(@NonNull final MotionEvent event) {
if (mMagnifierAnimator == null) {
return;
}
final PointF showPosInView = new PointF();
final boolean shouldShow = !tooLargeTextForMagnifier()
&& obtainMagnifierShowCoordinates(event, showPosInView);
Log.d(TAG,"xxxxxxx updateMagnifier shouldShow: " + shouldShow);
if (shouldShow) {
// Make the cursor visible and stop blinking.
mRenderCursorRegardlessTiming = true;
mTextView.invalidateCursorPath();
suspendBlink();
mMagnifierAnimator.mMagnifier
.setOnOperationCompleteCallback(mHandlesVisibilityCallback);
mMagnifierAnimator.show(showPosInView.x, showPosInView.y);
} else {
dismissMagnifier();
}
}
上面那个打印log,是我加上去的.当烧进系统,查看log时,发现, 显示放大镜的时候shouldShow
为true,不显示放大镜的时候 shouldShow
为 false, 问题主要集中在给他赋值的两个函数
tooLargeTextForMagnifier():
private boolean tooLargeTextForMagnifier() {
final float magnifierContentHeight = Math.round(
mMagnifierAnimator.mMagnifier.getHeight()
/ mMagnifierAnimator.mMagnifier.getZoom());
final Paint.FontMetrics fontMetrics = mTextView.getPaint().getFontMetrics();
final float glyphHeight = fontMetrics.descent - fontMetrics.ascent;
Log.d(TAG,"xxxxxxx tooLargeTextForMagnifier glyphHeight: " + glyphHeight);
Log.d(TAG,"xxxxxxx tooLargeTextForMagnifier magnifierContentHeight: " + magnifierContentHeight);
return glyphHeight > magnifierContentHeight;
}
从我加的log可以看出:glyphHeight
这个是 当前系统的字体进行计算的差值, 但是大于 magnifierContentHeight
(值为58) 这个值,导致这个返回true,不显示放大镜了.
问题就发生在这里了, 可以看出,当字体大于这个判断值的时候,是不显示放大镜,从而进行dismissMagnifier();
操作.
解决方法:
第一个:
在ExitText中设置 android:fontFamily这个,找到符合尺寸的字体, 使其glyphHeight
小于 magnifierContentHeight
第二个
在ExitText中设置android:textSize ,将size设置小一点,因为我加的log,可以看到 glyphHeight
就比 magnifierContentHeight
大一点点,现在我将size设置小一号,瞒住了条件,显示了放大镜, 这个方法和第一个方法,都需要在用的设置的,如果有很多,修改也麻烦.
第三个
统一更改字体库,是因为字体库的不同,一些字体本身就偏高导致了版本不显示放大镜,只要将不显示显示放大镜的字体替换替换成可以显示放大镜的字体,可以解决这个问题
第四个
mWindowWidth = context.getResources().getDimensionPixelSize(R.dimen.magnifier_width);
mWindowHeight = context.getResources().getDimensionPixelSize(R.dimen.magnifier_height);
mWindowElevation = context.getResources().getDimension(R.dimen.magnifier_elevation);
mWindowCornerRadius = getDeviceDefaultDialogCornerRadius();
mZoom = context.getResources().getFloat(R.dimen.magnifier_zoom_scale);
修改magnifierContentHeight
的值,将其变大.修改这个需要小心,因为这个值跟mMagnifier 整个有关,需要自己修改frameworks/base/core/res/res/values/dimens.xml
里的magnifier_height
或magnifier_zoom_scale
这两个属性, 因为我已经把值打印出来,差距很小,因此我这边修改只是稍微修改大一点,只将 magnifier_height
这个值 48dp修改成49dp,最后满足了条件,显示了放大镜.
小结
自己推荐第三个,已经修改代码量最少,需要大佬帮忙指正有没有错误的地方.
转载请附上链接