最近在做公司项目的Android适配工作,将support依赖都升级到了28.0.0,很多问题扑面而来,最让我苦恼的就是RecyclerView嵌套RecyclerView时,item中的EditText获取焦点时,横向滑动的RecyclerView会自动滚动到最前面,我依稀记得在原来遇到过,同样是升级了RecyclerView的依赖版本后出现,上一次的解决方式是把版本又降回去,但是这样治标不治本,趁着这次把病根给它解决掉。
首先用 Gif 图展示下升级RecyclerView版本之后产生的问题(RecyclerView版本为28.0.0):
正常的情况应该如下(此RecyclerView版本为25.2.0):
因为RecyclerView版本不同产生的问题,当然先从RecyclerView开始查起,项目中使用这种嵌套时做了封装,并且为了更好的处理滑动冲突,进行了继承,我就从这个类中找到了重写的一个方法:
@Override
public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
boolean result = false;
if (child instanceof LinearLayout) {
for (int i = 0; i < ((LinearLayout) child).getChildCount(); i++) {
View childView = ((LinearLayout) child).getChildAt(i);
if (childView instanceof EditText) {
result = true;
break;
}
}
}
rectangle.top = 0;
rectangle.bottom = 0;
rectangle.left = 0;
rectangle.right = 0;
return result;
}
我将这个方法进行度娘,找到了一个文章中对官方文档的翻译:
通过翻译,我基本确定了问题就是由这个方法引起的,于是我将这个方法注释掉,再运行起来后滑动到最左边的问题是解决了,但是并没有像正常情况那样将EditText顶起,而是让EditText定位在屏幕的最右边。
没关系,至少咱成功了一半,EditText还能看得见。
我通过这个方法找到super的实现,也就是RecyclerView的源码实现:
public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) {
return this.mLayout.requestChildRectangleOnScreen(this, child, rect, immediate);
}
RecyclerView本身是调用了mLayout的 requestChildRectangleOnScreen
,找到这个mLayout的定义为 RecyclerView.LayoutManager mLayout;
,紧接着看这个mLayout赋值的地方:(以下省略部分源码)
public void setLayoutManager(@Nullable RecyclerView.LayoutManager layout) {
if (layout != this.mLayout) {
//省略部分代码
this.mChildHelper.removeAllViewsUnfiltered();
//这里进行赋值操作
this.mLayout = layout;
if (layout != null) {
if (layout.mRecyclerView != null) {
throw new IllegalArgumentException("LayoutManager " + layout + " is already attached to a RecyclerView:" + layout.mRecyclerView.exceptionLabel());
}
this.mLayout.setRecyclerView(this);
if (this.mIsAttached) {
this.mLayout.dispatchAttachedToWindow(this);
}
}
this.mRecycler.updateViewCacheSize();
this.requestLayout();
}
}
RecyclerView的源码跟到这可以知道问题的解决应该要看 LayoutManage.requestChildRectangleOnScreen
了,但是这里我就有一个疑问,我自己的类重写了RecyclerView的 requestChildRectangleOnScreen
方法,而且重写这个方法并没有调用super,那么就不应该会产生这个问题?难道RecyclerView中还有别的地方调用了LayoutManage.requestChildRectangleOnScreen
方法么?搜索一下,发现确实如此,在RecyclerView的源码中,有一个私有方法调用了(注意,这里调用的是5个参数的 requestChildRectangleOnScreen
方法):
private void requestChildOnScreen(@NonNull View child, @Nullable View focused) {
//省略部分源码
this.mLayout.requestChildRectangleOnScreen(this, child, this.mTempRect, !this.mFirstLayoutComplete, focused == null);
}
而这个方法又由其中的另一个方法调用:
public void requestChildFocus(View child, View focused) {
if (!this.mLayout.onRequestChildFocus(this, this.mState, child, focused) && focused != null) {
this.requestChildOnScreen(child, focused);
}
super.requestChildFocus(child, focused);
}
从方法名可以知道,这个方法的调用时机是在请求Child获取焦点的时候,这也验证了我们问题的产生是由我们点击EditText时引起的,我感觉我离真相很近了,后面的源码我们就看LayoutManage。
直接看LayoutManage中的 requestChildRectangleOnScreen
这个方法:
public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent, @NonNull View child, @NonNull Rect rect, boolean immediate) {
return this.requestChildRectangleOnScreen(parent, child, rect, immediate, false);
}
public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent, @NonNull View child, @NonNull Rect rect, boolean immediate, boolean focusedChildVisible) {
int[] scrollAmount = this.getChildRectangleOnScreenScrollAmount(parent, child, rect, immediate);
int dx = scrollAmount[0];
int dy = scrollAmount[1];
if ((!focusedChildVisible || this.isFocusedChildVisibleAfterScrolling(parent, dx, dy)) && (dx != 0 || dy != 0)) {
if (immediate) {
parent.scrollBy(dx, dy);
} else {
parent.smoothScrollBy(dx, dy);
}
return true;
} else {
return false;
}
}
可以看到有两个重载的方法,而第一个重载的方法调用了5个参数的重载方法,而5个参数的方法中有一段判断逻辑:
if ((!focusedChildVisible || this.isFocusedChildVisibleAfterScrolling(parent, dx, dy)) && (dx != 0 || dy != 0)) {
if (immediate) {
parent.scrollBy(dx, dy);
} else {
parent.smoothScrollBy(dx, dy);
}
return true;
} else {
return false;
}
明显看到了其中有两个滑动的操作,那么到底是不是这个引起的呢?用断点debug看一下:
命中了5个参数的方法,并且进入了判断逻辑,我们梳理一下几个重要的参数:
immediate = false;
focusedChildVisible = false;
dx = -522852;
dy = 0;
第一个if判断由于 !focusedChildVisible
成立并且 (dx != 0 || dy != 0)
成立,所以命中if代码块,紧接着第二个if判断中 immediate 为false,所以命中第二个判断的else代码块,也就是 parent.smoothScrollBy(dx, dy);
,dx = -522852;
而 dy = 0;
,所以会x方向滑动到最左侧,而y方向没有滑动,这正好印证了第一张Gif图的情况。
因此,得出的解决方案为继承LayoutManage,复写 requestChildRectangleOnScreen
5个参数的方法,因为4个参数的方法源码中也是调用的5个参数的,代码如下:
解决方案:重写LayoutManage的 requestChildRectangleOnScreen() 方法,如果是ScrollerView,则重写ScrollerView的该方法
class FixChildScrollBugLinearLayoutManager(context: Context, orientation: Int, reverseLayout: Boolean) : LinearLayoutManager(context, orientation, reverseLayout) {
override fun requestChildRectangleOnScreen(parent: RecyclerView, child: View, rect: Rect, immediate: Boolean, focusedChildVisible: Boolean): Boolean {
return false
}
}
事情的发展到了这里并没有结束,还记得是因为RecyclerView版本升级导致的问题么?为什么呢?肯定是两个版本的代码不一致,有疑问就要去验证,于是我查看了25.2.0的Recycler.LayoutManage关于 requestChildRectangleOnScreen
这个方法的代码,发现这个方法在这个版本只有4个参数的,并且其中的逻辑也大不相同:
public boolean requestChildRectangleOnScreen(RecyclerView parent, View child, Rect rect,
boolean immediate) {
final int parentLeft = getPaddingLeft();
final int parentTop = getPaddingTop();
final int parentRight = getWidth() - getPaddingRight();
final int parentBottom = getHeight() - getPaddingBottom();
final int childLeft = child.getLeft() + rect.left - child.getScrollX();
final int childTop = child.getTop() + rect.top - child.getScrollY();
final int childRight = childLeft + rect.width();
final int childBottom = childTop + rect.height();
final int offScreenLeft = Math.min(0, childLeft - parentLeft);
final int offScreenTop = Math.min(0, childTop - parentTop);
final int offScreenRight = Math.max(0, childRight - parentRight);
final int offScreenBottom = Math.max(0, childBottom - parentBottom);
// Favor the "start" layout direction over the end when bringing one side or the other
// of a large rect into view. If we decide to bring in end because start is already
// visible, limit the scroll such that start won't go out of bounds.
final int dx;
if (getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL) {
dx = offScreenRight != 0 ? offScreenRight
: Math.max(offScreenLeft, childRight - parentRight);
} else {
dx = offScreenLeft != 0 ? offScreenLeft
: Math.min(childLeft - parentLeft, offScreenRight);
}
// Favor bringing the top into view over the bottom. If top is already visible and
// we should scroll to make bottom visible, make sure top does not go out of bounds.
final int dy = offScreenTop != 0 ? offScreenTop
: Math.min(childTop - parentTop, offScreenBottom);
if (dx != 0 || dy != 0) {
if (immediate) {
parent.scrollBy(dx, dy);
} else {
parent.smoothScrollBy(dx, dy);
}
return true;
}
return false;
}
一切都明白了!