android:textIsSelectable="true"引起的RecyclerView自动滚动问题

从来都没想过这个属性会引起bug

问题描述

可以看这里https://www.jianshu.com/p/ff9df7c392e9
是在写上边的功能的时候碰到的。最早我是没问题的,好像是我把状态栏弄成透明以后就发现出问题了。每次进入页面recyclerview会自动往上滚动一段距离,

奇怪,而且监听onScrollStateChanged可以发现状态进去就成了2,也就是setting状态,可我根本没进行任何操作啊,我就设置了下数据。
然后就开始瞎折腾,想着把监听删了,额,监听是在滚动以后触发的,所以其实不设置监听它也滚动。想着是不是我给第一个item设置的addItemDecoration那个top 300的问题?我中间有次改成200发现好了。可我改回300又不行了,反正不知道是为啥,就放那不管了。
今早想着再试试,我就找了别的类,完事把那个类的item也改成这个类的item发现那个类的recyclerview也会自动滚上去。。
终于找到 是item的问题
那下边就开始把item里的东西一点一点的隐藏看看。
如下图,就一个相对布局,一个textview,我把相对布局invisible,完事发现问题还在,奇怪。
我另一个item本来复制的是这个,我这次就仿这个里边就写个textview,高度也一样,结果发下那边的没问题啊,然后我对比了一下,两个好像就差了个android:textIsSelectable="true",我就把这个加上,结果发现那边的也自动滚上去了,至此终于发现问题根源了。


    
//省略
    
    

测试

发现问题根源,测试的时候我就仔细看了下,我发现它自动滚上去的地方就是textview的底部,也就是本来正常状态textview是有一部分在屏幕外边看不见的,它会自动往上滚使textview完全可见。
然后我把textview高度改小,使得不滚动也能全部可见,果然它就不会自动滚动了。
完事我又想了下,那我textview的高度比屏幕高度还大的情况下,会是啥效果?
测试结果,这次是textview的顶部滚动动屏幕最上方为止。

结论

加上android:textIsSelectable="true"以后,如果textview的高度比屏幕高度还大,那么会让textview的top滚动到屏幕的顶部,
如果textview的高度没有屏幕高度大【而且textview有一部分不可见,在屏幕外边】,那么会让textview的bottomo滚动到屏幕的底部以使得textview完全可见。

其他人会碰到这问题吗

感觉不太会,一般人也不会没事弄个这么高的textview。我是测试用的,替代一堆复杂的布局,占个位置才这样的,而又为了顺手测试下文字选中功能,才加上这个。

如果真有人也碰到过,可以加个好友,几率这么低,难兄难弟以后多交流。

原因

感觉是设置了selectable以后,会进行foucus的处理,而这里会进行invalidate进行layout的
看写这篇帖子,感觉写的不错,原理和scrollview其实是样的
https://www.imooc.com/article/23067
看上边文章就行,下边只是跟着文章走一遍记录一下而已。
第6点,测试发现是无效的,难道姿势不对?
看下方法,确实的,设置了selectable以后,textview就会获取焦点的

    public void setTextIsSelectable(boolean selectable) {
        if (!selectable && mEditor == null) return; // false is default value with no edit data
        setFocusableInTouchMode(selectable);
        setFocusable(FOCUSABLE_AUTO);

        setClickable(selectable);
        setLongClickable(selectable);

    }

布局的加载过程,是从根root view开始一个一个add子child的
看下ViewGroup的addView方法

    public void addView(View child, int index, LayoutParams params) {

        // addViewInner() will call child.requestLayout() when setting the new LayoutParams
        // therefore, we call requestLayout() on ourselves before, so that the child's request
        // will be blocked at our level
        requestLayout();
        invalidate(true);
        addViewInner(child, index, params, false);
    }

继续

    private void addViewInner(View child, int index, LayoutParams params,
            boolean preventRequestLayout) {

        final boolean childHasFocus = child.hasFocus();
        if (childHasFocus) {
            requestChildFocus(child, child.findFocus());
        }

        if (child.hasDefaultFocus()) {
            // When adding a child that contains default focus, either during inflation or while
            // manually assembling the hierarchy, update the ancestor default-focus chain.
            setDefaultFocus(child);
        }
    }

