Create a custom View by inflating a layout?
Asked Answered
Z

7

144

I am trying to create a custom View that would replace a certain layout that I use at multiple places, but I am struggling to do so.

Basically, I want to replace this:

<RelativeLayout
 android:id="@+id/dolphinLine"
 android:layout_width="fill_parent"
 android:layout_height="wrap_content"
    android:layout_centerInParent="true"
 android:background="@drawable/background_box_light_blue"
 android:padding="10dip"
 android:layout_margin="10dip">
  <TextView
   android:id="@+id/dolphinTitle"
   android:layout_width="200dip"
   android:layout_height="100dip"
   android:layout_alignParentLeft="true"
   android:layout_marginLeft="10dip"
   android:text="@string/my_title"
   android:textSize="30dip"
   android:textStyle="bold"
   android:textColor="#2E4C71"
   android:gravity="center"/>
  <Button
   android:id="@+id/dolphinMinusButton"
   android:layout_width="100dip"
   android:layout_height="100dip"
   android:layout_toRightOf="@+id/dolphinTitle"
   android:layout_marginLeft="30dip"
   android:text="@string/minus_button"
   android:textSize="70dip"
   android:textStyle="bold"
   android:gravity="center"
   android:layout_marginTop="1dip"
   android:background="@drawable/button_blue_square_selector"
   android:textColor="#FFFFFF"
   android:onClick="onClick"/>
  <TextView
   android:id="@+id/dolphinValue"
   android:layout_width="100dip"
   android:layout_height="100dip"
   android:layout_marginLeft="15dip"
   android:background="@android:drawable/editbox_background"
   android:layout_toRightOf="@+id/dolphinMinusButton"
   android:text="0"
   android:textColor="#2E4C71"
   android:textSize="50dip"
   android:gravity="center"
   android:textStyle="bold"
   android:inputType="none"/>
  <Button
   android:id="@+id/dolphinPlusButton"
   android:layout_width="100dip"
   android:layout_height="100dip"
   android:layout_toRightOf="@+id/dolphinValue"
   android:layout_marginLeft="15dip"
   android:text="@string/plus_button"
   android:textSize="70dip"
   android:textStyle="bold"
   android:gravity="center"
   android:layout_marginTop="1dip"
   android:background="@drawable/button_blue_square_selector"
   android:textColor="#FFFFFF"
   android:onClick="onClick"/>
</RelativeLayout>

By this:

<view class="com.example.MyQuantityBox"
    android:id="@+id/dolphinBox"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:myCustomAttribute="@string/my_title"/>

So, I do not want a custom layout, I want a custom View (it should not be possible for this view to have child).

The only thing that could change from one instance of a MyQuantityBox to another is the title. I would very much like to be able to specify this in the XML (as I do on the last XML line)

How can I do this? Should I put the RelativeLayout in a XML file in /res/layout and inflate it in my MyBoxQuantity class? If yes how do I do so?

Thanks!

Zalea answered 1/12, 2010 at 20:34 Comment(1)
See "Compound Controls" in Android, and this link: #1476871Colvin
S
28

Yes you can do this. RelativeLayout, LinearLayout, etc are Views so a custom layout is a custom view. Just something to consider because if you wanted to create a custom layout you could.

What you want to do is create a Compound Control. You'll create a subclass of RelativeLayout, add all our your components in code (TextView, etc), and in your constructor you can read the attributes passed in from the XML. You can then pass that attribute to your title TextView.

http://developer.android.com/guide/topics/ui/custom-components.html

Separatrix answered 1/12, 2010 at 20:48 Comment(0)
E
173

A bit old, but I thought sharing how I'd do it, based on chubbsondubs' answer: I use FrameLayout (see Documentation), since it is used to contain a single view, and inflate into it the view from the xml.

Code following:

public class MyView extends FrameLayout {
    public MyView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initView();
    }

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

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

    private void initView() {
        inflate(getContext(), R.layout.my_view_layout, this);
    }
}
Evaporate answered 1/7, 2013 at 20:24 Comment(5)
Since View class has static inflate() method there is no need for LayoutInflater.from()Salt
Isn't this just the solution of Johannes from here: #17837195 Still, this inflates another layout within. So it's not really the best soltion I'd guess.Chemist
it is, but Johannes solution is from 7.24.13, and mind was from 7.1.13... Also, my solution uses FrameLayout which is supposed to contain only one View (as written in the doc referenced in the solution). So actually it is meant to be used as a placeholder for a View. I don't know any solution which doesn't invovle using a placeholder for the inflated View.Evaporate
I don't get it. That method (inflate) returns a view, which is ignored. Seems like you need to add it to the current view.Philanthropic
@Jeffrey Blattman please check out View.inflate method, we use this one (specifying the root as this, 3rd parameter)Winger
H
132

Here is a simple demo to create customview (compoundview) by inflating from xml

attrs.xml

<resources>
    
    <declare-styleable name="CustomView">
        <attr format="string" name="text"/>
        <attr format="reference" name="image"/>
    </declare-styleable>
</resources>

CustomView.kt

class CustomView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
        ConstraintLayout(context, attrs, defStyleAttr) {

    init {
        init(attrs)
    }

    private fun init(attrs: AttributeSet?) {
        View.inflate(context, R.layout.custom_layout, this)

        val image_thumb = findViewById<ImageView>(R.id.image_thumb)
        val text_title = findViewById<TextView>(R.id.text_title)

        val ta = context.obtainStyledAttributes(attrs, R.styleable.CustomView)
        try {
            val text = ta.getString(R.styleable.CustomView_text)
            val drawableId = ta.getResourceId(R.styleable.CustomView_image, 0)
            if (drawableId != 0) {
                val drawable = AppCompatResources.getDrawable(context, drawableId)
                image_thumb.setImageDrawable(drawable)
            }
            text_title.text = text
        } finally {
            ta.recycle()
        }
    }
}

custom_layout.xml

We should use merge here instead of ConstraintLayout because

If we use ConstraintLayout here, layout hierarchy will be ConstraintLayout->ConstraintLayout -> ImageView + TextView => we have 1 redundant ConstraintLayout => not very good for performance

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:parentTag="android.support.constraint.ConstraintLayout">

    <ImageView
        android:id="@+id/image_thumb"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:ignore="ContentDescription"
        tools:src="@mipmap/ic_launcher" />

    <TextView
        android:id="@+id/text_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="@id/image_thumb"
        app:layout_constraintStart_toStartOf="@id/image_thumb"
        app:layout_constraintTop_toBottomOf="@id/image_thumb"
        tools:text="Text" />

</merge>

Using activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <your_package.CustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#f00"
        app:image="@drawable/ic_android"
        app:text="Android" />

    <your_package.CustomView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#0f0"
        app:image="@drawable/ic_adb"
        app:text="ADB" />

</LinearLayout>

Result

enter image description here

See full code on:
Github

Hardwick answered 2/1, 2019 at 3:46 Comment(5)
This should either be the accepted or the most voted answer in this thread since it mentions unnecessary layout hierarchy.Broadax
Is there a way to somehow reference on custom view, all the attributes available on the text, and all of them from the image and somehow hook them to text view and image view without manually doing it?Tiphanie
After a 2-hours search of some practical example I found yours. That's why you have to scroll down past the accepted answer. That was very helpful, thank you!Backdrop
Thanks for mention of the tools:parentTag, even after 9 years of Android development I did not know it ❤️Bullyrag
Great answer, thank you. For those who prefer to use view binding: CustomViewBinding.bind(View.inflate(context, R.layout.custom_layout, this))Vinaigrette
S
28

Yes you can do this. RelativeLayout, LinearLayout, etc are Views so a custom layout is a custom view. Just something to consider because if you wanted to create a custom layout you could.

What you want to do is create a Compound Control. You'll create a subclass of RelativeLayout, add all our your components in code (TextView, etc), and in your constructor you can read the attributes passed in from the XML. You can then pass that attribute to your title TextView.

http://developer.android.com/guide/topics/ui/custom-components.html

Separatrix answered 1/12, 2010 at 20:48 Comment(0)
W
18

Use the LayoutInflater as I shown below.

public View myView() {
    View v; // Creating an instance for View Object
    LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    v = inflater.inflate(R.layout.myview, null);

    TextView text1 = v.findViewById(R.id.dolphinTitle);
    Button btn1 = v.findViewById(R.id.dolphinMinusButton);
    TextView text2 = v.findViewById(R.id.dolphinValue);
    Button btn2 = v.findViewById(R.id.dolphinPlusButton);

    return v;
}
Wail answered 10/9, 2011 at 13:6 Comment(1)
I have tried same. its working fine. but, when i click btn1, it will calling web services and after received response from server, i want to update some text in particular position text2..any help pls ?Showthrough
A
8

In practice, I have found that you need to be a bit careful, especially if you are using a bit of xml repeatedly. Suppose, for example, that you have a table that you wish to create a table row for each entry in a list. You've set up some xml:

In my_table_row.xml:

<?xml version="1.0" encoding="utf-8"?>
<TableRow xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent" android:id="@+id/myTableRow">
    <ImageButton android:src="@android:drawable/ic_menu_delete" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/rowButton"/>
    <TextView android:layout_height="wrap_content" android:layout_width="wrap_content" android:textAppearance="?android:attr/textAppearanceMedium" android:text="TextView" android:id="@+id/rowText"></TextView>
</TableRow>

Then you want to create it once per row with some code. It assume that you have defined a parent TableLayout myTable to attach the Rows to.

for (int i=0; i<numRows; i++) {
    /*
    * 1. Make the row and attach it to myTable. For some reason this doesn't seem
    * to return the TableRow as you might expect from the xml, so you need to
    * receive the View it returns and then find the TableRow and other items, as
    * per step 2.
    */
    LayoutInflater inflater = (LayoutInflater)getBaseContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    View v =  inflater.inflate(R.layout.my_table_row, myTable, true);

    // 2. Get all the things that we need to refer to to alter in any way.
    TableRow    tr        = (TableRow)    v.findViewById(R.id.profileTableRow);
    ImageButton rowButton = (ImageButton) v.findViewById(R.id.rowButton);
    TextView    rowText   = (TextView)    v.findViewById(R.id.rowText);
                            
    // 3. Configure them out as you need to
    rowText.setText("Text for this row");
    rowButton.setId(i); // So that when it is clicked we know which one has been clicked!
    rowButton.setOnClickListener(this); // See note below ...           

    /*
    * To ensure that when finding views by id on the next time round this
    * loop (or later) gie lots of spurious, unique, ids.
    */
    rowText.setId(1000+i);
    tr.setId(3000+i);
}

For a clear simple example on handling rowButton.setOnClickListener(this), see Onclicklistener for a programmatically created button.

Arlettaarlette answered 1/3, 2012 at 11:25 Comment(2)
Hi Neil, I have tried same. its working fine. but, when i click rowButton, it will calling web services and after received response from server, i want to update some text in particular position rowText..any help pls ?Showthrough
see my question also. #48720470Showthrough
B
1

There are multiple answers which point the same way in different approaches, I believe the below is the simplest approach without using any third-party libraries, even you can use it using Java.

In Kotlin;

Create values/attr.xml

<resources>
    <declare-styleable name="DetailsView">
        <attr format="string" name="text"/>
        <attr format="string" name="value"/>
    </declare-styleable>
</resources>

Create layout/details_view.xml file for your view

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/text_label"
            android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="0.5"
            tools:text="Label" />

        <TextView
            android:id="@+id/text_value"
            android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="0.5"
            tools:text="Value" />

    </LinearLayout>

</merge>

Create the custom view widget DetailsView.kt

import android.content.Context
import android.content.res.TypedArray
import android.util.AttributeSet
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
import com.payable.taponphone.R

class DetailsView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {

    private val attributes: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.DetailsView)
    private val view: View = View.inflate(context, R.layout.details_view, this)

    init {
        view.findViewById<TextView>(R.id.text_label).text = attributes.getString(R.styleable.DetailsView_text)
        view.findViewById<TextView>(R.id.text_value).text = attributes.getString(R.styleable.DetailsView_value)
    }
}

That's it now you can call the widget anywhere in your app as below

<com.yourapp.widget.DetailsView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:text="Welcome"
    app:value="Feb" />
Buiron answered 5/9, 2021 at 6:36 Comment(0)
T
-1

A simple Custom View using Kotlin

Replace FrameLayout with whatever view you Like to extend

/**
 * Simple Custom view
 */
class CustomView@JvmOverloads
constructor(context: Context,
            attrs: AttributeSet? = null,
            defStyleAttr: Int = 0)
    : FrameLayout(context, attrs, defStyleAttr) {

    init {
        // Init View
        val rootView = (getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
                .inflate(R.layout.custom_layout, this, true)
        val titleView= rootView.findViewById(id.txtTitle)

        // Load Values from XML
        val attrsArray = getContext().obtainStyledAttributes(attrs, R.styleable.CutsomView, defStyleAttr, 0)
        val titleString = attrsArray.getString(R.styleable.cutomAttrsText)
        attrsArray.recycle()
    }
}
Trahern answered 12/3, 2021 at 10:55 Comment(1)
What is R.styleable.CutsomView and R.styleable.cutomAttrsText?Twentyfour

© 2022 - 2024 — McMap. All rights reserved.