Freewind
Freewind

Reputation: 198238

How to define a custom widget using both XML and java code

I want to define a custom widget, which contains some other controls and has own logic. I want to use XML file to define the UI, and the java code to define the logic. But I don't know how to do it.

I followed this article Building mix-up custom Android component/widget using Java class and XML layout, but don't a working one. Following is my code.

XML:

<com.example.MyView xmlns:android="http://schemas.android.com/apk/res/android"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content">

    <Button android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="custom button 1"/>

    <Button android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="custom button 2"/>

</com.example.MyView>

Java code:

public class MyView extends LinearLayout {

    public MyView(Context context) {
        super(context);
    }

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

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

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        LayoutInflater.from(getContext()).inflate(R.layout.test_view, this);  // **line 32**
    }
}

And in the main layout, I invoke it like this:

<com.example.MyView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
</com.example.MyView>

Unfortunately, it throws an exception when running:

05-26 14:11:25.199: ERROR/AndroidRuntime(15799): FATAL EXCEPTION: main
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example/com.example.MyActivity}: android.view.InflateException: Binary XML file line #7: Error inflating class <unknown>
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1651)
    at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1667)
    at android.app.ActivityThread.access$1500(ActivityThread.java:117)
    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:935)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loop(Looper.java:130)
    at android.app.ActivityThread.main(ActivityThread.java:3687)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:507)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:867)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:625)
    at dalvik.system.NativeStart.main(Native Method)
    Caused by: android.view.InflateException: Binary XML file line #7: Error inflating class <unknown>
    at android.view.LayoutInflater.createView(LayoutInflater.java:518)
    at com.android.internal.policy.impl.PhoneLayoutInflater.onCreateView(PhoneLayoutInflater.java:56)
    at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:568)
    at android.view.LayoutInflater.rInflate(LayoutInflater.java:623)
    at android.view.LayoutInflater.inflate(LayoutInflater.java:408)
    at android.view.LayoutInflater.inflate(LayoutInflater.java:320)
    at android.view.LayoutInflater.inflate(LayoutInflater.java:276)
    at com.example.MyView.onFinishInflate(MyView.java:32)
    at android.view.LayoutInflater.rInflate(LayoutInflater.java:631)
    at android.view.LayoutInflater.inflate(LayoutInflater.java:408)
    at android.view.LayoutInflater.inflate(LayoutInflater.java:320)
    at android.view.LayoutInflater.inflate(LayoutInflater.java:276)
    at com.example.MyView.onFinishInflate(MyView.java:32)
    at android.view.LayoutInflater.rInflate(LayoutInflater.java:631)
    at android.view.LayoutInflater.inflate(LayoutInflater.java:408)
    at android.view.LayoutInflater.inflate(LayoutInflater.java:320)
    at android.view.LayoutInflater.inflate(LayoutInflater.java:276)
    at com.example.MyView.onFinishInflate(MyView.java:32)
    at android.view.LayoutInflater.rInflate(LayoutInflater.java:631)
    at android.view.LayoutInflater.inflate(LayoutInflater.java:408)
    at android.view.LayoutInflater.inflate(LayoutInflater.java:320)
    at android.view.LayoutInflater.inflate(LayoutInflater.java:276)
    at com.example.MyView.onFinishInflate(MyView.java:32)
    at android.view.LayoutInflater.rInflate(LayoutInflater.java:631)
    at android.view.LayoutInflater.inflate(LayoutInflater.java:408)
    at android.view.LayoutInflater.inflate(LayoutInflater.java:320)
    at android.view.LayoutInflater.inflate(LayoutInflater.java:276)
    ....

Upvotes: 4

Views: 2361

Answers (1)

user
user

Reputation: 87064

You can't declare the custom view like you did. If you use your custom view in the main layout the onFinishInflate method will be called to construct the custom view and in that method the layout above will be called. The problem is that you already have a custom view reference in that layout(which will be inflated again when the LayoutInflater encounters the custom view tag) so you'll get in a circular inflating situation.

You probably want something like this:

<merge xmlns:android="http://schemas.android.com/apk/res/android>

    <Button android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="custom button 1"/>

    <Button android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="custom button 2"/>

</merge>

so when your custom view will be inflated from a layout file it will append the content of the xml layout above. The onFinishInflate method will remain the same:

@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    LayoutInflater.from(getContext()).inflate(R.layout.test_view, this);  // **line 32**
}

Edit: It appears that inflating a content layout in the onFinishInflate method triggers the method again(at least on older versions). You could either modify the layout to use a LinearLayout like this:

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

    <Button android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="custom button 1"/>

    <Button android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="custom button 2"/>

</LinearLayout>

and then in the onFinishInflate method:

@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    View v = LayoutInflater.from(getContext()).inflate(R.layout.aaaaaaaaaaa, this, false); 
    addView(v);
}

This will introduce another useless element in the layout hierarchy so you might simply inflate the layout with merge tag in the constructors of the custom view:

public MyView(Context context) {
    super(context);
    LayoutInflater.from(getContext()).inflate(R.layout.test_view, this);
}

public MyView(Context context, AttributeSet attrs) {
    super(context, attrs);
    LayoutInflater.from(getContext()).inflate(R.layout.test_view, this);
}

public MyView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    LayoutInflater.from(getContext()).inflate(R.layout.test_view, this);
}

Upvotes: 4

Related Questions