Custom Views: make your android app stand out
CustomViews are always seen like a tailed beast, especially when you check the developer site on how you can create your custom view widgets, you see a lot of things that don’t really do what your looking for. Most of the tutorials on the developer site for creating custom views rely on you drawing your own layout which can be quiet confusing and difficult especially when you think of how to get a reference to the objects you draw. Here is an easier way to quickly get you through a custom view and still satisfy your ui concern.
One thing you need to know about creating a custom view is the FrameLayout is your friend. This ViewGroup isn’t commonly used by many developers especially since its hard to understand its role in the view heir achy well for one FrameLayout is one of the most convenient views to extend and start hacking away your custom widget.
STEP 1:
Create a new java file lets name it “Custom.java”, extend the FrameLayout and override the necessary constructors.
public class RevealView extends FrameLayout{ protected boolean toggle; protected Drawable lessArrow = getResources().getDrawable(R.drawable.ic_expand_less); protected Drawable moreArrow = getResources().getDrawable(R.drawable.ic_expand_more_white); private FontTextView topBarText, bottomBarText; protected View topBar; protected ViewGroup bottomBar; protected ImageView toggleBottom;
public RevealView(@NonNull Context context) { super(context); } public RevealView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public RevealView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { super(context, attrs, defStyleAttr); }}
STEP 2:
Lets create our xml layout file, this is where all the construction will take place. So for this tutorial we will create an Accordion which reveals extra content on touch. So head over to the res
folder and create our custom.xml
layout file. Now before you add the xml code you need to download and add two icons to your drawable folder, 1. an arrow facing up and 2. the other facing down. You can get them from the google material design icon pack
<?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:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content"> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/holo_blue_dark" android:id="@+id/topBar" android:paddingTop="8dp" android:paddingBottom="8dp" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/top_bar_title" android:layout_marginLeft="12dp" android:textSize="16sp" android:textColor="@android:color/white" android:layout_centerVertical="true" /> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/toggle_button" android:src="@drawable/ic_expand_more_white" android:layout_marginRight="12dp" android:layout_centerVertical="true" android:layout_alignParentRight="true" /> </RelativeLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/bottomBar" android:visibility="gone" android:animateLayoutChanges="true" android:background="@android:color/holo_blue_light" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="12dp" android:id="@+id/bottom_bar_text" android:textColor="@android:color/white"/> </LinearLayout></LinearLayout>
If you look closely you will notice the last ViewGroup which is a LinearLayout we are hiding the view by setting visibility:gone
just put this in your head as a side note we will come back to this view later.
STEP 3:
So here’s where the fun begins, you know all those properties you set on Views in your xml file? Well we are going to define some for our customView. To do that head over to the res
directory then look for a folder called values
there should be a file called attrs.xml
in there if its not available then create it in other words the full path your looking for should be res/values/attrs.xml
. Now add this block of code to the file to define each property we can set from any xml file where we use our customview.
<?xml version="1.0" encoding="utf-8"?><resources>
<declare-styleable name="RevealView"> <attr name="colorBar" format="color|reference" /> <attr name="contentBar" format="color|reference"/> <attr name="barTitle" format="string" localization="suggested"/> <attr name="message" format="string" localization="suggested"/> <attr name="showMessage" format="boolean"/></declare-styleable>
</resources>
Your file should look like the one above. So lets briefly go through the properties:
- colorBar: Defines the color for the header
- contentBar: Defines the color for the body/content(the part of the view that will be visible or hidden by user interaction.
- barTitle: Title text that will appear on the view header.
- message: Body text of the bottoview
- showMessage: A value that determines if the view should show the body container or not.
STEP 4:
Lets head over to our Custom.java and initialise some variables we’ll use in constructing our view.
public class RevealView extends FrameLayout{
//init properties protected boolean toggle; protected Drawable lessArrow = getResources().getDrawable(R.drawable.ic_expand_less); protected Drawable moreArrow = getResources().getDrawable(R.drawable.ic_expand_more_white); private TextView topBarText, bottomBarText; protected View topBar; protected ViewGroup bottomBar; protected ImageView toggleBottom;
public RevealView(@NonNull Context context) { super(context); } public RevealView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public RevealView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { super(context, attrs, defStyleAttr); }}
Now lets create our helper methods that will control the view. Add these methods to the bottom of the file.
i.e I know we haven’t initialised these properties yet don’t worry we are coming to that, we don’t want null pointers.
public void setTitle(String title){ topBarText.setText(title);}public void setMessage(String message){ bottomBarText.setText(message);}public void setTitleBarColor(int color){ topBar.setBackgroundColor(getResources().getColor(color));}public void setBottomBarColor(int color){ bottomBar.setBackgroundColor(getResources().getColor(color));}
Lets create our onClickListener so ever time someone touches the top bar the bottom part of the view is either revealed or hidden. You can add this to the bottom of the file.
Remember that view we added visibility:gone
to? this listener will take care of enabling or hiding it.
protected OnClickListener setToggle(final ImageView toggleButton, final View bottomBar){ return new OnClickListener() { @Override public void onClick(View v) { Drawable drawable = lessArrow; int visibility = View.VISIBLE; if(toggle){ drawable = moreArrow; visibility = View.GONE; toggle = false; }else { toggle = true; } toggleButton.setImageDrawable(drawable); bottomBar.setVisibility(visibility); } };}
Alright so now lets wrap them all together in an init method that the view will call every time an action is performed on it.
public void init(Context context, AttributeSet attributeSet){ inflate(context, R.layout.reveal_view, this); topBar = findViewById(R.id.topBar); bottomBar = (ViewGroup) findViewById(R.id.bottomBar); toggleBottom = (ImageView) findViewById(R.id.toggle_button); topBarText = (TextView) findViewById(R.id.top_bar_title); bottomBarText = (TextView) findViewById(R.id.bottom_bar_text); topBar.setOnClickListener(setToggle(toggleBottom, bottomBarText)); if(attributeSet != null){ TypedArray typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.RevealView); try { toggle = typedArray.getBoolean(R.styleable.RevealView_showMessage, false); bottomBar.setVisibility(toggle ? View.VISIBLE : View.GONE); toggleBottom.setImageDrawable(toggle ? lessArrow : moreArrow); if(typedArray.hasValue(R.styleable.RevealView_barTitle)){ topBarText.setText(typedArray.getString(R.styleable.RevealView_barTitle)); } if(typedArray.hasValue(R.styleable.RevealView_colorBar)){ topBar.setBackgroundColor(typedArray.getColor(R.styleable.RevealView_colorBar, getResources().getColor(R.color.top_bar_color))); } if(typedArray.hasValue(R.styleable.RevealView_contentBar)){ bottomBar.setBackgroundColor(typedArray.getColor(R.styleable.RevealView_contentBar, getResources().getColor(R.color.bottom_bar_color))); } if(typedArray.hasValue(R.styleable.RevealView_message)){ bottomBarText.setText(typedArray.getString(R.styleable.RevealView_message)); } } finally { typedArray.recycle(); } }}
Now we need to call the init()
in our constructors defined earlier. Note that whenever a view is updated the constructors are called hence our method will be called and we can perform actions on our view.
public RevealView(@NonNull Context context) { super(context); init(context, null);}public RevealView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(context, attrs);}public RevealView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs);}
Now you can see how we reference the attrs.xml
file we created earlier to see which value is set and modify our view based on that. So lets add our view to a layout file and see if we did good. So lets create reveal_tester.xml
To add your view just call the class name of the custom view and android studio autocomplete should help you with the full package name.
<?xml version="1.0" encoding="utf-8"?><FrameLayout 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:background="#EEEEEE" android:orientation="vertical"> <com.example.RevealExample.RevealView android:id="@+id/reveal_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:visibility="gone" app:showMessage="true"/></FrameLayout>
Now if you notice a namespace is added automatically for use app:
we use this to access our custom properties, and we set the showMessage property to true so the body can be shown on default.
So head over to main activity and set this layout as the layout for the activity so we can test what we’ve done.
setContentView(R.layout.reveal_tester);
thats all no need for extra inits since the view widget is already taking care of click/touch interactions.
Thanks for reading, hope this helps you when creating more complex custom views for your android apps.
Great