Android Customization: How to Build a UI Component That Does What You Want
It is not uncommon for developers to find themselves in need of aUIcomponent that is either not provided by the platform they are targeting or is, indeed, provided, but lacks a certain property or behavior. The answer to both scenarios is a custom UI component.

android customization

The Android UI model is inherently customizable, offering the means ofAndroid customization,testing, and the ability to createcustom UI componentsin various ways:
  • Inherit an existing component(i.e.TextView,ImageView, etc.), and add/override needed functionality. For example, aCircleImageViewthat inheritsImageView, overriding theonDraw()function to restrict the displayed image to a circle, and adding aloadFromFile()function to load an image from external memory.
  • Create a compound componentout of several components. This approach usually takes advantage ofLayoutsto control how the components are arranged on the screen. For example, aLabeledEditTextthat inheritsLinearLayoutwith horizontal orientation, and contains both aTextViewacting as a label and anEditTextacting as a text entry field.
    This approach could also make use of the previous one, i.e., the internal components could be native or custom.
  • The most versatile and most complex approach is tocreate a self drawn component. In this case, the component would inherit the genericViewclass and override functions likeonMeasure()to determine its layout,onDraw()to display its contents, etc. Components created this way usually depend heavily on Androids2D drawing API.

Android Customization Case Study: TheCalendarView

Android provides a nativeCalendarViewcomponent. It performs well and provides the minimum functionality expected from any calendar component, displaying a full month and highlighting the current day. Some might say it looks good as well, but only if you are going for a native look, and have no interest in customizing how it looks whatsoever.
For instance, theCalendarViewcomponent provides no way of changing how a certain day is marked, or what background color to use. There is also no way of adding any custom text or graphics, to mark a special occasion, for example. In short, the component looks like this, and almost nothing can be changed:



CalendarViewinAppCompact.Lighttheme.

Make Your Own

So, how does one go about creating ones own calendar view? Any of the approaches above would work. However, practicality will usually rule out the third option (2D graphics) and leave us with the two other methods, and we will employ a mixture of both in this article.
To follow along, you can find the source codehere.

1. The Component Layout

First, lets start with how the component looks. To keep thing simple, lets display days in a grid, and, at the top, the name of the month along with next month and previous month buttons.


Custom calendar view.

This layout is defined in the filecontrol_calendar.xml, as follows. Note that some repetitive markup has been abbreviated with...:


xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white">


android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:paddingLeft="30dp"
android:paddingRight="30dp">


android:id="@+id/calendar_prev_button"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:src="@drawable/previous_icon"/>


android:id="@+id/calendar_date_display"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toRightOf="@+id/calendar_prev_button"
android:layout_toLeftOf="@+id/calendar_next_button"
android:gravity="center"
android:textAppearance="@android:style/TextAppearance.Medium"
android:textColor="#222222"
android:text="current date"/>


android:id="@+id/calendar_next_button"
... Same layout as prev button.
android:src="@drawable/next_icon"/>



android:id="@+id/calendar_header"
android:layout_width="match_parent"
android:layout_height="40dp"
android:gravity="center_vertical"
android:orientation="horizontal">

android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_horizontal"
android:textColor="#222222"
android:text="SUN"/>

... Repeat for MON - SAT.



android:id="@+id/calendar_grid"
android:layout_width="match_parent"
android:layout_height="340dp"
android:numColumns="7"/>

2. The Component Class

The previous layout can be included as-is in anActivityor aFragmentand it will work fine. But encapsulating it as a standalone UI component will prevent code repetition and allow for a modular design, where each module handles one responsibility.
Our UI component will be aLinearLayout, to match the root of the XML layout file. Note that only the important parts are shown from the code. The implementation of the component resides inCalendarView.java:

public class CalendarView extends LinearLayout
{
// internal components
private LinearLayout header;
private ImageView btnPrev;
private ImageView btnNext;
private TextView txtDate;
private GridView grid;

public CalendarView(Context context)
{
super(context);
initControl(context);
}

/**
* Load component XML layout
*/
private void initControl(Context context)
{
LayoutInflater inflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

inflater.inflate(R.layout.control_calendar, this);

// layout is inflated, assign local variables to components
header = (LinearLayout)findViewById(R.id.calendar_header);
btnPrev = (ImageView)findViewById(R.id.calendar_prev_button);
btnNext = (ImageView)findViewById(R.id.calendar_next_button);
txtDate = (TextView)findViewById(R.id.calendar_date_display);
grid = (GridView)findViewById(R.id.calendar_grid);
}
}


The code is pretty straightforward. Upon creation, the component inflates the XML layout, and when that is done, it assigns the internal controls to local variables for easier access later on.

3. Some Logic is Needed

To make this component actually behave as a calendar view, some business logic is in order. It might seem complicated at first, but there is really not much to it. Lets break it down:
  1. The calendar view is seven days wide, and it is guaranteed that all months will start somewhere in the first row.
  2. First, we need to figure out what position the month starts at, then fill all the positions before that with the numbers from the previous month (30, 29, 28.. etc.) until we reach position 0.
  3. Then, we fill out the days for the current month (1, 2, 3 etc).
  4. After that come the days for the next month (again, 1, 2, 3.. etc), but this time we only fill the remaining positions in the last row(s) of the grid.
The following diagram illustrates these steps:


Custom calendar view business logic.


The width of the grid is already specified to be seven cells, denoting a weekly calendar, but how about the height? The largest size for the grid be can be determined by the worst case scenario of a 31-days month starting on a Saturday, which is the last cell in the first row, and will need 5 more rows to display in full. So, setting the calendar to display six rows (totaling 42 days) will be sufficient to handle all cases.
But not all months have 31 days! We can avoid complications arising from that by using Androids built-in date functionality, avoiding the need to figure out the number of days ourselves.
As mentioned before, the date functionalities provided by theCalendarclass make the implementation pretty straightforward. In our component, theupdateCalendar()function implements this logic:

private void updateCalendar()
{
ArrayList cells = new ArrayList<>();
Calendar calendar = (Calendar)currentDate.clone();

// determine the cell for current month's beginning
calendar.set(Calendar.DAY_OF_MONTH, 1);
int monthBeginningCell = calendar.get(Calendar.DAY_OF_WEEK) - 1;

// move calendar backwards to the beginning of the week
calendar.add(Calendar.DAY_OF_MONTH, -monthBeginningCell);

// fill cells (42 days calendar as per our business logic)
while (cells.size() < DAYS_COUNT)
{
cells.add(calendar.getTime());
calendar.add(Calendar.DAY_OF_MONTH, 1);
}

// update grid
((CalendarAdapter)grid.getAdapter()).updateData(cells);

// update title
SimpleDateFormat sdf = new SimpleDateFormat("MMM yyyy");
txtDate.setText(sdf.format(currentDate.getTime()));
}

4. Customizable at Heart

Since the component responsible for displaying individual days is aGridView, a good place to customize how days are displayed is theAdapter, since it is responsible for holding the data and inflating views for individual grid cells.
For this example, we will require the following from ourCalendearView:
  • Present day should be inbold blue text.
  • Days outside current month should begreyed out.
  • Days with an event should display a special icon.
  • The calendar header should change colors depending on season (Summer, Fall, Winter, Spring).
The first three requirements are simple to achieve by changing text attributes and background resources. Lets us implement aCalendarAdapterto carry out this task. It is simple enough that it can be a member class inCalendarView. By overriding thegetView()function, we can achieve the above requirements:
@Override
public View getView(int position, View view, ViewGroup parent)
{
// day in question
Date date = getItem(position);

// today
Date today = new Date();

// inflate item if it does not exist yet
if (view == null)
view = inflater.inflate(R.layout.control_calendar_day, parent, false);

// if this day has an event, specify event image
view.setBackgroundResource(eventDays.contains(date)) ?
R.drawable.reminder : 0);

