美文网首页
parameter must be a descendant o

parameter must be a descendant o

作者: 薛瑄 | 来源:发表于2020-01-04 15:54 被阅读0次

    前言

    最近的一系列源码分析,都是基于一个错误,逐步深入源码。这样更有目的性的看源码,思路会更清楚一点。
    网络上有文章给出了有针对性的解决方案。我通过源码给出更普通的解决思路,这个问题,没有特定的解决方案,所以只能领会精髓后,随机应变。

    下面通过我遇到的具体问题,展开源码的分析,所以不必太在意业务场景的相似,重在领会精髓

    报错

    我的具体场景是,在从某一个界面跳转到登录界面时,点击输入框EditText 时,出现的崩溃。

    java.lang.IllegalArgumentException: parameter must be a descendant of this view
            at android.view.ViewGroup.offsetRectBetweenParentAndChild(ViewGroup.java:6078)
            at android.view.ViewGroup.offsetDescendantRectToMyCoords(ViewGroup.java:6007)
            at android.view.FocusFinder.findNextFocusInAbsoluteDirection(FocusFinder.java:365)
            at android.view.FocusFinder.findNextFocus(FocusFinder.java:268)
            at android.view.FocusFinder.findNextFocus(FocusFinder.java:110)
            at android.view.FocusFinder.findNextFocus(FocusFinder.java:80)
            at android.view.ViewGroup.focusSearch(ViewGroup.java:1027)
            at android.view.ViewGroup.focusSearch(ViewGroup.java:1029)
            at android.view.ViewGroup.focusSearch(ViewGroup.java:1029)
            at android.view.ViewGroup.focusSearch(ViewGroup.java:1029)
            at android.view.ViewGroup.focusSearch(ViewGroup.java:1029)
            at android.view.ViewGroup.focusSearch(ViewGroup.java:1029)
            at android.view.ViewGroup.focusSearch(ViewGroup.java:1029)
            at android.view.ViewGroup.focusSearch(ViewGroup.java:1029)
            at android.view.ViewGroup.focusSearch(ViewGroup.java:1029)
            at android.view.ViewGroup.focusSearch(ViewGroup.java:1029)
            at android.view.View.focusSearch(View.java:10843)
            at android.widget.TextView.onCreateInputConnection(TextView.java:7862)
            at androidx.appcompat.widget.AppCompatEditText.onCreateInputConnection(AppCompatEditText.java:186)
            at android.view.inputmethod.InputMethodManager.startInputInner(InputMethodManager.java:1290)
            at android.view.inputmethod.InputMethodManager.checkFocus(InputMethodManager.java:1485)
            at android.view.inputmethod.InputMethodManager.viewClicked(InputMethodManager.java:1667)
            at android.widget.TextView.viewClicked(TextView.java:12009)
            at android.widget.TextView.onTouchEvent(TextView.java:10109)
            at android.view.View.dispatchTouchEvent(View.java:12513)
            at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
            at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2719)
            at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
            at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2719)
            at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
            at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2719)
            at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
            at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2719)
            at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
            at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2719)
            at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
            at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2719)
            at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
            at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2719)
            at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
            at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2719)
            at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
            at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2719)
            at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)
            at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2719)
            at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:440)
            at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1830)
            at android.app.Activity.dispatchTouchEvent(Activity.java:3400)
    

    源码分析 Android 获取View焦点的流程

    深入剖析崩溃的原因,涉及到Android其他方面的知识,所以这里只分析到引出这个异常的地方

    下面是跳转到登录界面后,点击输入框EditText 时,点击事件层层分发,到focusSearch 在指定方向上,搜索下一个可以获取焦点的View

    在这里插入图片描述 mParent.focusSearch(this, direction)有两处实现,分别是:RecyclerView 、ViewGroup

    下面分析ViewGroup中的focusSearch(this, direction)

    代码段1
        /**
         * Find the nearest view in the specified direction that wants to take
         * focus.
         *
         * @param focused The view that currently has focus
         * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and
         *        FOCUS_RIGHT, or 0 for not applicable.
         */
        @Override
        public View focusSearch(View focused, int direction) {
           
            if (isRootNamespace()) {
               //如果是根布局也就是 DecorView    
                // root namespace means we should consider ourselves the top of the
                // tree for focus searching; otherwise we could be focus searching
                // into other tabs.  see LocalActivityManager and TabHost for more info.
                return FocusFinder.getInstance().findNextFocus(this, focused, direction);
            } else if (mParent != null) {
                //不断调用父视图的focusSearch
                return mParent.focusSearch(focused, direction);
            }
            return null;
        }
    

    接着调用了FocusFinder 里面的函数 findNextFocus(ViewGroup root, View focused, int direction)

    代码段2
        /**
         * Find the next view to take focus in root's descendants, starting from the view
         * that currently is focused.
         * @param root Contains focused. Cannot be null.
         * @param focused Has focus now.
         * @param direction Direction to look.
         * @return The next focusable view, or null if none exists.
         */
        public final View findNextFocus(ViewGroup root, View focused, int direction) {
            return findNextFocus(root, focused, null, direction);
        }
    
    代码段3
        private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
            View next = null;
            ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
            if (focused != null) {
                //查找下一个由用户指定的可获取焦点的View
                //那么如何指定下一个获取焦点的view呢?通过View 中的一系列函数 setNextFocusLeftId setNextFocusRightId setNextFocusUpId setNextFocusDownId setNextFocusForwardId 来设置
                next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
            }
            if (next != null) {
                //如果找到符合条件的view,则返回
                return next;
            }
    
            // 如果没有找到,则通过遍历root(也就是DecorView)下所有的可获取焦点的非touch_mode的 view 
            ArrayList<View> focusables = mTempList;
            try {
                focusables.clear();
                //遍历是从这个函数开始的,所有符合条件的view被添加到focusables
                effectiveRoot.addFocusables(focusables, direction);
                if (!focusables.isEmpty()) {
                   //在focusables 中查询,下一个可以获取焦点的view
                    next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
                }
            } finally {
                focusables.clear();
            }
            return next;
        }
    

    下面详细的分析一下 effectiveRoot.addFocusables(focusables, direction);

    代码段4
        /**
         * Add any focusable views that are descendants of this view (possibly
         * including this view if it is focusable itself) to views.  If we are in touch mode,
         * only add views that are also focusable in touch mode.
         *
         * @param views Focusable views found so far
         * @param direction The direction of the focus
         */
        public void addFocusables(ArrayList<View> views, @FocusDirection int direction) {
    
            //这个方法有5个地方实现了它:DrawerLayout、RecyclerView、View、ViewGroup、ViewPage
            addFocusables(views, direction, isInTouchMode() ? FOCUSABLES_TOUCH_MODE : FOCUSABLES_ALL);
        }
    

    下面主要对ViewGroup和View 中的addFocusables 进行分析

    View 中的addFocusables 函数

    代码段5
        public void addFocusables(ArrayList<View> views, @FocusDirection int direction,
                @FocusableMode int focusableMode) {
            if (views == null) {
                return;
            }
            //如果不能获取焦点,就不添加到views中,直接返回
            if (!canTakeFocus()) {
                return;
            }
            //如果是触摸模式,并且在触摸模式下不能获取焦点,直接返回
            //也就是说,如果不是触摸模式或者触摸模式下可获取焦点,就添加到views
            if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE
                    && !isFocusableInTouchMode()) {
                return;
            }
            views.add(this);
        }
    

    关于TOUCH_MODE更详细的说明,参考官方博客

    ViewGroup 中的addFocusables 函数

    代码段6
        @Override
        public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
            final int focusableCount = views.size();
            //自身ViewGroup与它后代view的关系,是在后代view之前、之后获取焦点,或者不让后台view 获取焦点
            final int descendantFocusability = getDescendantFocusability();
            final boolean blockFocusForTouchscreen = shouldBlockFocusForTouchscreen();
             //isFocusableInTouchMode() 在touchMode下 是否可以获取或保持焦点
            final boolean focusSelf = (isFocusableInTouchMode() || !blockFocusForTouchscreen);
    
            //后代view不能获取焦点
            if (descendantFocusability == FOCUS_BLOCK_DESCENDANTS) {
                //自己可以获取焦点
                if (focusSelf) {
                    //调用view 中的addFocusables,把当前布局添加到views
                    super.addFocusables(views, direction, focusableMode);
                }
                return;
            }
    
            if (blockFocusForTouchscreen) {
                focusableMode |= FOCUSABLES_TOUCH_MODE;
            }
            //在后代view之前获取焦点,并且自己可以获取焦点
            if ((descendantFocusability == FOCUS_BEFORE_DESCENDANTS) && focusSelf) {
                //调用view 中的addFocusables,把当前布局添加到views
                super.addFocusables(views, direction, focusableMode);
            }
    
            int count = 0;
            final View[] children = new View[mChildrenCount];
            for (int i = 0; i < mChildrenCount; ++i) {
                View child = mChildren[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                   //获取当前view 下可见的子view
                    children[count++] = child;
                }
            }
            FocusFinder.sort(children, 0, count, this, isLayoutRtl());
            for (int i = 0; i < count; ++i) {
                //遍历子view,如果是view就添加到 views,如果是viewGroup就再次调用addFocusables进行判断
                children[i].addFocusables(views, direction, focusableMode);
            }
    
            // When set to FOCUS_AFTER_DESCENDANTS, we only add ourselves if
            // there aren't any focusable descendants.  this is
            // to avoid the focus search finding layouts when a more precise search
            // among the focusable children would be more interesting.
            //在后代view之后获取焦点,并且自己可以获取焦点 并且仅在没有可聚焦后代(views的数量没有变)的情况下添加自己
            if ((descendantFocusability == FOCUS_AFTER_DESCENDANTS) && focusSelf
                    && focusableCount == views.size()) {
                super.addFocusables(views, direction, focusableMode);
            }
        }
    

    这篇文章所给出的解决方法,就是

    代码段3 中effectiveRoot.addFocusables(focusables, direction);调用后 ,focusables 中是所有可获取焦点的View,在非空的情况下调用如下代码findNextFocus函数

    代码段7
        private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
                int direction, ArrayList<View> focusables) {
            if (focused != null) {
                //本篇文章分析的流程,focused 不为空
                if (focusedRect == null) {
                    focusedRect = mFocusedRect;
                }
                // fill in interesting rect from focused
                //获取focused所在的矩形区域到mOtherRect中
                focused.getFocusedRect(focusedRect);
                //把focused的坐标,转换为相对于root的坐标
                root.offsetDescendantRectToMyCoords(focused, focusedRect);
            } else {
               //如果focused  为空,就在root布局的指定方向添加一个focusedRect
                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;
                        case View.FOCUS_FORWARD:
                            if (root.isLayoutRtl()) {
                                setFocusBottomRight(root, focusedRect);
                            } else {
                                setFocusTopLeft(root, focusedRect);
                            }
                            break;
    
                        case View.FOCUS_LEFT:
                        case View.FOCUS_UP:
                            setFocusBottomRight(root, focusedRect);
                            break;
                        case View.FOCUS_BACKWARD:
                            if (root.isLayoutRtl()) {
                                setFocusTopLeft(root, focusedRect);
                            } else {
                                setFocusBottomRight(root, focusedRect);
                            break;
                        }
                    }
                }
            }
    
            switch (direction) {
                case View.FOCUS_FORWARD:
                case View.FOCUS_BACKWARD:
                    return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect,
                            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);
            }
        }
    
    • 如果focused不是null,说明当前获取到焦点的View存在,则获得绘制焦点的Rect到focusedRect,然后根据rootView遍历所有ParentView从子View纠正坐标到根View坐标。
    • 如果focused是null,则说明当前没有View获取到焦点,则把focusedRect根据不同的direction重置为“一点”。

    根据direction调用FocusFinder::findNextFocusInAbsoluteDirection方法进行对比查找“下一个”View。

    代码段8
        View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused,
                Rect focusedRect, int direction) {
            // initialize the best candidate to something impossible
            // (so the first plausible view will become the best choice)
            //先设置focusedRect 为最佳的候选矩阵
            mBestCandidateRect.set(focusedRect);
            //根据不同的方向,偏移一个像素,为了方便比较?
            switch(direction) {
                case View.FOCUS_LEFT:
                    mBestCandidateRect.offset(focusedRect.width() + 1, 0);
                    break;
                case View.FOCUS_RIGHT:
                    mBestCandidateRect.offset(-(focusedRect.width() + 1), 0);
                    break;
                case View.FOCUS_UP:
                    mBestCandidateRect.offset(0, focusedRect.height() + 1);
                    break;
                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所在的矩形区域到mOtherRect中,这个focusable是之前获取的可聚焦的views
                focusable.getFocusedRect(mOtherRect);
                //把focusable的坐标(矩阵),转换为相对于root的坐标(矩阵)
                root.offsetDescendantRectToMyCoords(focusable, mOtherRect);
                //mOtherRect 是否比mBestCandidateRect 更优
                if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
                    //如果mOtherRect 更优,则mBestCandidateRect设置为mOtherRect ,for循环结束后,得到最优的
                    mBestCandidateRect.set(mOtherRect);
                    closest = focusable;
                }
            }
            //返回最优的view
            return closest;
        }
    
    代码段9
        /**
         * Offset a rectangle that is in a descendant's coordinate
         * space into our coordinate space.
         * @param descendant A descendant of this view
         * @param rect A rectangle defined in descendant's coordinate space.
         */
        public final void offsetDescendantRectToMyCoords(View descendant, Rect rect) {
            offsetRectBetweenParentAndChild(descendant, rect, true, false);
        }
    
    代码段10
        /**
         * Helper method that offsets a rect either from parent to descendant or
         * descendant to parent.
         */
        void offsetRectBetweenParentAndChild(View descendant, Rect rect,
                boolean offsetFromChildToParent, boolean clipToBounds) {
    
            // already in the same coord system :)
            if (descendant == this) {
                return;
            }
    
            ViewParent theParent = descendant.mParent;
    
            // search and offset up to the parent
            //通过不断的循环,把descendant的坐标,也就是矩阵rect,转换为相对于当前view(因为offsetRectBetweenParentAndChild是view的方法)的坐标(矩阵)
            while ((theParent != null)
                    && (theParent instanceof View)
                    && (theParent != this)) {
                
                if (offsetFromChildToParent) {
                   //偏移矩阵,例如:布局viewGoupA 里面有viewGoupB 里面有 view C
                   //把view C 相对于父布局viewGoupB的坐标转换为相对于viewGoupA的坐标
                   //这里mLeft 相对于父布局的x坐标,-x方向滚动的距离mScrollX,才是原来真是的位置
                    rect.offset(descendant.mLeft - descendant.mScrollX,
                            descendant.mTop - descendant.mScrollY);
                    if (clipToBounds) {
                       //修剪矩阵
                        View p = (View) theParent;
                        //intersect  压紧到公共区域
                        boolean intersected = rect.intersect(0, 0, p.mRight - p.mLeft,
                                p.mBottom - p.mTop);
                        if (!intersected) {
                            rect.setEmpty();
                        }
                    }
                } else {
                     //上面的反向操作,布局viewGoupA 里面有viewGoupB 里面有 view C
                     //view C 已经转为相对viewGoupA的坐标了,下面的操作就是转为相对于viewGoupB的坐标
                    if (clipToBounds) {
                        View p = (View) theParent;
                        boolean intersected = rect.intersect(0, 0, p.mRight - p.mLeft,
                                p.mBottom - p.mTop);
                        if (!intersected) {
                            rect.setEmpty();
                        }
                    }
                    //偏移矩阵,从父view 到子view
                    rect.offset(descendant.mScrollX - descendant.mLeft,
                            descendant.mScrollY - descendant.mTop);
                }
    
                descendant = (View) theParent;
                theParent = descendant.mParent;
            }
    
            // now that we are up to this view, need to offset one more time
            // to get into our coordinate space
            if (theParent == this) {
                if (offsetFromChildToParent) {
                    rect.offset(descendant.mLeft - descendant.mScrollX,
                            descendant.mTop - descendant.mScrollY);
                } else {
                    rect.offset(descendant.mScrollX - descendant.mLeft,
                            descendant.mScrollY - descendant.mTop);
                }
            } else {
               //经过我的分析,这个错误只有两种情况
                throw new IllegalArgumentException("parameter must be a descendant of this view");
            }
        }
    

    <font color =blue>经过我的分析,这个错误只有两种情况:

    1. descendant 不是当前view 的后代 ,(theParent instanceof View) == false 退出循环 ,descendant 为ViewRootImpl (就是这个解决方法[Another java.lang.IllegalArgumentException: parameter must be a descendant of this view](https://stackoverflow.com/questions/30585561/another-java-lang-illegalargumentexception-parameter-must -be-a-descendant-of-th))
    2. descendant 的mParent为空 (theParent != null) == false 退出循环。我遇到的就是这个问题


      在这里插入图片描述

    下面继续回到代码段8,来看看函数isBetterCandidate是怎么比较出更优的rect

    代码段11
        /**
         * Is rect1 a better candidate than rect2 for a focus search in a particular
         * direction from a source rect?  This is the core routine that determines
         * the order of focus searching.
         * @param direction the direction (up, down, left, right)
         * @param source The source we are searching from
         * @param rect1 The candidate rectangle
         * @param rect2 The current best candidate.
         * @return Whether the candidate is the new best.
         */
        boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) {
    
            // to be a better candidate, need to at least be a candidate in the first
            // place :)
            //rect1是否在source的指定方向direction的下一个可获得焦点的矩阵
            if (!isCandidate(source, rect1, direction)) {
               //rect1不是候选的,说明rect1 没有rect2 更优,返回false
                return false;
            }
    
            // we know that rect1 is a candidate.. if rect2 is not a candidate,
            // rect1 is better
            if (!isCandidate(source, rect2, direction)) {
              //rect2不是候选的,说明rect1 比rect2 更优,返回true
                return true;
            }
    
    
            //如果都是候选的,比较rect1 和rect2 哪个更优,比较的方法大概是:两个候选rect分表与source比较,是否重叠,是否在希望的方向上等
            // if rect1 is better by beam, it wins
            if (beamBeats(direction, source, rect1, rect2)) {
                return true;
            }
    
            // if rect2 is better, then rect1 cant' be :)
            if (beamBeats(direction, source, rect2, rect1)) {
                return false;
            }
    
            // otherwise, do fudge-tastic comparison of the major and minor axis
            return (getWeightedDistanceFor(
                            majorAxisDistance(direction, source, rect1),
                            minorAxisDistance(direction, source, rect1))
                    < getWeightedDistanceFor(
                            majorAxisDistance(direction, source, rect2),
                            minorAxisDistance(direction, source, rect2)));
        }
    

    解决方法:

    根据上面的原因,对应两种解决方法:
    1、就是可获取焦点的view是在报错View的后代
    2、保证可获取焦点view的mParent 不为null

    这篇文章,与我分析的第二种解决方案一样,他给出的解决方案更为具体,可以参考
    【原创】【ViewFlow+GridView】Parameter must be a descendant of this view问题分析

    这篇文章,分析了本篇文章中,未涉及到的其他的几处代码实现
    Android焦点流程代码分析

    相关文章

      网友评论

          本文标题:parameter must be a descendant o

          本文链接:https://www.haomeiwen.com/subject/zatfactx.html