防止/捕获“IllegalArgumentException:参数必须是此视图的后代”错误

66

我有一个ListView,里面有一些可聚焦的组件(大部分是EditText)。是的,我知道这不是推荐的做法,但总的来说,几乎所有的东西都能正常工作,并且焦点会到达应该去的地方(我必须编写一些调整才能实现)。无论如何,我的问题是,在手指滚动列表并突然使用轨迹球“显示IME键盘”时,存在奇怪的竞态条件。某些东西必须越界并被回收,此时offsetRectBetweenParentAndChild()方法必须启动并抛出IllegalArgumentException。

问题在于,这个异常是在我无法插入try/catch块的任何块之外抛出的(据我所知)。因此,这个问题有两个有效的解决方案:

  1. 有人知道为什么会抛出这个异常以及如何阻止它发生
  2. 有人知道如何在至少让我的应用程序存活的某个位置放置try/catch块。据我所知,问题在于焦点,因此它绝对不应该使我的应用程序崩溃(这就是它正在做的事情)。我尝试覆盖ViewGroup的方法,但这两个offset * 方法标记为final。

堆栈跟踪:

08-17 18:23:09.825: ERROR/AndroidRuntime(1608): FATAL EXCEPTION: main
08-17 18:23:09.825: ERROR/AndroidRuntime(1608): java.lang.IllegalArgumentException: parameter must be a descendant of this view
08-17 18:23:09.825: ERROR/AndroidRuntime(1608):     at android.view.ViewGroup.offsetRectBetweenParentAndChild(ViewGroup.java:2633)
08-17 18:23:09.825: ERROR/AndroidRuntime(1608):     at android.view.ViewGroup.offsetDescendantRectToMyCoords(ViewGroup.java:2570)
08-17 18:23:09.825: ERROR/AndroidRuntime(1608):     at android.view.ViewRoot.scrollToRectOrFocus(ViewRoot.java:1624)
08-17 18:23:09.825: ERROR/AndroidRuntime(1608):     at android.view.ViewRoot.draw(ViewRoot.java:1357)
08-17 18:23:09.825: ERROR/AndroidRuntime(1608):     at android.view.ViewRoot.performTraversals(ViewRoot.java:1258)
08-17 18:23:09.825: ERROR/AndroidRuntime(1608):     at android.view.ViewRoot.handleMessage(ViewRoot.java:1859)
08-17 18:23:09.825: ERROR/AndroidRuntime(1608):     at android.os.Handler.dispatchMessage(Handler.java:99)
08-17 18:23:09.825: ERROR/AndroidRuntime(1608):     at android.os.Looper.loop(Looper.java:130)
08-17 18:23:09.825: ERROR/AndroidRuntime(1608):     at android.app.ActivityThread.main(ActivityThread.java:3683)
08-17 18:23:09.825: ERROR/AndroidRuntime(1608):     at java.lang.reflect.Method.invokeNative(Native Method)
08-17 18:23:09.825: ERROR/AndroidRuntime(1608):     at java.lang.reflect.Method.invoke(Method.java:507)
08-17 18:23:09.825: ERROR/AndroidRuntime(1608):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:839)
08-17 18:23:09.825: ERROR/AndroidRuntime(1608):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:597)
08-17 18:23:09.825: ERROR/AndroidRuntime(1608):     at dalvik.system.NativeStart.main(Native Method)

1
不管值不值得(或者谁看到这条信息),我已经放弃了在这个活动中使用ListView的方法。除了随机崩溃之外,几乎不可能正确设置焦点行为而不设置windowSoftInputMode="adjustPan",这会带来很多其他的问题。相反,我选择了一个“简单”的ScrollView,这一做法已经非常好用了。 - dmon
1
遇到了类似的问题,使用带有EditText的ListView并不值得。实际上,这个问题对我来说是一个致命错误。 - devmiles.com
1
ListView中替代EditText的一种方法是使用样式为EditTextTextView,当点击时弹出一个带有EditText的样式化对话框。 - FunkTheMonk
我正在RecyclerView项中使用EditText,在点击第二个项的EditText时出现异常。 - Vivek Pratap Singh
如果有人在使用Google广告的Android React Native应用程序中遇到了此崩溃,并且看到了与此相关的问题上的评论,请查看已经确定的解决方法。 - Jadent
显示剩余2条评论
18个回答

40

虽然 Bruce 的答案 解决了这个问题,但是它采用了一种非常粗暴的方式,会影响用户体验,因为一旦我们滚动过视图,它就会清除每个视图的焦点。

它处理了问题的症状,但没有解决实际原因。

如何重现这个问题:

您的 EditText 具有焦点并且键盘已打开,然后您将滚动到 EditText 移出屏幕的位置,并且它没有被回收到一个新的 EditText 中再次显示。

首先让我们了解为什么会出现这个问题:

正如大家所知,ListView 会回收视图并重新使用它们,但有时它不需要立即使用已经移出屏幕的视图,所以它会保留该视图以备将来使用,由于它不再需要显示,因此会解除附加,导致该视图.mParent 变成 null。

然而,键盘需要知道如何传递输入,它通过选择具有焦点的视图,或者更准确地说是 EditText 来完成。

因此,问题在于我们有一个具有焦点的 EditText,但突然没有了父级,所以我们得到了“参数必须是此视图的后代”错误。这很有道理。

使用滚动监听器会造成更多问题。

解决方案:

我们需要监听一个事件,它将告诉我们一个视图已经移出堆栈并且不再附加,幸运的是 ListView 公开了这个事件。

listView.setRecyclerListener(new AbsListView.RecyclerListener() {
        @Override
        public void onMovedToScrapHeap(View view) {
            if ( view.hasFocus()){
                view.clearFocus(); //we can put it inside the second if as well, but it makes sense to do it to all scraped views
                //Optional: also hide keyboard in that case
                if ( view instanceof EditText) {
                    InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
                    imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
                }
            }
        }
    });

你有能够重现这个异常的示例代码吗? - Guillaume Perrot
我没有样例代码,但我认为它相当容易复制: 创建一个listView, 创建许多带有编辑文本的listView行, 运行,然后聚焦于其中一个editText(键盘将被打开), 现在滚动,使得该行不在屏幕上(而键盘仍然打开)。 - ndori
1
很棒的解释! - Edson Horacio Junior
7
我在使用EditText和RecyclerView时遇到了相同的异常。有没有人在RecyclerView上也遇到过这个问题? - Vivek Pratap Singh
我还没有使用过RecyclerView和EditText一起,但我可以看到在RecyclerView中也有setRecyclerListener,你尝试过这种解决方案吗?你能否确认它是否有效? - ndori
显示剩余2条评论

32

很抱歉告诉你,我发现之前的回答并不是解决这个问题的最佳方式。

所以我尝试了这个方法:
在您的Activity中添加一个ScrollListener,在listView开始滚动时清除当前焦点。

protected class MyScrollListener implements OnScrollListener {

        @Override
        public void onScroll(AbsListView view, int firstVisibleItem,
                int visibleItemCount, int totalItemCount) {
            // do nothing 
        }

        @Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {
            if (SCROLL_STATE_TOUCH_SCROLL == scrollState) {
                View currentFocus = getCurrentFocus();
                if (currentFocus != null) {
                    currentFocus.clearFocus();
                }
            }
        }

    }

1
这对我很有用。在我的情况下,我有一个ExpandableListView,其中包含一个EditText作为标题,在活动的清单中使用android:windowSoftInputMode="adjustPan"。当IME键盘弹出时向下滚动ExpandableListView会可靠地导致设备崩溃。感谢您解决了这个问题。 - Dororo
在我的情况下,当我从列表视图中删除一行时,如果焦点在该行的编辑文本内部,问题就会发生。这个答案让我在我的remove点击的onClick监听器中添加了parent.clearChildFocus(parent.findFocus())。我将其放置在调用该位置的项目之前的try-catch中。我的onClick监听器位于适配器的getView中,其中parentViewGroup - Mira_Cole
我和 @Mira_Cole 一样遇到了这个问题。你能再详细解释一下吗? - jvargas

24

试一下这个

 @Override
public View getView(int position, View convertView, ViewGroup parent) {
    //abandon current focus
    View currentFocus = ((Activity)mContext).getCurrentFocus();
    if (currentFocus != null) {
        currentFocus.clearFocus();
    }

    // other code
}

编辑:

另请参阅:更好的解决方案


4
试着添加一些关于这段代码的说明,以及它如何解决原始问题的信息。 - Jake McGraw
1
我不太确定这个解决方案是否适用于这种问题。在我看来,这个问题是由于listView和IME键盘之间的冲突引起的。当你把一个EditText放在ListView中并尝试编辑它时,IME键盘可能会在EditText上显示一个提示视图,当你滑动ListView直到聚焦的EditText消失时,应用程序会崩溃。因此,我认为提示视图与ListView存在某种冲突。 - Bruce

11

我使用Bruce的答案并进行了微小的调整。

