Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trying to understand view hierarchy

I'm writing an app with the sole purpose of trying to understand how the view hierarchy in Android works. I am having some really harsh problems in it right now. I'll try to be concise in my explanation here.

Setup:

Currently I have three Views. 2 are ViewGroups and 1 is just a View. Let's say they're in this order:

    TestA extends ViewGroup
    TestB extends ViewGroup
    TestC extends View
    TestA->TestB->TestC
    Where TestC is in TestB and TestB is in TestA.

In my Activity I simply display the views like so:

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

Problems:

  1. The onDraw(Canvas canvas) method of TestA is never called. I've seen a couple solutions to this saying that my view doesn't have any dimensions (height/width = 0), however, this is not the case. When I override onLayout(), I get the dimensions of my layout and they are correct. Also, getHeight()/Width() are exactly as they should be. I can also override dispatchDraw() and get my base views to draw themselves.

  2. I want to animate an object in TestB. Traditionally, I would override the onDraw() method on call invalidate() on itself until the object finish the animation it was supposed to do. However, in TestB, when I call invalidate() the view never gets redrawn. I'm under the impression that it's the job of my parent view to call the onDraw() method again, but my parent view does not call the dispatchDraw() again.

I guess my questions are, why would my onDraw() method of my parent view never get called to begin with? What methods in my parent view are supposed to be called when one of it's children invalidate itself? Is the parent the one responsible for ensure it's children get drawn or does Android take care of that? If Android responds to invalidate(), why does my TestB never get drawn again?

like image 667
DeeV Avatar asked Sep 15 '11 18:09

DeeV


1 Answers

Ok, after some research and a lot of trying, I've found out I was doing three things wrong in regards to problem #2. A lot of it was simple answers but not very obvious.

  • You need to override onMeasure() for every view. Within onMeasure(), you need to call measure() for every child contained in the ViewGroup passing in the MeasureSpec that the child needs.

  • You need to call addView() for every child you want to include. Originally, I was simply created a view object and using it directly. This allowed me to draw it once, but the view was not include in the view tree, thus it when I called invalidate() it wasn't invalidating the view tree and not redrawing. For example:

In 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);
 }

This will draw the child view once. However, if that view needs updating for animations or whatever, that was it. I put addView(childView) in the constructor of TestA to add it to the view tree. Final code is as such:

 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);
 }

Alternatively, I could override dispatchDraw(Canvas canvas) like so if I had many more children to draw, but I need some custom element between each drawing like grid lines or something.

@Override
protectd void dispatchDraw(Canvas canvas){
   int childCount = getChildCount();
   for(int i = 0; i < size; i++)
   {
       drawCustomElement();
       getChildAt(i).draw(canvas);
   }
}
  • You must override onLayout() (it's abstract in ViewGroup anyway, so it's required anyway). Within the this method, you must call layout for every child. Even after doing the first two things, my views wouldn't invalidate. As soon as I did this, everything worked just perfectly.

UPDATE: Problem #1 has been solved. Another extremely simply but not-so-obvious solution.

When I create an instance of TestA, I have to call setWillNotDraw(false) or else Android will not draw it for optimization reasons. So the full setup is:

TestA myView = new TestA(context);
myView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT));
myView.setWillNotDraw(false);
setContentView(myView);
like image 72
DeeV Avatar answered Oct 11 '22 04:10

DeeV