javaandroidxmlandroid-layoutlayout-inflater

Android: How to avoid doubling ViewGroups with LayoutInflater


I found an solution

A big thank you to cyroxis for the constructive criticism and suggestions that led me into the correct direction. At the beginning I thought it isn't the accepted answer, but after some thinking I am sure it is.

Nevertheless I want to provide some additionale informations that matches my case more precisely.

You can find the solution text at the bottom of this post

Original question

my first question here. :-) Let's start:

I wonder if there is a simple way (through the API, maybe?!) to avoid a unnecessary deep view hierarchy when using LayoutInflater inside a custom class that extends a ViewGroup e.g. LinearLayout.

For example

This is an Android xml layout file "my_xml_layout.xml"

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="New Text"
        android:id="@+id/textView" />

    <ImageView
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:id="@+id/imageView"
        android:layout_gravity="center_horizontal" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="New Button"
        android:id="@+id/button"
        android:layout_gravity="center_horizontal" />
</LinearLayout>

And now I want to encapsulate the layout into a custom view and control the behavior and interaction of all its components (the TextView, the Button and the ImageView). So I create a class which extends e.g LinearLayout like this one

public class MyCustomView extends LinearLayout {

    public MyCustomView(Context context) {
        super(context);
        inflateLayout(context);
    }

    public MyCustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        inflateLayout(context);
    }

    public MyCustomView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        inflateLayout(context);
    }

    private void inflateLayout(Context context){
        LayoutInflater.from(context).inflate(R.layout.my_xml_layout, this, true);
    }

...

}

This works fine and now I have all subcomponents accessible inside MyCustomView on the one Hand and can simple add MyCustomView as a View to e.g. an Activity or a Fragment layout.

BUT: Now I have two LinearLayout nodes in my layout hierarchy. One that comes from the layout xml file and a second that comes from the MyCustomView parent ViewGroup class.

So how can I, say, delete one of the redundant ViewGroups (but leave the attribute values of the xml file's ViewGroup intact).

The previous way I always achieved this was to type an extensive number of code that inflates the xml layout into a local variable and extracts every subcomponent from it just to attach it to the java ViewGroup again.

Or am I doing things totally wrong? Is there another way to program a complex view's behavior but designing it within the xml editor?

Your thoughts are very appreciated.

Thank you!

Martin

Update #1

The suggested solution from cyroxis is sadly not the solution. Let me explain my observation (sorry for my bad English and please correct me if I make mistakes - this helps me improve my English skills :-) ):

Using the full qualifier name makes (from my experience) only sense when you create a fully customized view in Java code with all the onDraw and onMeasure implementation. Then to include your custom view into a layout hierarchy in xml you have to go this way and use the full qualifier name in the layout file. Having also defined additional attributes within a styleable xml file you can simple configure the custom view without changing a single line in Java code.

Nevertheless I've tried to use the full qualifier just to tests if I missed something or if my knowledge lags behind the implemented functionality of the Android APIs. But no, I couldn't find a configuration where the usage of the full qualifier name helps me out. Even worse, Android Studio is very sensitive to qualifier name changes at this stage of development and crashes very often when the preview renderer is enabled and there are syntax errors or some weird dependency loops. ;-)

To be clear: Encapsulating the whole layout structure with the full qualifier name works in general (in terms of not crashing the IDE) but does not "simply" merges it's child views without another call of the LayoutInflater - what cyroxis wants to avoid.

Speaking of "merging": The merge tag is the closest thing I've found that behaves the way I want it to. Encapsulating my layout with merge and inflating it within my custom view class does spare one level of layout hierarchy but at the cost of an intuitive xml layout workflow - because the layout editor has no ViewGroup informations anymore to set the merge's child views position (but it works at runtime).

To make my position clear: I want to be able to use Android's standard views to compose a more complex view and then orchestrate all the sub-components in Java code to create a fancy new view that I can reuse in other xml layout files, Activities and Fragments. Programming the logic of the composed view within an Activity or a Fragment is no a good choice in my opinion, because it makes the code redundant for any further Activities or Fragments. Creating static methods, or completely loose classes that requires an instance to one, some or all layout components of my complex view hits the idea of easily maintainable code and encapsulated modules.