// clear styling
view.setTypeface(null, Typeface.NORMAL);
view.setTextColor(Color.BLACK);

if (date.getMonth() != today.getMonth() ||
date.getYear() != today.getYear())
{
// if this day is outside current month, grey it out
view.setTextColor(getResources().getColor(R.color.greyed_out));
}
else if (date.getDate() == today.getDate())
{
// if it is today, set it to blue/bold
view.setTypeface(null, Typeface.BOLD);
view.setTextColor(getResources().getColor(R.color.today));
}

// set text
view.setText(String.valueOf(date.getDate()));

return view;
}


The final design requirement takes a bit more work. First, lets add the colors for the four seasons in/res/values/colors.xml:

#44eebd82
#44d8d27e
#44a1c1da
#448da64b


Then, lets use an array to define the season for each month (assuming northern-hemisphere, for simplicity; sorry Australia!). InCalendarViewwe add the following member variables:

// seasons' rainbow
int[] rainbow = new int[] {
R.color.summer,
R.color.fall,
R.color.winter,
R.color.spring
};

int[] monthSeason = new int[] {2, 2, 3, 3, 3, 0, 0, 0, 1, 1, 1, 2};


This way, selecting an appropriate color is done by selecting the appropriate season (monthSeason[currentMonth]) and then picking the corresponding color (rainbow[monthSeason[currentMonth]), this is added toupdateCalendar()to make sure the appropriate color is selected whenever the calendar is changed.

// set header color according to current season
int month = currentDate.get(Calendar.MONTH);
int season = monthSeason[month];
int color = rainbow[season];
header.setBackgroundColor(getResources().getColor(color));


With that, we get the following result:

Android Customization

Header color changes according to season.

Important Notedue to the wayHashSetcompares objects, the above checkeventDays.contains(date)inupdateCalendar()will not yield true for date objects unless they are exactly identical. It does not perform any special checks for theDatedata type. To work around this, this check is replaced by the following code:
for (Date eventDate : eventDays)
{
if (eventDate.getDate() == date.getDate() &&
eventDate.getMonth() == date.getMonth() &&
eventDate.getYear() == date.getYear())
{
// mark this day for event
view.setBackgroundResource(R.drawable.reminder);
break;
}
}

5. It Looks Ugly in Design Time

Androids choice for placeholders in design-time can be questionable. Fortunately, Android actually instantiates our component in order to render it in the UI designer, and we can exploit this by callingupdateCalendar()in the component constructor. This way the component will actually make sense in design time.


If initializing the component calls for lots of processing or loads lots of data, it can affect the performance of the IDE. In this case, Android provides a nifty function calledisInEditMode()that can be used to limit the amount of data used when the component is actually instantiated in the UI designer. For example, if there are lots of events to be loaded into theCalendarView, we can useisInEditMode()inside theupdateCalendar()function to provide an empty/limited event list in design mode, and load the real one otherwise.

6. Invoking the Component

The component can be included in XML layout files (a sample usage can be found inactivity_main.xml):
 android:id="@+id/calendar_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>


And retrieved to interact with once the layout is loaded:

HashSet events = new HashSet<>();
events.add(new Date());

CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view));
cv.updateCalendar(events);


The above code creates aHashSetof events, adds the current day to it, then passes it toCalendarView. As a result, theCalendarViewwill display the current day in bold blue and also put the event marker on it:



CalendarViewdisplaying an event

7. Adding Attributes

Another facility provided by Android is to assign attributes to a custom component. This allowsAndroid developersusing the component to select settings via the layout XML and see the result immediately in the UI designer, as opposed to having to wait and see how theCalendarViewlooks like in runtime. Lets add the ability to change the date format display in the component, for example, to spell out the full name of the month instead of the three-letter abbreviation.
To do this, the following steps are needed:
  • Declare the attribute. Lets call itdateFormatand give itstringdata type. Add it to/res/values/attrs.xml:





  • Use the attribute in the layout that is using the component, and give it the value"MMMM yyyy":
 xmlns:calendarNS="http://schemas.android.com/apk/res/samples.aalamir.customcalendar"