go on

    public void requestChildFocus(View child, View focused) {
//设置了FOCUS_BLOCK_DESCENDANTS的属性,那么就被中断了,不会往上传递了,所以如果我们设置了focus的view的任何父类添加了这个属性,就不会传递给scrollview了。
        if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
            return;
        }

        // Unfocus us, if necessary
        super.unFocus(focused);

        // We had a previous notion of who had focus. Clear it.
        if (mFocused != child) {
            if (mFocused != null) {
                mFocused.unFocus(focused);
            }

//这里也能看到,如果add多个focus的child的话,最后的位置其实是最后一个child的位置。
            mFocused = child;
        }
        if (mParent != null) {
            mParent.requestChildFocus(this, focused);
        }
    }

可以看到代码结尾又调用了parent的同名方法,也就是一层一层往上走了,直到中断
如果没有重写这个方法,那么方法还是执行上边的代码
不过ScrollView,RecyclerView都重写了这个方法的,
ScrollView如下,可以看到对child进行了scroll操作。

    public void requestChildFocus(View child, View focused) {
        if (focused != null && focused.getRevealOnFocusHint()) {
            if (!mIsLayoutDirty) {//这个boolean值标记的是布局有没有完成,完成的话就是false
                scrollToChild(focused);
            } else {
                // The child may not be laid out yet, we can't compute the scroll yet
                mChildToScrollTo = focused;
            }
        }
//super方法[处理FOCUS_BLOCK_DESCENDANTS情况]是在上边滚动处理之后调用的,
//所以给ScrollView设置android:descendantFocusability="blocksDescendants"属性是无效的
        super.requestChildFocus(child, focused);
    }

下边的方法处理了上边的else情况。

    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        mIsLayoutDirty = false;
        // Give a child focus if it needs it
        if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
            scrollToChild(mChildToScrollTo);
        }
        mChildToScrollTo = null;
}

最后看下view的几个focus方法

    public boolean isFocused() {
        return (mPrivateFlags & PFLAG_FOCUSED) != 0;
    }
    public boolean hasFocus() {
        return (mPrivateFlags & PFLAG_FOCUSED) != 0;
    }
    public boolean hasFocus() {
        return (mPrivateFlags & PFLAG_FOCUSED) != 0 || mFocused != null;
    }

ViewGroup的,先看自己是不是,是就返回自己,不是就往下找,mFocused 这个变量上边代码有赋值

    public View findFocus() {

        if (isFocused()) {
            return this;
        }

        if (mFocused != null) {
            return mFocused.findFocus();
        }
        return null;
    }

我们看下上边分析的viewgroup的方法

    public void requestChildFocus(View child, View focused) {


        // Unfocus us, if necessary
        super.unFocus(focused);//child有焦点的话,会clear掉自己的focus标志

        // We had a previous notion of who had focus. Clear it.
        if (mFocused != child) {
            if (mFocused != null) {
                mFocused.unFocus(focused);//添加了新的focused view,上一个focused view会被清除focus标志
            }
            mFocused = child;
        }

    }

看下unFocus

    public void clearFocus() {
        clearFocusInternal(null, true, true);
    }

//看下if条件即可,可以发现,如果自身是focused的话,会执行非操作,也就是把focused属性给去掉了
    void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
        if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
            mPrivateFlags &= ~PFLAG_FOCUSED;

            if (propagate && mParent != null) {
                mParent.clearChildFocus(this);
            }

            onFocusChanged(false, 0, null);
            refreshDrawableState();

            if (propagate && (!refocus || !rootViewRequestFocus())) {
                notifyGlobalFocusCleared(this);
            }
        }
    }

早上写的浏览器关闭没了,懒得写了,看下日志,可以发现childFocus的调用并不是在addView里的,而且日志里打印的hasFouce都是false,可以看到是在onLayout之后操作的,而且先调用的是onFocus方法,从root view一层一层的往下

I: LinearLayoutTemp:addView start=========android.support.v7.widget.AppCompatTextView{5168538 V.ED..... ......ID 0,0-0,0}===false====null
I: LinearLayoutTemp:addView end=========android.support.v7.widget.AppCompatTextView{5168538 V.ED..... ......ID 0,0-0,0}===false====null
I: LinearLayoutTemp:addView start=========com.charliesong.demo0327.reader.LRTextView{1e72513 VFED..CL. ......ID 0,0-0,0 #7f0a024b app:id/tv_test}===false====null
I: LinearLayoutTemp:addView end=========com.charliesong.demo0327.reader.LRTextView{1e72513 VFED..CL. ......ID 0,0-0,0 #7f0a024b app:id/tv_test}===false====null
I: LinearLayoutTemp:addView start=========android.support.v7.widget.AppCompatTextView{c68c350 VFED..CL. ......ID 0,0-0,0 #7f0a024a app:id/tv_temp}===false====null
I: LinearLayoutTemp:addView end=========android.support.v7.widget.AppCompatTextView{c68c350 VFED..CL. ......ID 0,0-0,0 #7f0a024a app:id/tv_temp}===false====null
I: touch slop===========16
I: result0==============false/false======false==false
I: ScrollViewTemp:refreshDrawableState==========================
I: ScrollViewTemp:onMeasure===========start
I: LinearLayoutTemp:onMeasure===========start
I: LinearLayoutTemp:onMeasure==============end
I: ScrollViewTemp:onMeasure==============end
I: ScrollViewTemp:onMeasure===========start
I: LinearLayoutTemp:onMeasure===========start
I: LinearLayoutTemp:onMeasure==============end
I: ScrollViewTemp:onMeasure==============end
I: ScrollViewTemp:onLayout===========start
I: LinearLayoutTemp:onLayout===========start
I: LinearLayoutTemp:onLayout==============end
I: ScrollViewTemp:onLayout==============end
I: 0/4==========0/144
I: 5/9==========144/270
I: 10/13==========270/349
I: ScrollViewTemp:requestFocus start=======262144
I: ScrollViewTemp:onRequestFocusInDescendants start=======direction:2==null
I: ScrollViewTemp:onRequestFocusInDescendants=============com.charliesong.demo0327.reader.LRTextView{1e72513 VFED..CL. ......ID 0,30-750,596 #7f0a024b app:id/tv_test}
I: LRTextView:requestFocus start=======
I: LinearLayoutTemp:requestChildFocus==start=====com.charliesong.demo0327.reader.LRTextView{1e72513 VFED..CL. .F....ID 0,30-750,596 #7f0a024b app:id/tv_test}===========com.charliesong.demo0327.reader.LRTextView{1e72513 VFED..CL. .F....ID 0,30-750,596 #7f0a024b app:id/tv_test}
I: ScrollViewTemp:requestChildFocus==start=====com.charliesong.demo0327.reader.LinearLayoutTemp{c9829b0 V.E...... ......ID 0,0-768,655 #7f0a0132 app:id/layout1}===========com.charliesong.demo0327.reader.LRTextView{1e72513 VFED..CL. .F....ID 0,30-750,596 #7f0a024b app:id/tv_test}
I: ScrollViewTemp:requestChildFocus==end=====com.charliesong.demo0327.reader.LinearLayoutTemp{c9829b0 V.E...... ......ID 0,0-768,655 #7f0a0132 app:id/layout1}===========com.charliesong.demo0327.reader.LRTextView{1e72513 VFED..CL. .F....ID 0,30-750,596 #7f0a024b app:id/tv_test}
I: LinearLayoutTemp:requestChildFocus==end=====com.charliesong.demo0327.reader.LRTextView{1e72513 VFED..CL. .F....ID 0,30-750,596 #7f0a024b app:id/tv_test}===========com.charliesong.demo0327.reader.LRTextView{1e72513 VFED..CL. .F....ID 0,30-750,596 #7f0a024b app:id/tv_test}
I: ScrollViewTemp:onRequestFocusInDescendants end=======true
I: ScrollViewTemp:draw===========start
I: ScrollViewTemp:onDraw===========start
I: ScrollViewTemp:onDraw==============end
I: ScrollViewTemp:dispatchDraw===========start
I: LinearLayoutTemp:dispatchDraw===========start
I: onDraw===========16=========16
I: LinearLayoutTemp:dispatchDraw==============end
I: ScrollViewTemp:dispatchDraw==============end
I: ScrollViewTemp:draw==============end

看下ScrollView

    override fun requestFocus(direction: Int, previouslyFocusedRect: Rect?): Boolean {
        println("${javaClass.simpleName}:requestFocus start=======${descendantFocusability}")
        return super.requestFocus(direction, previouslyFocusedRect)
    }

//super的方法如下,因为我们没有设置,所以默认走的是第三个FOCUS_AFTER_DESCENDANTS
    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
        int descendantFocusability = getDescendantFocusability();

        switch (descendantFocusability) {
            case FOCUS_BLOCK_DESCENDANTS:
                return super.requestFocus(direction, previouslyFocusedRect);
            case FOCUS_BEFORE_DESCENDANTS:{}
            case FOCUS_AFTER_DESCENDANTS: {
                final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
                return took ? took : super.requestFocus(direction, previouslyFocusedRect);
            }
            default:
        }
    }

然后ScrollView重写的如下的方法,这里会找到第一个设置了focus的view,我们例子里的LRTextView

    protected boolean onRequestFocusInDescendants(int direction,
            Rect previouslyFocusedRect) {

        // convert from forward / backward notation to up / down / left / right
        // (ugh).
        if (direction == View.FOCUS_FORWARD) {
            direction = View.FOCUS_DOWN;
        } else if (direction == View.FOCUS_BACKWARD) {
            direction = View.FOCUS_UP;
        }

        final View nextFocus = previouslyFocusedRect == null ?
                FocusFinder.getInstance().findNextFocus(this, null, direction) :
                FocusFinder.getInstance().findNextFocusFromRect(this,
                        previouslyFocusedRect, direction);

        if (nextFocus == null) {
            return false;
        }

        if (isOffScreen(nextFocus)) {
            return false;
        }

        return nextFocus.requestFocus(direction, previouslyFocusedRect);
    }

如何找到这个view后边再分析,先看下最后一行代码干啥了

    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
        return requestFocusNoSearch(direction, previouslyFocusedRect);
    }

    private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
    //省略
        handleFocusGainInternal(direction, previouslyFocusedRect);
        return true;
    }
//下边方法就找到了我们要的requestChildFocus,然后一层一层往上走,和我们的日志一样
    void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
        if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
            mPrivateFlags |= PFLAG_FOCUSED;

            if (mParent != null) {
                mParent.requestChildFocus(this, this);//这里调用的
                updateFocusedInCluster(oldFocus, direction);
            }
//省略
        }
    }

现在分析下咋找到那个focus的view的

        final View nextFocus = previouslyFocusedRect == null ?
                FocusFinder.getInstance().findNextFocus(this, null, direction) :
                FocusFinder.getInstance().findNextFocusFromRect(this,
                        previouslyFocusedRect, direction);
//首次加载布局的时候,那个rect是空的,所以走的findNextFocus,direction = View.FOCUS_DOWN
    public final View findNextFocus(ViewGroup root, View focused, int direction) {
        return findNextFocus(root, focused, null, direction);
    }
    private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
        View next = null;
        ViewGroup effectiveRoot = getEffectiveRoot(root, focused);//focused为空,返回的还是root
        if (focused != null) {
            next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
        }
        if (next != null) {
            return next;
        }
        ArrayList focusables = mTempList;
        try {
            focusables.clear();
            effectiveRoot.addFocusables(focusables, direction);
//这个看viewgroup里的方法,就是找到所有focus的View添加进来,我们例子中有2个,
//具体实现在ViewGroup方法里同名方法,如果child是ViewGroup,会继续调用同名方法,添加它的子view
            if (!focusables.isEmpty()) {//最后执行的是这句
                next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
            }
        } finally {
            focusables.clear();
        }
        return next;
    }

继续上边的findNextFocus方法,第二个第三个参数都是null

    private View findNextFocus(ViewGroup root, View focused, Rect focused,
            int direction, ArrayList focusables) {
        if (focused != null) {
        } else {
            if (focusedRect == null) {
                focusedRect = mFocusedRect;
                // make up a rect at top left or bottom right of root
                switch (direction) {
                    case View.FOCUS_RIGHT:
                    case View.FOCUS_DOWN://是这个
                        setFocusTopLeft(root, focusedRect);
                        break;
                    }
                }
            }
        }

        switch (direction) {
            case View.FOCUS_UP:
            case View.FOCUS_DOWN:
            case View.FOCUS_LEFT:
            case View.FOCUS_RIGHT:
                return findNextFocusInAbsoluteDirection(focusables, root, focused,
                        focusedRect, direction);
            default:
                throw new IllegalArgumentException("Unknown direction: " + direction);
        }
    }

//继续往下。。。。。。。。。。。。。。。
    View findNextFocusInAbsoluteDirection(ArrayList focusables, ViewGroup root, View focused,
            Rect focusedRect, int direction) {
        mBestCandidateRect.set(focusedRect);
        switch(direction) {
            case View.FOCUS_DOWN:
                mBestCandidateRect.offset(0, -(focusedRect.height() + 1));
        }

        View closest = null;

        int numFocusables = focusables.size();
        for (int i = 0; i < numFocusables; i++) {
            View focusable = focusables.get(i);//

            // only interested in other non-root views
            if (focusable == focused || focusable == root) continue;

            // get focus bounds of other view in same coordinate system
            focusable.getFocusedRect(mOtherRect);//获取的就是view的布局rect
            root.offsetDescendantRectToMyCoords(focusable, mOtherRect);//

//下边的if里方法条件判断太复杂了,看的脑袋大,就不研究了。
            if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
//找到第一个就把rect赋值给mBestCandidateRect,参与下一个view的比较,看谁更合适。
                mBestCandidateRect.set(mOtherRect);
                closest = focusable;
            }
        }
        return closest;
    }


判断谁更靠谱好像最后应该走的这里,我们例子是FOCUS_DOWN,换句话说,看谁的top更接小谁就更靠谱

    static int majorAxisDistanceRaw(int direction, Rect source, Rect dest) {
        switch (direction) {
            case View.FOCUS_LEFT:
                return source.left - dest.right;
            case View.FOCUS_RIGHT:
                return dest.left - source.right;
            case View.FOCUS_UP:
                return source.top - dest.bottom;
            case View.FOCUS_DOWN:
                return dest.top - source.bottom;
        }
        throw new IllegalArgumentException("direction must be one of "
                + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    }

简单结论

从最底层的root view开始执行requestFocus
ViewGroup,分了3种情况,第三种是默认的
第一种,会执行super,也就是view的同名方法,换句话说就是直接判断自己是否可以获取焦点
第二种,先super,就是先判断自己能不能获取焦点,能那就自己处理,不能再往下找
第三种,相反,先找最里边的。

    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
        int descendantFocusability = getDescendantFocusability();

        switch (descendantFocusability) {
            case FOCUS_BLOCK_DESCENDANTS:
                return super.requestFocus(direction, previouslyFocusedRect);
            case FOCUS_BEFORE_DESCENDANTS: {
                final boolean took = super.requestFocus(direction, previouslyFocusedRect);
                return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
            }
            case FOCUS_AFTER_DESCENDANTS: {
                final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
                return took ? took : super.requestFocus(direction, previouslyFocusedRect);
            }
            default:
        }
    }

看下第三种默认的,ViewGroup的,默认就是一层一层的去找的,而ScrollView是重写了这个方法的,上边有分析

    protected boolean onRequestFocusInDescendants(int direction,
            Rect previouslyFocusedRect) {
        int index;
        int increment;
        int end;
        int count = mChildrenCount;
        if ((direction & FOCUS_FORWARD) != 0) {
            index = 0;
            increment = 1;
            end = count;
        } else {
            index = count - 1;
            increment = -1;
            end = -1;
        }
        final View[] children = mChildren;
        for (int i = index; i != end; i += increment) {
            View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                if (child.requestFocus(direction, previouslyFocusedRect)) {
                    return true;
                }
            }
        }
        return false;
    }

其他

RecyclerView滚动是一样的道理,最后能看到scroll的字段的

    public void requestChildFocus(View child, View focused) {
        if (!mLayout.onRequestChildFocus(this, mState, child, focused) && focused != null) {
            requestChildOnScreen(child, focused);
        }
        super.requestChildFocus(child, focused);
    }


    private void requestChildOnScreen(@NonNull View child, @Nullable View focused) {

        mLayout.requestChildRectangleOnScreen(this, child, mTempRect, !mFirstLayoutComplete,
                (focused == null));
    }



        public boolean requestChildRectangleOnScreen(RecyclerView parent, View child, Rect rect,
                boolean immediate,
                boolean focusedChildVisible) {
            int[] scrollAmount = getChildRectangleOnScreenScrollAmount(parent, child, rect,
                    immediate);
            int dx = scrollAmount[0];
            int dy = scrollAmount[1];
            if (!focusedChildVisible || isFocusedChildVisibleAfterScrolling(parent, dx, dy)) {
                if (dx != 0 || dy != 0) {
                    if (immediate) {
                        parent.scrollBy(dx, dy);
                    } else {
                        parent.smoothScrollBy(dx, dy);
                    }
                    return true;
                }
            }
            return false;
        }

RecyclerView是一样的道理,解决办法也都一样,在这个view的子view里添加
android:descendantFocusability="blocksDescendants"

ListView看了下,并没有处理requestChildFocus,所以没有这个问题。

你可能感兴趣的:(android:textIsSelectable="true"引起的RecyclerView自动滚动问题)