0

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ụ: 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 toptal-blog-image-1437058450062-c6bcabd2e0bc051b7fdd1a4854a48945.jpg

Make Your Own

  1. 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" toptal-blog-image-1437058547911-0fdabca2327f61203007cfdf091329d7.jpg

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_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"
			android:layout_width="30dp"
			android:layout_height="30dp"
			android:layout_centerVertical="true"
			android:layout_alignParentRight="true"
			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"/>

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

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

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

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

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

		<TextView
			android:layout_width="0dp"
			android:layout_height="wrap_content"
			android:layout_weight="1"
			android:gravity="center_horizontal"
			android:textColor="#222222"
			android:text="SAT"/>
	</LinearLayout>

	<!-- days view -->
	<GridView
		android:id="@+id/calendar_grid"
		android:layout_width="match_parent"
		android:layout_height="match_parent"
		android:numColumns="7"/>
</LinearLayout>
  1. 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);
   }
}
  1. 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

toptal-blog-image-1437058575255-c547b1c889c1b399f4706badb04e709b.jpg

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()));
}
  1. 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:

toptal-blog-image-1437058608136-4d048c622a39cd80824501e738952f77.jpg

  1. Sử dụng CalendarView

Thêm trong file activity_main.xml

<com.example.android.CalendarView
   android:id="@+id/calendar_view"
   android:layout_width="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);

toptal-blog-image-1437058652585-dcfd9311d048ab657200b10e7744f915.jpg

  1. 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_width="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);
  1. 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;
   }
});

Muốn gửi sự kiện cho component cha ta sẽ khai báo interface EventHandler trong CalendarView.java

public interface EventHandler
{
   void onDayLongPress(Date date);
}

Sau đó trong file MainActivity.java ta sẽ set listener cho component

@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()
   {

      public void onDayLongPress(Date date)
      {
         // show returned day
         DateFormat df = SimpleDateFormat.getDateInstance();
         Toast.makeText(MainActivity.this, df.format(date),  LENGTH_SHORT).show();
      }
   });
}

source code

source: Ahmed Al-Amir's blog


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí