安卓 - 初始布局后的响应

4

这个问题以各种形式被问过很多次,但我没有找到一个明确的答案。

我需要在容器(contentView)的初始布局后获取子Views的尺寸和位置。在这种情况下所需的尺寸是不可预测的,可能有多行文本、最小高度以适应装饰等。我不想在每次布局(onLayout)通过做这件事。我需要测量的是一个嵌套非常深的子项,因此在每个容器中覆盖onLayout/onMeasure似乎是一个不好的选择。我不会做任何会导致循环(触发事件期间的事件)的事情。

Romain Guy曾经暗示,我们可以在View的构造函数中.post一个Runnable(我认为它会在UI线程上执行)。这似乎在大多数设备上都有效(在我手头的4个设备中的3个),但在Nexus S上失败了。看起来Nexus S直到对每个子项进行一次通行证之前都没有所有内容的尺寸,并且当调用postRunnablerun方法时,没有正确的尺寸。我试着在onLayout中计算布局通行证数(使用getChildCount进行比较),这在4个设备中的其中3个上又起作用了(在Droid Razr上失败了,在这里似乎只进行一次通行证-并获得所有测量-而不管子项的数量)。我尝试了以上各种变化(正常发布;发布到Handler;将getContext()强制转换为Activity并调用runOnUiThread...所有结果都相同)。

最后我使用了一个可怕且可耻的不好的方法,通过检查目标组的高度与其父级的高度是否相同来判断它是否已被布局。显然,这不是最好的方法,但似乎是在各种设备和实现之间可靠工作的唯一方法。我知道没有内置事件或回调可用,但有更好的方法吗?

谢谢您!

/编辑 对我来说,关键问题在于Nexus S没有等待布局完成后再.posting()一个Runnable,这是我测试过的所有其他设备的情况,也是Romain Guy和其他人建议的情况。我没有找到一个好的解决方法。有吗?

2个回答

9

我成功地使用了OnGlobalLayoutListener。

在我的Nexus S上效果很好。

final LinearLayout marker = (LinearLayout) findViewById(R.id.distance_marker);
OnGlobalLayoutListener listener = new OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        //some code using marker.getHeight(), etc.
        markersContainer.getViewTreeObserver().removeGlobalOnLayoutListener(this);
    }
};
marker.getViewTreeObserver().addOnGlobalLayoutListener(listener);

我需要在所有布局完成后仅发生一次的事件。我相信OnGlobalLayoutListener每次更改时都会触发-这是正确的吗?如果是这样,是否有一种方法可以使用您发布的内容来确保所有“正常”布局都已完成?谢谢。 - momo
在onGlobalLayout的结尾处移除监听器,就像我在那里所做的那样,意味着它只会被执行一次。对于我来说,在一个非常复杂的视图中,所有正常的布局都已经完成了,但是我需要更多的阅读或更好地了解您的情况才能保证更多的内容。 - HannahMitt
似乎可以工作,谢谢。我会将这个问题保留几天,以防有更“标准”的解决方案,但如果没有,我会接受并奖励。现在+1。 - momo
1
OnGlobalLayoutListener,你最好的朋友和最老的技巧。另外,看一下https://dev59.com/dW025IYBdhLWcg3wvIu2#6661701,因为测量可能会更快,特别是当你打算修复某些东西并导致另一个无效化/布局时。 - Sherif elKhatib

3
我的经验表明,OnGlobalLayoutListener 并不总是等待所有子视图布局完成后才触发。因此,如果您正在寻找特定子视图(或其子视图)的尺寸,则在 onGlobalLayout() 方法中测试它们时,可能会发现它们反映了布局过程的某个中间状态,而不是将显示给用户的最终(静止)状态。
在测试中,我注意到通过使用 postDelayed() 函数并设置一秒延迟(显然这是一种竞态条件,但这只是为了测试),我得到了正确的值(对于一个子视图的子视图),但如果我简单地使用 post() 函数而没有延迟,我就得到了错误的值。
我的解决方法是避免使用 OnGlobalLayoutListener,并改用在我的顶级视图(包含我需要测量的子视图的子视图)中直接重写 dispatchDraw() 函数。默认的 dispatchDraw() 函数会绘制当前视图的所有子视图,因此,如果您在调用 super.dispatchDraw(canvas) 后在 dispatchDraw() 函数中发布执行测量的代码的调用,那么您可以确保在进行测量之前所有子视图都会被绘制。
下面是示例代码:
@Override
protected void dispatchDraw(Canvas canvas) {
  //Draw the child views
  super.dispatchDraw(canvas);

  //All child views should have been drawn now, so we should be able to take measurements
  // of the global layout here
  post(new Runnable() {
    @Override
    public void run() {
      testAndRespondToChildViewMeasurements();
    }
  });
}

当然,在进行这些测量之前会进行绘图,如果这不是问题的话,那么这种方法似乎非常可靠地提供了后代视图的最终(静止)测量结果。但是,如果已经绘图并且无法阻止它,那么就为时已晚。

一旦您获得所需的测量值,您将想要在testAndRespondToChildViewMeasurements()顶部设置一个标志,以便它返回而不再执行任何其他操作(或在dispatchDraw()覆盖中进行类似的测试),因为与OnGlobalLayoutListener不同,没有办法删除此覆盖。


1
太棒了 - 我很兴奋去尝试一下。谢谢。 - momo

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