12

I'm trying to create a custom Android control that contains a LinearLayout. You can think of it as an extended LinearLayout with fancy borders, a background, an image on the left...

I could do it all in XML (works great) but since I have dozens of occurences in my app it's getting hard to maintain. I thought it would be nicer to have something like this:

/* Main.xml */
<MyFancyLayout>
    <TextView />   /* what goes inside my control's linear layout */
</MyfancyLayout>

How would you approach this? I'd like to avoid re-writing the whole linear layout onMeasure / onLayout methods. This is what I have for the moment:

/* MyFancyLayout.xml */
<TableLayout>
    <ImageView />
    <LinearLayout id="container" />   /* where I want the real content to go */
</TableLayout>    

and

/* MyFancyLayout.java */
public class MyFancyLayout extends LinearLayout
{
    public MyFancyLayout(Context context) {
        super(context);
        View.inflate(context, R.layout.my_fancy_layout, this);
    }
}

How would you go about inserting the user-specified content (the TextView in main.xml) in the right place (id=container)?

Cheers!

Romain

----- edit -------

Still no luck on this, so I changed my design to use a simpler layout and decided to live with a bit of repeated XML. Still very interested in anyone knows how to do this though!

Romain
  • 2,318
  • 1
  • 23
  • 31

4 Answers4

11

This exact question bugged me for some time already but it's only now that I've solved it.

From a first glance, the problem lies in the fact that a declarative content (TextView in Your case) is instantiated sometime after ctor (where we're usually inflating our layouts), so it's too early have both declarative and template content at hand to push the former inside the latter.

I've found one such place where we can manipulate the both: it's a onFinishInflate() method. Here's how it goes in my case:

    @Override
    protected void onFinishInflate() {
        int index = getChildCount();
        // Collect children declared in XML.
        View[] children = new View[index];
        while(--index >= 0) {
            children[index] = getChildAt(index);
        }
        // Pressumably, wipe out existing content (still holding reference to it).
        this.detachAllViewsFromParent();
        // Inflate new "template".
        final View template = LayoutInflater.from(getContext())
            .inflate(R.layout.labeled_layout, this, true);
        // Obtain reference to a new container within "template".
        final ViewGroup vg = (ViewGroup)template.findViewById(R.id.layout);
        index = children.length;
        // Push declared children into new container.
        while(--index >= 0) {
            vg.addView(children[index]);
        }

        // They suggest to call it no matter what.
        super.onFinishInflate();
    }

A labeled_layout.xml referenced above is not unlike something like this:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation             ="vertical"
    android:layout_width            ="fill_parent"
    android:layout_height           ="wrap_content"
    android:layout_marginLeft       ="8dip"
    android:layout_marginTop        ="3dip"
    android:layout_marginBottom     ="3dip"
    android:layout_weight           ="1"
    android:duplicateParentState    ="true">

    <TextView android:id            ="@+id/label"
        android:layout_width        ="fill_parent"
        android:layout_height       ="wrap_content"
        android:singleLine          ="true"
        android:textAppearance      ="?android:attr/textAppearanceMedium"
        android:fadingEdge          ="horizontal"
        android:duplicateParentState="true" />

    <LinearLayout
        android:id                  ="@+id/layout"
        android:layout_width        ="fill_parent"
        android:layout_height       ="wrap_content"
        android:layout_marginLeft   ="8dip"
        android:layout_marginTop    ="3dip" 
        android:duplicateParentState="true" />
</LinearLayout>

Now (still omitting some details) elsewhere we might use it like this:

        <com.example.widget.LabeledLayout
            android:layout_width    ="fill_parent"
            android:layout_height   ="wrap_content">
            <!-- example content -->
        </com.example.widget.LabeledLayout> 
esteewhy
  • 1,300
  • 13
  • 23
7

This approach saves me a lot of code! :)

As esteewhy explains, just swap the xml-defined contents into where you want them internally in your own layout, in onFinishInflate(). Example:

I take the contents that I specify in the xml:

<se.jog.custom.ui.Badge ... >
    <ImageView ... />
    <TextView ... />
</se.jog.custom.ui.Badge>

... and move them to my internal LinearLayout called contents where I want them to be:

public class Badge extends LinearLayout {
    //...
    private LinearLayout badge;
    private LinearLayout contents;

    // This way children can be added from xml.
    @Override
    protected void onFinishInflate() {      
        View[] children = detachChildren(); // gets and removes children from parent
        //...
        badge = (LinearLayout) layoutInflater.inflate(R.layout.badge, this);
        contents = (LinearLayout) badge.findViewById(R.id.badge_contents);
        for (int i = 0; i < children.length; i++)
            addView(children[i]); //overridden, se below.
        //...
        super.onFinishInflate();
    }

    // This way children can be added from other code as well.
    @Override
    public void addView(View child) {
        contents.addView(child);
}

Combined with custom XML attributes things gets very maintainable.

JOG
  • 5,590
  • 7
  • 34
  • 54
1

You can create your MyFancyLayout class by extending LinearLayout. Add the three constructors which call a method ("initialize" in this case) to set up the rest of the Views:

public MyFancyLayout(Context context) {
    super(context);
    initialize();
}

public MyFancyLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
    initialize();
}

public MyFancyLayout(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    initialize();
}

Within initialize, you do anything you need to to add the extra views. You can get the LayoutInflater and inflate another layout:

final LayoutInflater inflator = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflator.inflate(R.layout.somecommonlayout, this);

Or you can create Views in code and add them:

        ImageView someImageView = new ImageView(getContext());
        someImageView.setImageDrawable(myDrawable);
        someImageView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
        addView(someImageView);

If you're going to use the Context a lot, you can store a reference to it in your constructors and use that rather than getContext() to save a little overhead.

Ian G. Clifton
  • 9,349
  • 2
  • 33
  • 34
  • Thanks! This should help a lot. So in this case, I'm guessing the children provided in main.xml are already attached. Did you ever have any luck putting them somewhere else? In my case I'd like to reattach them to one of the children created in code (the "container"). – Romain Jan 11 '11 at 03:41
  • That's correct; they're all attached. If you want to move them to another container, you'll loop through them (use getChildCount to see how many you have and getChildAt(index) to get each one) and add them to the other ViewGroup. After adding to the other view, you can do removeViewAt(index) to remove it from being a direct child of MyFancyLayout. – Ian G. Clifton Jan 11 '11 at 03:47
  • Actually, now that I think about it, the children might not be inflated until after that, so you might not be able to loop through them right away. I'm not sure on that. – Ian G. Clifton Jan 11 '11 at 04:01
  • I'll have to experiment with that tonight, thanks. I'll post the edited code if it works! – Romain Jan 11 '11 at 04:04
0

just use something like this:

<org.myprogram.MyFancyLayout>
 ...
</org.myprogram.MyFancyLayout>

Useful link - http://www.anddev.org/creating_custom_views_-_the_togglebutton-t310.html

Igor
  • 1,476
  • 10
  • 6
  • Thanks. Yes I need the full scope indeed, I was simplifying. But my problem is that all these examples don't use any children, for example . In my case I'd like to have contents inside, just like a normal layout. – Romain Jan 05 '11 at 14:33
  • hm..for example ... does not work? – Igor Jan 05 '11 at 14:34
  • I mean you can put parameters inside like in standard layout, and it should works fine – Igor Jan 05 '11 at 14:41
  • I edited the original post to include more detail. What you suggest works great, but I can't seem to combine the "real" content (provided by the user) and MyFancyLayout's content (the extra borders...) – Romain Jan 05 '11 at 21:33