I'll provide here another workaround I've been using for a longer time now in a couple of hours. It makes me wonder if I'm one of only few people who are really bothered by this issue?!

Update #2

I did some further investigations and found something interesting - and maybe it has a lot in common with cyroxis' solution:

  1. I added the full qualifier name to my composed xml view's topmost ViewGroup, pointing to my Java Class (like cyroxis said)
  2. I added the xml layout to my Fragment's layout xml file by using the "include" directive.

The result is, that the renderer (and my device) is showing the composed view correctly. So I assume that it takes the ViewGroup informations (that it is actually a LinearLayout) from my Java class, because this is the only place where this is noted.

Now: If this is the case, how can I access the different sub-components?

this.getChildAt(...) 

does not work for me.

The second observation:

If I add the xml layout not via the include-directive but directly with

<net.martinmajewski.challengeme.views.panels.HintActivationPanel/>

I get a invisible node without any dimensions. So the layout attributes are not available at the Java Class at this point.

My Java Class does nothing special so far. It just implements the three constructors, and (like mentioned above) tries to access the sub-components.

Solution

Prerequisite:

Now you can (re)use your view in different layouts by including it with the directive. By layouts I literally mean layout xml files. I want to be able to design my UI in a WYSIWYG manner for quick prototyping and reviewing. So I want to enable the preview renderer to use a xml file without having to write a single logic line at this point in time. And there comes the problem. When you place your composed view inside an Activity's / fragment's layout it gets completely and "automatically" inflated by the setContentView or within the onCreateView Method. So at this point it is already there and it has called one of the three available public constructors of your Java class. I realized this fact thanks to cyroxis. Cyroxis suggested to inflate the view manually inside an Activity, but that would lead to a redundant inflations. I think cyroxis just forgot to take into account that the custom view is already a part of the parent layout.

So now that the layout exists and is correctly hooked to a parent without doubling any node everything is perfect, right?

No yet.

Your Java class has no references to its sub-components. You can do this like cyroxis, but if you have implemented some interaction of the sub components without any outside influence intended, you have to initialize your sub-views at the beginning (saving the references). To do so you have to provide a public initialization method that you call after the inflation process. At this point of time your Java object can finally find the ids of the sub-views and call the findViewById method without getting a null pointer back. This is also true for the getChildAt method.

Maybe this will help some of you, too.

Bye Martin


Solution

  • You should not be inflating the layout in your extended class, instead you should specify the fully qualified class name in your xml file.

    <?xml version="1.0" encoding="utf-8"?>
    <com.package.MyCustomView xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical" android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="New Text"
            android:id="@+id/textView" />
    
        <ImageView
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:id="@+id/imageView"
            android:layout_gravity="center_horizontal" />
    
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="New Button"
            android:id="@+id/button"
            android:layout_gravity="center_horizontal" />
    </LinearLayout>
    

    Here is an article with more information

    EDIT

    Normal android XML files can be considered shorthand for programmatically creating views. Your XML above instructs android to create view of type LinearLayout with the given params then to adds a child TextView with the given params and so forth.

    This layout can be used two way.

    1. Include the layout inside another layout
    2. Inflate the layout (i.e. use the layout inflator) which will return an object of type LinearLayout

    You can create a class as follows

    public class MyCustomView extends LinearLayout {
    
      // Note: I am not inflating the layout here
    
      public void foo() {
          // Do some foo
      }
    
      public void setTitle(String text) {
          findViewById(R.id.textView).setText(text);
      }
      ....
    
    }
    

    Then using the layout above you can use the following code (say activity/fragment)

     MyCustomView view = (MyCustomView) LayoutInflater.from(context).inflate(R.layout.my_xml_layout, parent, true);
     view.foo(); // Do some foo
     view.setTitle("Hello Word!!!");
    

    This allows you to create your own custom "widgets" that behave much like the built in system widgets (TextView, ListView, etc). It will only have one view group (MyCustomView) and you can add any extra behavior you need.