Android Customization: How to Build a UI Component That Does What You Want
Posted On:
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
Make Your Own
1. The Component Layout
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
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);
}
}
3. Some Logic is Needed
- The calendar view is seven days wide, and it is guaranteed that all months will start somewhere in the first row.
- 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.
- Then, we fill out the days for the current month (1, 2, 3 etc).
- 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.
private void updateCalendar()
{
ArrayListcells = 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
- 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).
@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;
}
#44eebd82 #44d8d27e #44a1c1da #448da64b
// 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};
// 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));
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
6. Invoking the Component
android:id="@+id/calendar_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
HashSetevents = new HashSet<>();
events.add(new Date());
CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view));
cv.updateCalendar(events);
7. Adding Attributes
- 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);
8. Interacting With the Component
- Capture events inside the component, and
- Report events to the components parent (could be aFragment, anActivityor even another component).
// 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;
}
});
public interface EventHandler
{
void onDayLongPress(Date date);
}
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
HashSetevents = 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();
}
});
}
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.
Conclusion
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.