测试平台:
首先, 这个问题的前提是, 使用的输入设备是**鼠标**, 普通的触摸屏并不会出现这个问题. 大致的流程是APP的UI布局中采用ScrollView作为根容器, 之后添加各类子控件, 在一起准备就绪后, 使用鼠标进行功能测试, 出现无法点击控件触发事件响应.
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
style="@style/settingsItems">
<TextView style="@style/TV"
android:text="XXX"
android:layout_weight="1"
/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="BTN"/>
LinearLayout>
<LinearLayout
style="@style/settingsItems">
<TextView style="@style/TV"
android:text="XXX"
android:layout_weight="1"
/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="BTN"/>
LinearLayout>
LinearLayout>
ScrollView>
最先从onInterceptTouchEvent
函数入手, 各个层级的LOG大致分析, 由下往上发现控件BUTTON中根本没有捕获到MotionEvent, 那原因只可能是父控件自己捕获了而不下发. 兜兜转转最终来到了ScrollView.
通过重写onInterceptHoverEvent 判断是否时间已被捕获
@Override
public boolean onInterceptHoverEvent(MotionEvent event) {
boolean b = super.onInterceptHoverEvent(event);
Logger.d(TAG, "onInterceptHoverEvent " + b);
return b;
}
从输出的LOG可以看出来, 当使用鼠标的时候, TRUE 和 FALSE 均有可能出现(在后面排查是才发现这和控件处的位置有关), 当TRUE是, 说明事件由ScrollView处理了, 子控件自然就接收不到事件下发.
顺着onInterceptHoverEvent
往上查:
frameworks/base/core/java/android/view/ViewGroup.java
public boolean onInterceptHoverEvent(MotionEvent event) {
if (event.isFromSource(InputDevice.SOURCE_MOUSE)) {
final int action = event.getAction();
final float x = event.getX();
final float y = event.getY();
if ((action == MotionEvent.ACTION_HOVER_MOVE
|| action == MotionEvent.ACTION_HOVER_ENTER) && isOnScrollbar(x, y)) {
return true;
}
}
return false;
}
从代码中可以看出, 基本的判断条件都是成立的, 鼠标输入 + MOVE/ENTER时间, 最后一个是isOnScrollbar, 顾名思义输入鼠标的位置在ScrollBar 上?
frameworks/base/core/java/android/view/View.java
boolean isOnScrollbar(float x, float y) {
if (mScrollCache == null) {
return false;
}
x += getScrollX();
y += getScrollY();
if (isVerticalScrollBarEnabled() && !isVerticalScrollBarHidden()) {
final Rect touchBounds = mScrollCache.mScrollBarTouchBounds;
getVerticalScrollBarBounds(null, touchBounds);
if (touchBounds.contains((int) x, (int) y)) {
return true;
}
}
if (isHorizontalScrollBarEnabled()) {
final Rect touchBounds = mScrollCache.mScrollBarTouchBounds;
getHorizontalScrollBarBounds(null, touchBounds);
if (touchBounds.contains((int) x, (int) y)) {
return true;
}
}
return false;
}
private void getVerticalScrollBarBounds(@Nullable Rect bounds, @Nullable Rect touchBounds) {
if (mRoundScrollbarRenderer == null) {
getStraightVerticalScrollBarBounds(bounds, touchBounds);
} else {
getRoundVerticalScrollBarBounds(bounds != null ? bounds : touchBounds);
}
}
private void getStraightVerticalScrollBarBounds(@Nullable Rect drawBounds,
@Nullable Rect touchBounds) {
final Rect bounds = drawBounds != null ? drawBounds : touchBounds;
if (bounds == null) {
return;
}
final int inside = (mViewFlags & SCROLLBARS_OUTSIDE_MASK) == 0 ? ~0 : 0;
final int size = getVerticalScrollbarWidth();
int verticalScrollbarPosition = mVerticalScrollbarPosition;
if (verticalScrollbarPosition == SCROLLBAR_POSITION_DEFAULT) {
verticalScrollbarPosition = isLayoutRtl() ?
SCROLLBAR_POSITION_LEFT : SCROLLBAR_POSITION_RIGHT;
}
final int width = mRight - mLeft;
final int height = mBottom - mTop;
switch (verticalScrollbarPosition) {
default:
case SCROLLBAR_POSITION_RIGHT:
bounds.left = mScrollX + width - size - (mUserPaddingRight & inside);
break;
case SCROLLBAR_POSITION_LEFT:
bounds.left = mScrollX + (mUserPaddingLeft & inside);
break;
}
bounds.top = mScrollY + (mPaddingTop & inside);
bounds.right = bounds.left + size;
bounds.bottom = mScrollY + height - (mUserPaddingBottom & inside);
if (touchBounds == null) {
return;
}
if (touchBounds != bounds) {
touchBounds.set(bounds);
}
final int minTouchTarget = mScrollCache.scrollBarMinTouchTarget;
if (touchBounds.width() < minTouchTarget) {
final int adjust = (minTouchTarget - touchBounds.width()) / 2;
if (verticalScrollbarPosition == SCROLLBAR_POSITION_RIGHT) {
touchBounds.right = Math.min(touchBounds.right + adjust, mScrollX + width);
touchBounds.left = touchBounds.right - minTouchTarget;
} else {
touchBounds.left = Math.max(touchBounds.left + adjust, mScrollX);
touchBounds.right = touchBounds.left + minTouchTarget;
}
}
if (touchBounds.height() < minTouchTarget) {
final int adjust = (minTouchTarget - touchBounds.height()) / 2;
touchBounds.top -= adjust;
touchBounds.bottom = touchBounds.top + minTouchTarget;
}
}
/**
* ScrollabilityCache holds various fields used by a View when scrolling
* is supported. This avoids keeping too many unused fields in most
* instances of View.
*/
private static class ScrollabilityCache implements Runnable {
/**
* Scrollbars are not visible
*/
public static final int OFF = 0;
/**
* Scrollbars are visible
*/
public static final int ON = 1;
/**
* Scrollbars are fading away
*/
public static final int FADING = 2;
public boolean fadeScrollBars;
public int fadingEdgeLength;
public int scrollBarDefaultDelayBeforeFade;
public int scrollBarFadeDuration;
public int scrollBarSize;
public int scrollBarMinTouchTarget;
public ScrollBarDrawable scrollBar;
public float[] interpolatorValues;
public View host;
public final Paint paint;
public final Matrix matrix;
public Shader shader;
public final Interpolator scrollBarInterpolator = new Interpolator(1, 2);
private static final float[] OPAQUE = { 255 };
private static final float[] TRANSPARENT = { 0.0f };
/**
* When fading should start. This time moves into the future every time
* a new scroll happens. Measured based on SystemClock.uptimeMillis()
*/
public long fadeStartTime;
/**
* The current state of the scrollbars: ON, OFF, or FADING
*/
public int state = OFF;
private int mLastColor;
public final Rect mScrollBarBounds = new Rect();
public final Rect mScrollBarTouchBounds = new Rect();
public static final int NOT_DRAGGING = 0;
public static final int DRAGGING_VERTICAL_SCROLL_BAR = 1;
public static final int DRAGGING_HORIZONTAL_SCROLL_BAR = 2;
public int mScrollBarDraggingState = NOT_DRAGGING;
public float mScrollBarDraggingPos = 0;
public ScrollabilityCache(ViewConfiguration configuration, View host) {
fadingEdgeLength = configuration.getScaledFadingEdgeLength();
scrollBarSize = configuration.getScaledScrollBarSize();
scrollBarMinTouchTarget = configuration.getScaledMinScrollbarTouchTarget();
scrollBarDefaultDelayBeforeFade = ViewConfiguration.getScrollDefaultDelay();
scrollBarFadeDuration = ViewConfiguration.getScrollBarFadeDuration();
paint = new Paint();
matrix = new Matrix();
// use use a height of 1, and then wack the matrix each time we
// actually use it.
shader = new LinearGradient(0, 0, 0, 1, 0xFF000000, 0, Shader.TileMode.CLAMP);
paint.setShader(shader);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
this.host = host;
}
public void setFadeColor(int color) {
if (color != mLastColor) {
mLastColor = color;
if (color != 0) {
shader = new LinearGradient(0, 0, 0, 1, color | 0xFF000000,
color & 0x00FFFFFF, Shader.TileMode.CLAMP);
paint.setShader(shader);
// Restore the default transfer mode (src_over)
paint.setXfermode(null);
} else {
shader = new LinearGradient(0, 0, 0, 1, 0xFF000000, 0, Shader.TileMode.CLAMP);
paint.setShader(shader);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
}
}
}
public void run() {
long now = AnimationUtils.currentAnimationTimeMillis();
if (now >= fadeStartTime) {
// the animation fades the scrollbars out by changing
// the opacity (alpha) from fully opaque to fully
// transparent
int nextFrame = (int) now;
int framesCount = 0;
Interpolator interpolator = scrollBarInterpolator;
// Start opaque
interpolator.setKeyFrame(framesCount++, nextFrame, OPAQUE);
// End transparent
nextFrame += scrollBarFadeDuration;
interpolator.setKeyFrame(framesCount, nextFrame, TRANSPARENT);
state = FADING;
// Kick off the fade animation
host.invalidate(true);
}
}
}
View中的代码有点多, 简单的来说, 就是isOnScrollbar
这个函数通过获取ScrollBar的位置大小信息判断输入的事件是否处于其捕获的范围.
通过反射调用getVerticalScrollBarBounds
并输出读取的信息: touchRect=[1464,0][1512,674], 基本可以判定是滚动条的位置.
Rect touchRect = new Rect();
getVerticalScrollBarBoundsRe(null, touchRect);
Logger.d(TAG, "touchRect=" + touchRect.toShortString());
void getVerticalScrollBarBoundsRe(Rect r, Rect r2){
try {
@SuppressLint("SoonBlockedPrivateApi")
Method getVerticalScrollBarBounds = View.class.getDeclaredMethod("getVerticalScrollBarBounds", Rect.class, Rect.class);
getVerticalScrollBarBounds.setAccessible(true);
getVerticalScrollBarBounds.invoke(this, r, r2);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
计算宽度1512 - 1464 = 48, 这个宽度默认在系统中又定义, 如果是自定义的ScrollBar则大小不一定是48, 根据configuration.getScaledMinScrollbarTouchTarget();
查到源码的定义如下:
frameworks/base/core/java/android/view/ViewConfiguration.java
private static final int MIN_SCROLLBAR_TOUCH_TARGET = 48;
/**
* @return the minimum size of the scrollbar thumb's touch target in pixels
* @hide
*/
public int getScaledMinScrollbarTouchTarget() {
return mMinScrollbarTouchTarget;
}
问题的根源如下图所示, 红色滚动条的宽度为48:
PS: 上图中的滚动条默认情况下并没有显示出来.
android:scrollbars="none”
onInterceptHoverEvent
函数