Customizing Android: Creating a UI Component to Suit Your Needs

It’s common for developers to need UI elements not offered by their target platform, or that lack specific features. The solution is custom UI components.

Android’s UI model is inherently adaptable, allowing for customization, testing, and creating custom UI components in several ways:

  • Extend an existing component (like TextView, ImageView), adding or modifying functionality. For instance, a CircleImageView extending ImageView could override onDraw() to display images as circles and add a loadFromFile() function.

  • Combine components: This uses Layouts to position elements. An example is a LabeledEditText extending LinearLayout horizontally, containing a TextView label and an EditText input field.

    This method can utilize the previous one, using native or custom internal components.

  • The most flexible, but complex, approach is creating a component from scratch. This involves inheriting the View class and overriding functions like onMeasure() for layout and onDraw() for content. These components rely heavily on Android’s 2D drawing API.

Customizing Android: The CalendarView

Android provides a native CalendarView component. It’s functional, showing a month and highlighting the current day. It might look fine for a native style, but customization is limited.

For instance, you can’t change how days are marked or the background color. Adding custom text or graphics, like for special occasions, is not possible. It looks like this:

Screenshot
CalendarView in AppCompact.Light theme.

Creating a Custom Calendar

So, how do you create a custom calendar view? Any approach works, but practicality often rules out 2D graphics, leaving the other two. We’ll combine them here.

You can find the source code here.

1. Layout

We’ll display days in a grid, with the month name and navigation buttons at the top:

Screenshot
Custom calendar view.

The layout file, control_calendar.xml, looks like this (repetitive markup is shortened):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
   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">

   <!-- date toolbar -->
   <RelativeLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:paddingTop="12dp"
      android:paddingBottom="12dp"
      android:paddingLeft="30dp"
      android:paddingRight="30dp">

      <!-- prev button -->
      <ImageView
         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"/>

      <!-- date title -->
      <TextView
         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"/>

      <!-- next button -->
      <ImageView
         android:id="@+id/calendar_next_button"
         ... Same layout as prev button.
         android:src="@drawable/next_icon"/>
   </RelativeLayout>

   <!-- days header -->
   <LinearLayout
      android:id="@+id/calendar_header"
      android:layout_width="match_parent"
      android:layout_height="40dp"
      android:gravity="center_vertical"
      android:orientation="horizontal">

      <TextView
         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.
   </LinearLayout>

   <!-- days view -->
   <GridView
      android:id="@+id/calendar_grid"
      android:layout_width="match_parent"
      android:layout_height="340dp"
      android:numColumns="7"/>
</LinearLayout>

2. Component Class

This layout works within an Activity or Fragment, but encapsulation as a UI component promotes modularity.

Our UI component will be a LinearLayout:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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);
   }
}

This code inflates the XML layout and assigns internal controls to variables.

3. Adding Logic

For calendar functionality, we need some logic:

  1. The calendar is seven days wide, with all months starting in the first row.

  2. Determine the starting position of the month, filling preceding positions with previous month’s days.

  3. Fill in the current month’s days.

  4. Fill remaining positions in the last row(s) with next month’s days.

This diagram illustrates the process:

Screenshot
Custom calendar view business logic.

A six-row grid (42 days) accommodates all cases. Using Android’s date functions simplifies handling different month lengths.

The updateCalendar() function implements this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private void updateCalendar()
{
   ArrayList<Date> 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. Customization

To customize day display, we’ll use an Adapter. Our CalendearView needs:

  • Bold blue text for the current day.
  • Grayed-out days outside the current month.
  • A special icon for event days.
  • Season-based header colors.

The first three are simple. Here’s a CalendarAdapter implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@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;
}

For season-based colors, we’ll add colors to /res/values/colors.xml:

1
2
3
4
<color name="summer">#44eebd82</color>
<color name="fall">#44d8d27e</color>
<color name="winter">#44a1c1da</color>
<color name="spring">#448da64b</color>

Then, define an array mapping months to seasons:

1
2
3
4
5
6
7
8
9
// 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};

In updateCalendar(), we select the color based on the month and season:

1
2
3
4
5
6
// 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));

This gives us:

Android Customization
Header color changes according to season.

Note: Due to HashSet’s object comparison, we replace eventDays.contains(date) with:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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. Design-Time Appearance

We can call updateCalendar() in the constructor to improve the design-time view:

Screenshot

For performance, use isInEditMode() to limit data loading in the designer.

6. Using the Component

Include the component in XML layouts (see activity_main.xml):

1
2
3
4
<samples.aalamir.customcalendar.CalendarView
   android:id="@+id/calendar_view"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"/>

Retrieve and interact with it:

1
2
3
4
5
HashSet<Date> events = new HashSet<>();
events.add(new Date());

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

This displays the current day with an event marker:

Screenshot
CalendarView displaying an event

7. Adding Attributes

We can add attributes for customization via XML. Let’s add a dateFormat attribute:

1
2
3
4
5
<resources>
   <declare-styleable name="CalendarDateElement">
      <attr name="dateFormat" format="string"/>
   </declare-styleable>
</resources>

Use it in the layout:

1
2
3
4
5
6
<samples.aalamir.customcalendar.CalendarView
   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"/>

And implement it:

1
2
3
TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CalendarView);

dateFormat = ta.getString(R.styleable.CalendarView_dateFormat);

Now, the UI designer will reflect the chosen date format:

Screenshot
Changing the CalendarView attributes.

8. Handling Events

To handle events like long-presses, we can add a listener:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 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;
   }
});

We can then report these events. One way is through an interface:

1
2
3
4
public interface EventHandler
{
   void onDayLongPress(Date date);
}

The parent can provide an implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
protected void onCreate(Bundle savedInstanceState)
{
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);

   HashSet<Date> 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 now triggers this handler:

Screenshot

Another option is using Android’s Intents and BroadcastReceivers for broadcasting events to multiple receivers.

Conclusion

Behold the awesome power of Android customization!

To create custom components:

  • Design the XML layout.
  • Derive your class from a suitable parent.
  • Add logic and use attributes for customization.
  • Use isInEditMode() for better design-time experience.

This calendar view example demonstrates the process, but you can create any component you need!

Good luck with your coding!

Licensed under CC BY-NC-SA 4.0