RecyclerView嵌套或者ScrollView嵌套,包含EditText,EditText获取焦点时滑动异常问题解决记录

2 篇文章 0 订阅
2 篇文章 0 订阅

最近在做公司项目的Android适配工作,将support依赖都升级到了28.0.0,很多问题扑面而来,最让我苦恼的就是RecyclerView嵌套RecyclerView时,item中的EditText获取焦点时,横向滑动的RecyclerView会自动滚动到最前面,我依稀记得在原来遇到过,同样是升级了RecyclerView的依赖版本后出现,上一次的解决方式是把版本又降回去,但是这样治标不治本,趁着这次把病根给它解决掉。

首先用 Gif 图展示下升级RecyclerView版本之后产生的问题(RecyclerView版本为28.0.0):
28.0.0版本
正常的情况应该如下(此RecyclerView版本为25.2.0):
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看一下:
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.0Recycler.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;
}

一切都明白了!

致谢:
农民伯伯 - Android 中文 API (100) —— ScrollView

由于本人水平有限,本篇文章并没有对源码中涉及到的细节进行逐一解读,主要是分享自己发现问题、定位问题、解决问题的思路,仅此而已

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值