141

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!

nbarraille
  • 9,926
  • 14
  • 65
  • 92
  • See "Compound Controls" in Android, and this link: http://stackoverflow.com/questions/1476371/android-writing-a-custom-compound-component – greg7gkb Aug 09 '12 at 20:26

7 Answers7

172

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);
    }
}
WerWet
  • 323
  • 5
  • 14
Fox
  • 2,513
  • 2
  • 20
  • 24
  • 20
    Since View class has static inflate() method there is no need for LayoutInflater.from() – outlying Jul 22 '13 at 14:22
  • 2
    Isn't this just the solution of Johannes from here: http://stackoverflow.com/questions/17836695/android-custom-view-from-inflated-layout Still, this inflates another layout within. So it's not really the best soltion I'd guess. – Tobias Reich Oct 18 '14 at 14:59
  • 3
    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. – Fox Oct 18 '14 at 15:43
  • 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. – Jeffrey Blattman Jun 16 '20 at 17:56
  • 1
    @Jeffrey Blattman please check out [View.inflate](https://developer.android.com/reference/android/view/View#inflate(android.content.Context,%20int,%20android.view.ViewGroup)) method, we use this one (specifying the root as `this`, 3rd parameter) – V1raNi Jul 30 '20 at 21:17
127

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

Grzegorz Dev
  • 3,073
  • 1
  • 18
  • 22
Linh
  • 57,942
  • 23
  • 262
  • 279
  • 19
    This should either be the accepted or the most voted answer in this thread since it mentions unnecessary layout hierarchy. – Farid Nov 27 '19 at 11:44
  • 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? – uberchilly Oct 22 '20 at 10:41
  • 1
    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! – Mykola Yakovliev Apr 03 '21 at 13:54
  • 5
    Thanks for mention of the `tools:parentTag`, even after 9 years of Android development I did not know it ❤️ – Billda May 08 '21 at 08:52
  • 4
    Great answer, thank you. For those who prefer to use view binding: `CustomViewBinding.bind(View.inflate(context, R.layout.custom_layout, this))` – giorgos.nl Jun 22 '21 at 12:59
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

chubbsondubs
  • 37,646
  • 24
  • 106
  • 138
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;
}
WerWet
  • 323
  • 5
  • 14
Tsunaze
  • 3,204
  • 7
  • 44
  • 81
  • 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 ? – harikrishnan Feb 10 '18 at 10:12
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.

WerWet
  • 323
  • 5
  • 14
Neil Townsend
  • 6,024
  • 5
  • 35
  • 52
  • 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 ? – harikrishnan Feb 10 '18 at 10:00
  • see my question also. https://stackoverflow.com/questions/48719970/android-adding-view-from-inflate-layout-and-how-to-update-particular-textview-va – harikrishnan Feb 10 '18 at 10:58
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" />
Googlian
  • 6,077
  • 3
  • 38
  • 44
-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()
    }
}
WerWet
  • 323
  • 5
  • 14
Hitesh Sahu
  • 41,955
  • 17
  • 205
  • 154