How to Build a UI Component That Does What You Want
Đôi khi những component sẵn có trong android không đáp ứng được chức năng mà app ta đang hướng đến. Khi đó ta sẽ custom component để tạo ra component đáp ứng được yêu cầu đó. Các mô hình Android UI vốn đã tùy biến, cung cấp khả năng tùy chỉnh theo các cách sau: Kế thừa Component có sẵn(ví dụ: ...
Đôi khi những component sẵn có trong android không đáp ứng được chức năng mà app ta đang hướng đến. Khi đó ta sẽ custom component để tạo ra component đáp ứng được yêu cầu đó.
Các mô hình Android UI vốn đã tùy biến, cung cấp khả năng tùy chỉnh theo các cách sau:
- Kế thừa Component có sẵn(ví dụ: ImageView, TextView, v.v...)
- Kế thừa lớp View và override các phương thức cần thiết như onDraw(), onMeasure(), onKeyDown()...
CalendarView
Android cung cấp CalendarView với các chức năng đáp ứng các yêu cầu cơ bản như hiển thị tất cả ngày của tháng. Tuy nhiên, CalendarView không thể đánh dấu một sự kiện đặc biệt
Make Your Own
- Thành phần giao diện
Trước tiên ta sẽ bắt đầu với giao diện của component ta sẽ build. Để đơn giản ta sẽ hiển thị ngày theo grid, bên trên sẽ hiển thị tháng, buttons "next month" và "previous month"
Giao diện trên được định nghĩa trong file control_calendar.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_awidth="match_parent" android:layout_height="match_parent" android:background="@android:color/white"> <RelativeLayout android:layout_awidth="match_parent" android:layout_height="wrap_content" android:paddingTop="12dp" android:paddingBottom="12dp" android:paddingLeft="30dp" android:paddingRight="30dp"> <ImageView android:id="@+id/calendar_prev_button" android:layout_awidth="30dp" android:layout_height="30dp" android:layout_centerVertical="true" android:layout_alignParentLeft="true" android:src="@drawable/previous_icon"/> <TextView android:id="@+id/calendar_date_display" android:layout_awidth="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"/> <ImageView android:id="@+id/calendar_next_button" android:layout_awidth="30dp" android:layout_height="30dp" android:layout_centerVertical="true" android:layout_alignParentRight="true" android:src="@drawable/next_icon"/> </RelativeLayout> <LinearLayout android:id="@+id/calendar_header" android:layout_awidth="match_parent" android:layout_height="40dp" android:gravity="center_vertical" android:orientation="horizontal"> <TextView android:layout_awidth="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center_horizontal" android:textColor="#222222" android:text="SUN"/> <TextView android:layout_awidth="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center_horizontal" android:textColor="#222222" android:text="MON"/> <TextView android:layout_awidth="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center_horizontal" android:textColor="#222222" android:text="TUE"/> <TextView android:layout_awidth="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center_horizontal" android:textColor="#222222" android:text="WED"/> <TextView android:layout_awidth="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center_horizontal" android:textColor="#222222" android:text="THU"/> <TextView android:layout_awidth="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center_horizontal" android:textColor="#222222" android:text="FRI"/> <TextView android:layout_awidth="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center_horizontal" android:textColor="#222222" android:text="SAT"/> </LinearLayout> <GridView android:id="@+id/calendar_grid" android:layout_awidth="match_parent" android:layout_height="match_parent" android:numColumns="7"/> </LinearLayout>
- Java class của thành phần
Ta định nghĩa trong file CalendarView.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); } }
- Logic
Để thành phần UI này hoạt động như UI lịch ta sẽ thêm logic cần thiết:
- CalendarView sẽ có chiều rộng là 7 ngày, và tất cả các tháng đều bắt đầu từ hàng đầu tiên
- Đầu tiên ta sẽ tìm vị trí bắt đầu của tháng ở hàng đầu tiên, sau đó điền các ô trước đó ngày của tháng trước đó
- Sau đó ta điền các ngày trong tháng hiện tại
- Sau đó ở các ô còn lại ta điền ngày của tháng tiếp theo
Về chiều rộng của CalendarView sẽ là 7 để biểu thị cho 1 tuần. Tuy nhiên về chiều cao của CalendarView ta xét trường hợp xấu nhất là tháng có 31 ngày và bắt đầu vào thứ 7(tức là ở ô cuối cùng của hàng đầu tiên), và sẽ cần thêm 5 dòng để hiển thị đầy đủ. Vậy, tổng cộng ta cần 6 hàng để hiển thị tất cả trường hợp
Ta sẽ thực hiện đoạn logic trên trong phương thức updateCalendar()
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())); }
- Tùy chỉnh CalendarView
Ta dùng GridView để hiển thị từng ngày, vì vậy ta sẽ tùy chỉnh Adapter để hiển thị ngày, ví dụ:
- Ngày hiện tại sẽ hiển thị là màu xanh(blue)
- Những ngày không phải tháng hiện tại sẽ hiển thị màu xám
- Ngày có sự kiện sẽ hiển thị icon đặc biệt
- Tiêu đề CalendarView sẽ thay đổi màu theo mùa
3 yêu cầu đầu tiên ta sẽ thực hiện trong CalendarAdapter
@Override public View getView(int position, View view, ViewGroup parent) { // day in question Date date = getItem(position); int day = date.getDate(); int month = date.getMonth(); int year = date.getYear(); // 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(0); if (eventDays != null) { for (Date eventDate : eventDays){ if (eventDate.getDate() == day && eventDate.getMonth() == month && eventDate.getYear() == year){ // mark this day for event view.setBackgroundResource(R.drawable.reminder); break; } } } // clear styling ((TextView)view).setTypeface(null, Typeface.NORMAL); ((TextView)view).setTextColor(Color.BLACK); if (month != today.getMonth() || year != today.getYear()) { // if this day is outside current month, grey it out ((TextView)view).setTextColor(getResources().getColor(R.color.greyed_out)); } else if (day == today.getDate()) { // if it is today, set it to blue/bold ((TextView)view).setTypeface(null, Typeface.BOLD); ((TextView)view).setTextColor(getResources().getColor(R.color.today)); } // set text ((TextView)view).setText(String.valueOf(date.getDate())); return view; }
Để thay đổi màu của header theo mùa ta thực hiện như sau
Đầu tiên sẽ thêm màu: /res/values/colors.xml
<color name="summer">#44eebd82</color> <color name="fall">#44d8d27e</color> <color name="winter">#44a1c1da</color> <color name="spring">#448da64b</color>
Sau đó tạo mảng để lưu mùa, trong CalendarView.java thêm biến sau:
// 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};
Bằng cách này ta sẽ chọn màu thích hợp như sau: rainbow[monthSeason[currentMonth]] trong phương thức updateCalendar() ta thêm đoạn code:
// 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));
Khi đó ta sẽ có giao diện như sau:
- Sử dụng CalendarView
Thêm trong file activity_main.xml
<com.example.android.CalendarView
android:id="@+id/calendar_view"
android:layout_awidth="match_parent"
android:layout_height="wrap_content"/>
Sau đó trong file java
HashSet<Date> events = new HashSet<>(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events);
- Thêm thuộc tính
- Khai báo thuộc tính trong file /res/values/attrs.xml
<resources> <declare-styleable name="CalendarView"> <attr name="dateFormat" format="string"/> </declare-styleable> </resources>
- Sử dụng thuộc tính trên trong file xml khai báo của component
<com.example.android.calendarview.CalendarView
xmlns:calendarNS="http://schemas.android.com/apk/res/com.example.android.calendarview"
android:id="@+id/calendar_view"
android:layout_awidth="match_parent"
android:layout_height="wrap_content"
calendarNS:dateFormat="MMMM yyyy"/>
- Cuối cùng thì sẽ sử dụng thuộc tính trong file CalendarView.java
TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CalendarView); dateFormat = ta.getString(R.styleable.CalendarView_dateFormat);
- Tương tác với component
Để tương tác với component ta sẽ thực hiện như sau:
- Bắt sự kiện của component
- Gửi sự kiện bắt được cho component cha của CalendarView như Fragment, Activity để xử lý
Để bắt sự kiện của component ta sẽ đăng ký listener
// long-pressing a day grid.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { 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;