Understand and Implement Custom Views in Android - Part 2

in #utopian-io6 years ago (edited)

Repository:

https://github.com/enyason/custom_view

Repository

https://github.com/enyason/custom_view

What Will I Learn?

  • How custom views work
  • How to extend a built-in view
  • How to build a compound control

Requirements

  • System Requirements: Java JDK, Android Studio
  • OS Support for Java: Windows, Mac OS, Linux
  • Required Knowledge: A fair knowledge of Java, OOP and Android Studio

Resources for Java and this tutorial

Difficulty : Beginner

Tutorial Duration 25- 30 Minutes

Tutorial Content

In the first part of this tutorial series, we considered a custom view, what it means and how to implement it on android. We started off by extending a simple text view that displayed a custom icon and a text. For this second part of the series, we are going to implement a compound control.

What is a compound control?

  • A group of views often with additional functionalities
  • Modularize code into custom compound control with this, you can reuse the same functionality

To delve further into compound control, we are going to implement a ListTile. The list tile will have 3 views: A trailing icon, A leading icon, and a title. see image below for clarity.

ListTile.png

Step 1 : Create an XML layout

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

    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"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >

<ImageView
        android:id="@+id/trailing"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_margin="5dp" />

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_toRightOf="@id/trailing"
        android:textSize="18sp" />

    <ImageView
        android:id="@+id/leading"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_margin="5dp" />


</RelativeLayout>

This XML code creates our ListTile layout as shown below. The image views for displaying the trailing and leading icons

ListTile.png

Step 2: Define custom attributes for Listile

This attributes will be tied up to the listTile view so it can be referenced from XML

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ListTile">

        <attr name="title" format="string" />
        <attr name="leading" format="reference" />
        <attr name="trailing" format="reference" />

    </declare-styleable>

</resources>

We add the required attributes in the declare-styleable tag using the attr tag. The name property of the attr tag defines a label for the attribute while the format property defines the Type of the attribute. String format defines a Type for text while reference defines a Type for resource files e.g a drawable. Note: The name property of the declare-styleable tag should have a value equal to the custom view class name created.

Step 3 : Create a Java Class ListTile.java

public class ListTile extends RelativeLayout {

    //define android views
    private ImageView trailing,leading;
    private TextView title;

    public ListTile(Context context) {
        super(context);
        init(context,null);
    }

    public ListTile(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context,attrs);

    }

    public ListTile(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context,attrs);

    }


    void init(Context context,AttributeSet attrs) {


        Drawable leadingIcon,trailingIcon;
        String text;

        //check for if AttributeSet is null
        if (attrs ==null) {

            return;
        }


        //get attributes from attrs.xml into array
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ListTile);

        //extract attributes from array into the variables
        leadingIcon = typedArray.getDrawable(R.styleable.ListTile_leading);
        trailingIcon = typedArray.getDrawable(R.styleable.ListTile_trailing);
        text = typedArray.getString(R.styleable.ListTile_title);


        //inflate layout
        LayoutInflater inflater = LayoutInflater.from(getContext());
        View tile= inflater.inflate(R.layout.list_tile,this);

        tile.setBackground(getResources().getDrawable(R.drawable.background));

        //find views
        trailing = findViewById(R.id.trailing);
        leading = findViewById(R.id.leading);
        title = findViewById(R.id.title);

        //set view properties
        trailing.setImageDrawable(trailingIcon);
        leading.setImageDrawable(leadingIcon);
        title.setText(text);

        invalidate(); // redraw ui


    }

}
  • The ListTile extends a RelativeLayout and overrides 3 constructors, passing the current context and attributes to the init function

  • We get the attributes defined in our attrs.xml file and store in a TypedArray. A Typed array is a Container for an array of values that were retrieved from XML.

  • We inflate the layout we defined for our custom view that's list_tile.xml. This is done using the layout inflater. This is similar to when we call setContentView in onCreate method of an Activity

  • We call setBackground on the tile view and pass a drawable from XML to it. This drawable marks the Tile border with a specified color. see code snippet for code

    <?xml version="1.0" encoding="utf-8"?>
    <shape
        android:shape="rectangle"
        xmlns:android="http://schemas.android.com/apk/res/android">
    
        <solid android:color="#fff"/>
        <stroke android:width="1dp" android:color="#cacaca"/>
        
    </shape>
    
    • Basically, I created an XML file in the drawable folder,
    • Create a basic rectangular shape
    • define the needed properties with the appropriate values
  • Get a reference to the views defined in the list_tile.xml

  • set view properties from the attributes specified in XML

  • call invalidate();. This method call will redraw the UI to reflect changes made in XML immediately