android:id="@+id/calendar_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
calendarNS:dateFormat="MMMM yyyy"/>

  • Finally, have the component make use of the attribute value:
TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CalendarView);
dateFormat = ta.getString(R.styleable.CalendarView_dateFormat);

Build the project and you will notice the displayed date changes in theUI designerto use the full name of the month, like July 2015. Try providing different values and see what happens.


Changing theCalendarViewattributes.

8. Interacting With the Component

Have you tried pressing on a specific day? The inner UI elements in our component still behave in their normal expected way and will fire events in response to user actions. So, how do we handle those events?
The answer involves two parts:
  • Capture events inside the component, and
  • Report events to the components parent (could be aFragment, anActivityor even another component).
The first part is pretty straightforward. For example, to handle long-pressing grid items, we assign a corresponding listener in our component class:

// long-pressing a day
grid.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener()
{

@Override
public boolean onItemLongClick(AdapterView view, View cell, int position, long id)
{
// handle long-press
if (eventHandler == null)
return false;

Date date = view.getItemAtPosition(position);
eventHandler.onDayLongPress(date);

return true;
}
});


There are several methods for reporting events. A direct and simple one is to copy the way Android does it: it provides an interface to the components events that is implemented by the components parent (eventHandlerin the above code snippet).
The interfaces functions can be passed any data that is relevant to the application. In our case, the interface needs to expose one event handler, which is passed the date for the pressed day. The following interface is defined inCalendarView:
public interface EventHandler
{
void onDayLongPress(Date date);
}

The implementation provided by the parent can be supplied to the calendar view via asetEventHandler(). Here is sample usage from `MainActivity.java:

@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

HashSet events = new HashSet<>();
events.add(new Date());

CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view));
cv.updateCalendar(events);

// assign event handler
cv.setEventHandler(new CalendarView.EventHandler()
{
@Override
public void onDayLongPress(Date date)
{
// show returned day
DateFormat df = SimpleDateFormat.getDateInstance();
Toast.makeText(MainActivity.this, df.format(date), LENGTH_SHORT).show();
}
});
}


Long-pressing a day will fire a long-press event that is captured and handled by theGridViewand reported by callingonDayLongPress()in the provided implementation, which, in turn, will show the date of the pressed day on the screen:



Another, more advanced way to handle this is by using AndroidsIntentsandBroadcastReceivers. This is particularly helpful when several components need to be notified of the calendars event. For example, if pressing a day in the calendar requires a text to be displayed in anActivityand a file to be downloaded by a backgroundService.

Using the previous approach will require theActivityto provide anEventHandlerto the component, handling the event and then passing it to theService. Instead, having the component broadcast anIntentand both theActivityandServiceaccepting it via their ownBroadcastReceiversnot only makes life easier but also helps decouple theActivityand theServicein question.

Conclusion

Behold the awesome power of Android customization!

So, this is how you create your own custom component in a few simple steps:
  • Create the XML layout and style it to suit your needs.
  • Derive your component class from the appropriate parent component, according to your XML layout.
  • Add your components business logic.
  • Use attributes to enable users to modify the components behavior.
  • To make it easier to use the component in the UI designer, use AndroidsisInEditMode()function.
In this article, we created a calendar view as an example, mainly because the stock calendar view is, in many ways, lacking. But, you are in no way limited as to what kind of components you can create. You can use the same technique to create anything you need, the sky is the limit!
Thank you for reading this guide, I wish you the best of luck in your coding endeavors!



Posted on :

About the Author

Demir Selmanovic

Demir is a developer and project manager with over 15 years of professional experience in a wide range of software development roles. He excels as a solo developer, team member, team leader, or manager of multiple distributed teams. He works closely with clients to define ideas and deliver products.

« Previous
Android Testing Tutorial: Unit Testing like a True Green Droid


x
Google+