Wednesday, September 22, 2010

Android view containers PT. 1

Ok, so what do you do if you need to build something like the app drawer from scratch, having freedom to add Views to it at will?

To build a layout that allows for the display and management of child views itself, you need to inherit from ViewGroup, or a subclass of view group (technically you COULD build out your own implementation inheriting from a straight view, but we wont get into that now).

As a sort of experiment in software development, I'll be building a ViewGroup subclass that will effectively work like a LinearLayout, with the exception that the content will actually wrap. Its important to note that I intent to start with very simple minimal code, and build it into something I'd consider ready for primetime.

First things first we need a new class. I personally created a new WrappingLinearLayout class in my com.mycompany.UI package. This class should obviously extend "ViewGroup", and implement the abstract methods etc.

Drawing in android works in two major phases.. measuring (helping plan ahead as far as how to draw controls accounting or not for things like margins, and weights), and laying out.

Measuring


The concept is more or less loop through children views, and decide measurements for individual view items to be drawn. Depending on the complexity of your layout, you may just do nothing more than measure, or you might go to the other opposite end of the spectrum where you account for orientation, dpi independent rendering, LayoutParams like fill_parent and wrap_contents, and weighting. To start with the simplest possible solution.. we'll provide bare minimum implementation of Overridden onMeasure method.

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 //get count of children view elements
 final int childCount = this.getChildCount();
  
 //iterate through children and measure each accordingly.
 for (int i = 0; i < childCount; i++) {
     View child = getChildAt(i);
      
      //call measure on each child, so that we can call getMeasuredHeight() later.
      //UNSPECIFIED basically tells the measure to use whatever was originally supplied for size and layoutparam information
      child.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
     
  }
  
  //now we need to measure the actual controls dimensions... we dont have to call base here, but for now
  //let android do its thing.
  super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 }

Pretty simple, and commented if there is any confusion.

Now that we've got everything measured out in there most basic forms, lets get to the layout.

Layout


The gist of this step is to handle the actual layout... (in the end, the measure step will have determined the size of all the views including margins weights etc, and now its time to perform layout of those children)

So currently my goal is to simulate the most basic of functionality of LinearLayout, which is that I can take the childviews and tile em (note: at this point, views will still eventually overflow off this custom view). So with that said, we need to effectively iterate through the child views drawing the first, and subsequently offsetting the next by offset+width of the first. Lets override onLayout to achieve this basic effect.

@Override
 protected void onLayout(boolean changed, int l, int t, int r, int b) {
  //get count for iteration through child views
  int childCount = this.getChildCount();
  
  //initialize the x offset of the view. (currently only handling horizontal layout, and doesnt wrap yet!)
  int lOffset = 0;
  
  //iterate through the child views laying them out as specified in the initial instances.
  for (int i = 0; i < childCount; i++) {
      
   View child = getChildAt(i);
   
   //if this view is not "Visible" or "Invisible" just skip
   if (child == null || child.getVisibility() == GONE)
    continue;
   
   //i'm giving children a 5px margin top and left here, which should be done by theming.
   //note: for the left parameter its the lOffset and that the right parameter needs to include
   //      offset+width. Also, we call getMeasured(Width | Height) because getHeight wont work here.
      child.layout(lOffset+5,5,lOffset+child.getMeasuredWidth(), child.getMeasuredHeight());
      
      //finally lets make sure offset gets incremented the width of just-processed view.
      lOffset+=child.getMeasuredWidth();
  }
  
 }


Ok, so as far as basic implementation goes, we've got the bare minimums there. We didn't build out the constructor for use with declarative instantiation, so we'll slap together a quick bunch of code to instantiate the WrappingLinearLayout, and add a couple items to it.

//instantiate control
        WrappingLinearLayout wrl = new WrappingLinearLayout(this.getApplicationContext());
        
        //set bg color of control to white for better troubleshooting
        wrl.setBackgroundColor(Color.WHITE);
        
        //create a button for testing
        Button button = new Button(this.getApplicationContext());
        button.setText("DoStuff()");
        button.setLayoutParams(new          LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT));
        button.setWidth(100);
        button.setHeight(50);
        
        //gotta make sure it actually flows... need a second control
        Button button2 = new Button(this.getApplicationContext());
        button2.setText("DoStuff2()");
        button2.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT));
        button2.setWidth(100);
        button2.setHeight(50);
        
        //add two buttons to WrappingLinearLayout
        wrl.addView(button);
        wrl.addView(button2);

        //finally set the content view to the custom layout view we created...
        setContentView(wrl);

Conclusion

So there you have it... a very simple and rudimentary example of the beginnings of a flow layout container. Lots of things missing that I hope to add in the near future with the rest of this series. This code should by all means NOT be used, for it has a lot of hardcoded values... but should be a little easier to explain HOW it all works.

Until next time!

2 comments:

Natali said...

It's great! Very big thank to you!

lilpiggie said...

Thank you! This article was very helpful to me. :)