在我的活动中,我需要使用adjustResize而不是adjustpan,但是当我尝试使用它时,错误再次发生。
我将ScrollView替换为<android.support.v4.widget.NestedScrollView,现在它可以正常工作。希望这能帮助到别人!


太棒了!这个安静而“埋藏”的回复帮助了我! - Krishnan V S
太棒了。非常感谢。 - Renat Kaitmazov

11

说实话,我已经放弃了在这个Activity中使用ListView的方法。除了随机崩溃之外,几乎不可能正确设置焦点行为而不设置windowSoftInputMode="adjustPan",这会引发许多其他问题。相反,我选择了“简单”的ScrollView,它一直运作良好。


2
我可以在我的应用程序中使用ListView和ScrollView重现此问题,如果我快速滚动EditText并在单击焦点后将其隐藏,则ListView会崩溃,而ScrollView会自行滚动。 - Niko
1
请问您能帮忙解决这个问题吗?我无法重现这个问题,但是在生产环境中经常出现这个堆栈跟踪错误。我有一个ListView用于SearchFilter,其中包含基于服务器响应动态填充(可聚焦)的元素,例如EditText、Spinner等。我需要摆脱我的ListView并将它们托管在一些非适配器视图中吗? - kiruwka

9
我有一个简单但不是很好的解决方案。只需扩展NestedScrollView并覆盖onSizeChanged方法,在其中添加try catch块。
public class FixFocusErrorNestedScrollView extends NestedScrollView {

    public FixFocusErrorNestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        try {
            super.onSizeChanged(w, h, oldw, oldh);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在我的情况下,我有两层视图,顶层是listView,底部是NestedScrollView。当我切换层时,错误就发生了。ListeView项(按钮)获取了焦点。
所以我无法让按钮失去焦点。那么最好的解决方案就是扩展NestedScrollView。

当清除焦点和其他方法无效时,这个解决方案是有效的。 - SANAT

6

我曾经面临同样的问题,并找到了这个解决方案 - 在ExpandableListViewOnGroupCollapseListener / OnGroupExpandListenerOnScrollListener中,我清除了焦点并隐藏了强制键盘。还要不要忘记在你的活动manifest中设置windowSoftInputMode="adjustPan"

    expListView.setOnGroupCollapseListener(new OnGroupCollapseListener() {

        @Override
        public void onGroupCollapse(int groupPosition) {
            InputMethodManager inputManager = (InputMethodManager) getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE);
            if (getWindow().getCurrentFocus() != null) {
                inputManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
                getCurrentFocus().clearFocus();
            }
        }
    });

    expListView.setOnGroupExpandListener(new OnGroupExpandListener() {

        @Override
        public void onGroupExpand(int groupPosition) {
            InputMethodManager inputManager = (InputMethodManager) getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE);
            if (getWindow().getCurrentFocus() != null) {
                inputManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
                getCurrentFocus().clearFocus();
            }
        }
    });

    expListView.setOnScrollListener(new OnScrollListener() {

        @Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {
            InputMethodManager inputManager = (InputMethodManager) getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE);
            if (getCurrentFocus() != null) {
                inputManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
                getCurrentFocus().clearFocus();
            }
        }

        @Override
        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {}

    });

我不确定是否需要OnGroupExpandListener,它可能是无用的。


4

我也遇到了这个问题,validcat的解决方法对我很有效,但我还需要调用getWindow().getCurrentFocus().clearFocus()


我在我的Fragment中使用了View Binding。它有一个Recyclerview,我正在向现有列表提交更新后的列表,但应用程序崩溃并显示错误“参数必须是此视图的后代”,这发生在我输入EditText值之后。错误是由于即使键盘隐藏后,当前编辑文本仍然具有焦点。上述解决方案的作用是告诉recyclerview不再需要焦点。我使用binding.getRoot().clearFocus()完成了这个操作。 - Deepak J

4

当我在Recyclerview中使用EditText时,我遇到了相同的问题。经过长时间的挣扎和尝试不同的选项,我发现在删除行时打开键盘会产生这个问题。我通过强制关闭我的键盘并用notifyDataSetChanged()替换notifyItemRemoved(position)来解决这个问题。


这个回答帮助我知道了问题出自哪里。然而,提出的建议没有解决问题。这个回答将我引导到了解决方案,也就是在删除行之前清除焦点。对于帮助我解决这个问题的协助,给予+1。感谢! - Mira_Cole
你是如何强制键盘的?我在这里遇到了完全相同的问题。 - jvargas

4

如果您的可展开列表视图中有编辑文本,则需要在 Expandable List View 中更改子项之前的焦点能力。

expandableListView.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);

对我来说,在RecyclerView上也起作用。 - Klaasel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接