Step 4 : Use ListTile in MainActivity Layout file

With our ListTile ready, we can use it now in our 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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">


    <com.enyason.custom_view_1.ListTile
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:leading="@drawable/ic_navigate_next"
        app:title="User profile"
        app:trailing="@drawable/ic_person" />


</LinearLayout>

The code snippet above shows the use of the custom view created in the activity_main.xml file. Running the app gives the output show in the image below.

tile_single.png

Step 5 : Using the Merge Tag

<merge/> is useful because it can get rid of unneeded ViewGroups. In our list_tile.xml file, we have the root element as RelativeLayout which is inflated in the Listile.java class. With this class extended the Relative layout, we now have a duplicate view group. To see this duplication we'll use the Android Layout Inspector

  • Navigate to Layout Inspector from tools, from the tools bar

Screenshot 2.png

  • Select the process from the device connected

Screenshot 3.png

  • From the image below you see we have a Listile view which is a RelativeLayout and another redundant Relative Layout

Screenshot 1.png

  • To get rid of the unneeded view group, use the merge tag in list_tile.xml as shown in the code snippet below

    <?xml version="1.0" encoding="utf-8"?>
    <merge
        xmlns:android="http://schemas.android.com/apk/res/android"
           >
    
    <ImageView
            android:id="@+id/trailing"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_margin="5dp" />
    
        <TextView
            android:id="@+id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_toRightOf="@id/trailing"
            android:textSize="18sp" />
    
        <ImageView
            android:id="@+id/leading"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_margin="5dp" />
    
    
    </merge>
    
  • Checking the View hierarchy again, the redundant view group has been removed.

Screenshot with merge tag.png

UseCase

With the Listile created we should be able to make screens such as one below easily

tile_multiple.png

Curriculum

Proof of Work

The complete source code can be found on gitHub https://github.com/enyason/custom_view

Sort:  

Thank you for your contribution @ideba.
After reviewing your tutorial we suggest the following points listed below:

  • Your tutorial is quite short for a good tutorial. We recommend you aim for capturing at least 2-3 concepts.

  • Nice work on the explanations of your code, although adding a bit more comments to the code can be helpful as well.

  • The screenshots of your mobile phone are very large. Only place screenshot with content.

  • We suggest that you ident your code. It's not necessary to give many breaks of lines between the lines of your code.
    1.png

Looking forward to your upcoming tutorials.

Your contribution has been evaluated according to Utopian policies and guidelines, as well as a predefined set of questions pertaining to the category.

To view those questions and the relevant answers related to your post, click here.


Need help? Chat with us on Discord.

[utopian-moderator]

Thank you for your review, @portugalcoin! Keep up the good work!

Congratulations @ideba! You have completed the following achievement on the Steem blockchain and have been rewarded with new badge(s) :

You made more than 50 upvotes. Your next target is to reach 100 upvotes.

You can view your badges on your Steem Board and compare to others on the Steem Ranking
If you no longer want to receive notifications, reply to this comment with the word STOP

To support your work, I also upvoted your post!

Do not miss the last post from @steemitboard:

3 years on Steem - The distribution of commemorative badges has begun!
Happy Birthday! The Steem blockchain is running for 3 years.
Vote for @Steemitboard as a witness to get one more award and increased upvotes!

Hey, @ideba!

Thanks for contributing on Utopian.
We’re already looking forward to your next contribution!

Get higher incentives and support Utopian.io!
Simply set @utopian.pay as a 5% (or higher) payout beneficiary on your contribution post (via SteemPlus or Steeditor).

Want to chat? Join us on Discord https://discord.gg/h52nFrV.

Vote for Utopian Witness!

Hi, @ideba!

You just got a 0.67% upvote from SteemPlus!
To get higher upvotes, earn more SteemPlus Points (SPP). On your Steemit wallet, check your SPP balance and click on "How to earn SPP?" to find out all the ways to earn.
If you're not using SteemPlus yet, please check our last posts in here to see the many ways in which SteemPlus can improve your Steem experience on Steemit and Busy.