尝试理解视图层次结构

6
我正在编写一个应用程序,目的是为了尝试了解Android中视图层次结构的工作原理。我现在遇到了一些非常严重的问题。我会在这里简明扼要地解释一下。
设置:
目前我有三个视图。其中2个是ViewGroups,1个只是一个View。假设它们按照以下顺序排列:
    TestA extends ViewGroup
    TestB extends ViewGroup
    TestC extends View
    TestA->TestB->TestC
    Where TestC is in TestB and TestB is in TestA.

在我的Activity中,我只需像这样显示视图:
TestA myView = new TestA(context);
myView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT));
setContentView(myView);

问题:

  1. TestA的onDraw(Canvas canvas)方法从未被调用。我看到有一些解决方案说我的视图没有任何尺寸(高度/宽度=0),但这不是事实。当我覆盖onLayout()时,我获取了我的布局的尺寸,它们是正确的。而且,getHeight()/Width() 正好应该如此。我还可以重写dispatchDraw()并让我的基本视图绘制自己。

  2. 我想在TestB中动画一个对象。传统上,我会覆盖onDraw()方法并在其自身上调用invalidate()直到对象完成它应该执行的动画。然而,在TestB中,当我调用invalidate()时,该视图从未重新绘制。我认为它是我的父视图的工作再次调用onDraw()方法,但我的父视图不会再次调用dispatchDraw()

我的问题是,为什么我的父视图的onDraw()方法从未被调用呢?当其子项使自身无效时,父视图中应该调用哪些方法?父视图负责确保其子项被绘制还是由Android负责?如果Android响应invalidate(),为什么TestB再次没有被绘制?

2个回答

5

好的,在一些研究和尝试之后,我发现在解决问题2时做错了三件事。很多是简单的答案,但不太明显。

  • 你需要为每个视图重写 onMeasure()。在 onMeasure() 中,你需要为 ViewGroup 中包含的每个子元素调用 measure() 并传入子元素所需的 MeasureSpec。

  • 你需要对想要包括的每个子元素调用 addView()。最初,我只是创建一个视图对象并直接使用它。这使我可以绘制它一次,但该视图并未包含在视图树中,因此当我调用 invalidate() 时,它没有使视图树变为无效并且没有重绘。例如:

在 testA 中:

 TestB childView;
 TestA(Context context){
   ****** setup code *******
   LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
   childView = new TestB(context);
   childView.setLayoutParams(params);
 }

 @Override
 protected void dispatchDraw(Canvas canvas){
   childView.draw(canvas);
 }

这将绘制子视图一次。然而,如果该视图需要更新以进行动画或其他操作,那么这就是它了。我将 addView(childView) 放置在 TestA 的构造函数中以将其添加到视图树中。最终代码如下:

 TestB childView;
 TestA(Context context){
   ****** setup code *******
   LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
   childView = new TestB(context);
   childView.setLayoutParams(params);
   addView(childView);
 }

 @Override
 protected void dispatchDraw(Canvas canvas){
   childView.draw(canvas);
 }

如果我需要绘制更多的子元素,并且需要在每个元素之间添加一些自定义元素,例如网格线等,则可以通过重写dispatchDraw(Canvas canvas)方法来实现。

这样做的另一种方法是使用RecyclerView或ListView等适配器视图,它们可以处理大量的数据集并提供更好的性能。

@Override
protectd void dispatchDraw(Canvas canvas){
   int childCount = getChildCount();
   for(int i = 0; i < size; i++)
   {
       drawCustomElement();
       getChildAt(i).draw(canvas);
   }
}
  • 你必须重写 onLayout() 方法( ViewGroup 中该方法本来就是抽象方法,因此无论如何都需要)。在此方法中,您必须为每个子元素调用 layout 方法。即使完成了前两步,我的视图仍然无法失效。当我执行这个操作时,一切都完美地运行。

更新: 问题#1已解决。另一个非常简单但不那么显而易见的解决方案。

当我创建 TestA 的实例时,我必须调用 setWillNotDraw(false) ,否则 Android 将不会绘制它以进行优化。 因此,完整设置为:

TestA myView = new TestA(context);
myView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT));
myView.setWillNotDraw(false);
setContentView(myView);

你能在你的博客或其他地方分享这个Viewgroup的演示项目吗?这将非常有帮助。谢谢,伙计。 - user1169079
即使我们在XML中指定了子视图,我是否需要添加childView? - user1169079
很抱歉,我没有博客或任何可以上传的东西。我会尽力帮助你。回答你的第一个问题,不是的。当你膨胀XML时,子项包含在ViewGroup中。最早可以检索它的时间是onFinishInflate()。之后你有选择。要么通过getChildAt(int index)方法获取你的子项,要么使用findViewById(int id)。索引将是子项在xml中定位的顺序。但我不知道子项内部的索引如何工作。 - DeeV
@ Deev:你能邮件发送那个演示版吗?这样我和你解决我的疑问会更容易一些。我知道我在请求很大的帮助,但这将是非常棒的帮助!谢谢朋友。 - user1169079
抱歉,我的演示是在九月份编写的,而且已经发展成为完整的项目。将其精简至相关部分需要相当长的时间。 - DeeV
好的兄弟,那我猜我会在这里解决我的疑惑,同时继续进行我的演示... - user1169079

2
这不是一个直接的回答,但这里有一个很棒的教程,关于如何绘制自定义视图和动画视图。代码非常简单清晰易懂。

希望能对您有所帮助!


谢谢。这并没有解决我的问题,但它确实有助于引导解决方案的出现。 - DeeV

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