Kustomisasi Android: Cara Membangun Komponen UI yang Melakukan Apa yang Anda Inginkan
Diterbitkan: 2022-03-11Bukan hal yang aneh bagi pengembang untuk menemukan diri mereka membutuhkan komponen UI yang tidak disediakan oleh platform yang mereka targetkan atau memang disediakan, tetapi tidak memiliki properti atau perilaku tertentu. Jawaban untuk kedua skenario adalah komponen UI khusus.
Model UI Android secara inheren dapat disesuaikan, menawarkan sarana penyesuaian, pengujian, dan kemampuan Android untuk membuat komponen UI khusus dengan berbagai cara:
Mewarisi komponen yang ada (yaitu
TextView
,ImageView
, dll.), dan menambah/mengganti fungsionalitas yang diperlukan. Misalnya,CircleImageView
yang mewarisiImageView
, mengesampingkan fungsionDraw()
untuk membatasi gambar yang ditampilkan menjadi lingkaran, dan menambahkan fungsiloadFromFile()
untuk memuat gambar dari memori eksternal.Buat komponen gabungan dari beberapa komponen. Pendekatan ini biasanya memanfaatkan Layout untuk mengontrol bagaimana komponen diatur di layar. Misalnya,
LabeledEditText
yang mewarisiLinearLayout
dengan orientasi horizontal, dan berisiTextView
yang berfungsi sebagai label danEditText
yang berfungsi sebagai bidang entri teks.Pendekatan ini juga dapat menggunakan pendekatan sebelumnya, yaitu komponen internal dapat berupa native atau custom.
Pendekatan yang paling serbaguna dan paling kompleks adalah membuat komponen yang digambar sendiri . Dalam hal ini, komponen akan mewarisi kelas
View
generik dan menggantikan fungsi sepertionMeasure()
untuk menentukan tata letaknya,onDraw()
untuk menampilkan kontennya, dll. Komponen yang dibuat dengan cara ini biasanya sangat bergantung pada API gambar 2D Android.
Studi Kasus Penyesuaian Android: Tampilan CalendarView
Android menyediakan komponen CalendarView
asli. Ini berkinerja baik dan menyediakan fungsionalitas minimum yang diharapkan dari komponen kalender apa pun, menampilkan satu bulan penuh dan menyoroti hari ini. Beberapa orang mungkin mengatakan itu terlihat bagus juga, tetapi hanya jika Anda menginginkan tampilan asli, dan tidak tertarik untuk menyesuaikan tampilannya sama sekali.
Misalnya, komponen CalendarView
tidak menyediakan cara untuk mengubah bagaimana hari tertentu ditandai, atau warna latar belakang apa yang digunakan. Juga tidak ada cara untuk menambahkan teks atau gambar khusus, untuk menandai acara khusus, misalnya. Singkatnya, komponennya terlihat seperti ini, dan hampir tidak ada yang bisa diubah:
CalendarView
dalam tema AppCompact.Light
.
Buatlah milikmu sendiri
Jadi, bagaimana cara membuat tampilan kalender sendiri? Salah satu pendekatan di atas akan berhasil. Namun, kepraktisan biasanya akan mengesampingkan opsi ketiga (grafik 2D) dan meninggalkan kami dengan dua metode lainnya, dan kami akan menggunakan campuran keduanya dalam artikel ini.
Untuk mengikuti, Anda dapat menemukan kode sumber di sini.
1. Tata Letak Komponen
Pertama, mari kita mulai dengan tampilan komponen. Untuk mempermudah, mari tampilkan hari dalam kotak, dan, di atas, nama bulan bersama dengan tombol "bulan depan" dan "bulan sebelumnya".
Tata letak ini didefinisikan dalam file control_calendar.xml
, sebagai berikut. Perhatikan bahwa beberapa markup berulang telah disingkat dengan ...
:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro 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: 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: 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: ... Same layout as prev button. android:src="@drawable/next_icon"/> </RelativeLayout> <!-- days header --> <LinearLayout android: 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: android:layout_width="match_parent" android:layout_height="340dp" android:numColumns="7"/> </LinearLayout>
2. Kelas Komponen
Tata letak sebelumnya dapat dimasukkan apa adanya dalam Activity
atau Fragment
dan itu akan berfungsi dengan baik. Tetapi merangkumnya sebagai komponen UI mandiri akan mencegah pengulangan kode dan memungkinkan desain modular, di mana setiap modul menangani satu tanggung jawab.
Komponen UI kami akan menjadi LinearLayout
, untuk mencocokkan root dari file layout XML. Perhatikan bahwa hanya bagian penting yang ditampilkan dari kode. Implementasi komponen berada di 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); } }
Kodenya cukup mudah. Setelah dibuat, komponen akan mengembangkan tata letak XML, dan ketika selesai, komponen tersebut menetapkan kontrol internal ke variabel lokal untuk akses yang lebih mudah di kemudian hari.
3. Beberapa Logika Dibutuhkan
Untuk membuat komponen ini benar-benar berperilaku sebagai tampilan kalender, beberapa logika bisnis harus diatur. Ini mungkin tampak rumit pada awalnya, tetapi sebenarnya tidak banyak. Mari kita uraikan:
Tampilan kalender lebarnya tujuh hari, dan dijamin bahwa semua bulan akan dimulai di suatu tempat di baris pertama.
Pertama, kita perlu mencari tahu posisi awal bulan apa, lalu isi semua posisi sebelumnya dengan angka dari bulan sebelumnya (30, 29, 28.. dst) hingga kita mencapai posisi 0.
Kemudian, kami mengisi hari untuk bulan berjalan (1, 2, 3… dst).
Setelah itu datanglah hari-hari untuk bulan berikutnya (sekali lagi, 1, 2, 3... dst), tapi kali ini kita hanya mengisi posisi yang tersisa di baris terakhir dari grid.
Diagram berikut menggambarkan langkah-langkah tersebut:
Lebar kisi sudah ditentukan menjadi tujuh sel, yang menunjukkan kalender mingguan, tetapi bagaimana dengan tingginya? Ukuran terbesar untuk grid dapat ditentukan oleh skenario kasus terburuk dari bulan 31-hari dimulai pada hari Sabtu, yang merupakan sel terakhir di baris pertama, dan akan membutuhkan 5 baris lagi untuk ditampilkan secara penuh. Jadi, pengaturan kalender untuk menampilkan enam baris (total 42 hari) akan cukup untuk menangani semua kasus.
Tapi tidak semua bulan memiliki 31 hari! Kita dapat menghindari komplikasi yang timbul dari itu dengan menggunakan fungsionalitas tanggal bawaan Android, menghindari kebutuhan untuk menghitung sendiri jumlah hari.
Seperti disebutkan sebelumnya, fungsi tanggal yang disediakan oleh kelas Calendar
membuat implementasinya cukup mudah. Dalam komponen kami, fungsi updateCalendar()
mengimplementasikan logika ini:
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. Dapat Disesuaikan di Hati
Karena komponen yang bertanggung jawab untuk menampilkan hari-hari individual adalah GridView
, tempat yang baik untuk menyesuaikan bagaimana hari-hari ditampilkan adalah Adapter
, karena ini bertanggung jawab untuk menyimpan data dan meningkatkan tampilan untuk masing-masing sel grid.
Untuk contoh ini, kami akan membutuhkan yang berikut dari CalendearView
kami:
- Hari ini harus dalam teks biru tebal .
- Hari di luar bulan berjalan harus diabu-abukan .
- Hari dengan acara harus menampilkan ikon khusus.
- Header kalender harus berubah warna tergantung pada musim (Musim Panas, Gugur, Musim Dingin, Musim Semi).
Tiga persyaratan pertama mudah dicapai dengan mengubah atribut teks dan sumber daya latar belakang. Mari kita terapkan CalendarAdapter
untuk melakukan tugas ini. Cukup sederhana sehingga bisa menjadi kelas anggota di CalendarView
. Dengan mengganti fungsi getView()
, kita dapat mencapai persyaratan di atas:
@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; }
Persyaratan desain akhir membutuhkan sedikit lebih banyak pekerjaan. Pertama, mari tambahkan warna untuk empat musim di /res/values/colors.xml
:

<color name="summer">#44eebd82</color> <color name="fall">#44d8d27e</color> <color name="winter">#44a1c1da</color> <color name="spring">#448da64b</color>
Kemudian, mari kita gunakan array untuk menentukan musim untuk setiap bulan (dengan asumsi belahan bumi utara, untuk kesederhanaan; maaf Australia!). Di CalendarView
kami menambahkan variabel anggota berikut:
// 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};
Dengan cara ini, memilih warna yang sesuai dilakukan dengan memilih musim yang sesuai ( monthSeason[currentMonth]
) dan kemudian memilih warna yang sesuai ( rainbow[monthSeason[currentMonth]
), ini ditambahkan ke updateCalendar()
untuk memastikan warna yang sesuai dipilih setiap kali kalender diubah.
// 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));
Dengan itu, kita mendapatkan hasil sebagai berikut:
Catatan Penting karena cara HashSet
membandingkan objek, pemeriksaan di atas eventDays.contains(date)
di updateCalendar()
tidak akan menghasilkan nilai true untuk objek tanggal kecuali jika objek tersebut sama persis. Itu tidak melakukan pemeriksaan khusus untuk tipe data Date
. Untuk mengatasinya, cek ini diganti dengan kode berikut:
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. Terlihat Jelek dalam Waktu Desain
Pilihan Android untuk placeholder dalam waktu desain dapat dipertanyakan. Untungnya, Android sebenarnya membuat instance komponen kita untuk merendernya di desainer UI, dan kita bisa memanfaatkan ini dengan memanggil updateCalendar()
di konstruktor komponen. Dengan cara ini komponen akan benar-benar masuk akal dalam waktu desain.
Jika menginisialisasi komponen membutuhkan banyak pemrosesan atau memuat banyak data, ini dapat memengaruhi kinerja IDE. Dalam hal ini, Android menyediakan fungsi bagus yang disebut isInEditMode()
yang dapat digunakan untuk membatasi jumlah data yang digunakan saat komponen benar-benar dipakai di desainer UI. Misalnya, jika ada banyak acara yang akan dimuat ke dalam CalendarView
, kita dapat menggunakan isInEditMode()
di dalam fungsi updateCalendar()
untuk menyediakan daftar acara kosong/terbatas dalam mode desain, dan memuat yang asli sebaliknya.
6. Memanggil Komponen
Komponen dapat disertakan dalam file layout XML (contoh penggunaan dapat ditemukan di activity_main.xml
):
<samples.aalamir.customcalendar.CalendarView android: android:layout_width="match_parent" android:layout_height="wrap_content"/>
Dan diambil untuk berinteraksi dengan setelah tata letak dimuat:
HashSet<Date> events = new HashSet<>(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events);
Kode di atas membuat HashSet
acara, menambahkan hari ini, lalu meneruskannya ke CalendarView
. Akibatnya, CalendarView
akan menampilkan hari ini dengan warna biru tebal dan juga menempatkan penanda acara di atasnya:
CalendarView
menampilkan acara
7. Menambahkan Atribut
Fasilitas lain yang disediakan oleh Android adalah untuk menetapkan atribut ke komponen kustom. Hal ini memungkinkan pengembang Android menggunakan komponen untuk memilih pengaturan melalui XML tata letak dan melihat hasilnya langsung di perancang UI, bukan harus menunggu dan melihat tampilan CalendarView
saat runtime. Mari tambahkan kemampuan untuk mengubah tampilan format tanggal di komponen, misalnya untuk mengeja nama lengkap bulan, bukan singkatan tiga huruf.
Untuk melakukan ini, diperlukan langkah-langkah berikut:
- Nyatakan atributnya. Sebut saja
dateFormat
dan berikan tipe datastring
. Tambahkan ke/res/values/attrs.xml
:
<resources> <declare-styleable name="CalendarDateElement"> <attr name="dateFormat" format="string"/> </declare-styleable> </resources>
- Gunakan atribut di layout yang menggunakan komponen, dan beri nilai
"MMMM yyyy"
:
<samples.aalamir.customcalendar.CalendarView xmlns:calendarNS="http://schemas.android.com/apk/res/samples.aalamir.customcalendar" android: android:layout_width="match_parent" android:layout_height="wrap_content" calendarNS:dateFormat="MMMM yyyy"/>
- Terakhir, minta komponen menggunakan nilai atribut:
TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CalendarView); dateFormat = ta.getString(R.styleable.CalendarView_dateFormat);
] Bangun proyek dan Anda akan melihat perubahan tanggal yang ditampilkan di desainer UI untuk menggunakan nama lengkap bulan tersebut, seperti “Juli 2015”. Coba berikan nilai yang berbeda dan lihat apa yang terjadi.
CalendarView
.
8. Berinteraksi Dengan Komponen
Sudahkah Anda mencoba menekan pada hari tertentu? Elemen UI bagian dalam di komponen kami masih berperilaku dengan cara normal yang diharapkan dan akan memicu peristiwa sebagai respons terhadap tindakan pengguna. Jadi, bagaimana kita menangani peristiwa-peristiwa itu?
Jawabannya melibatkan dua bagian:
- Tangkap peristiwa di dalam komponen, dan
- Laporkan peristiwa ke induk komponen (bisa berupa
Fragment
,Activity
, atau bahkan komponen lain).
Bagian pertama cukup sederhana. Misalnya, untuk menangani item kisi yang ditekan lama, kami menetapkan pendengar yang sesuai di kelas komponen kami:
// 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; } });
Ada beberapa metode untuk melaporkan peristiwa. Yang langsung dan sederhana adalah menyalin cara Android melakukannya: ia menyediakan antarmuka ke peristiwa komponen yang diimplementasikan oleh induk komponen ( eventHandler
dalam cuplikan kode di atas).
Fungsi antarmuka dapat melewatkan data apa pun yang relevan dengan aplikasi. Dalam kasus kami, antarmuka perlu mengekspos satu event handler, yang melewati tanggal untuk hari yang ditekan. Antarmuka berikut didefinisikan di CalendarView
:
public interface EventHandler { void onDayLongPress(Date date); }
Implementasi yang disediakan oleh induk dapat diberikan ke tampilan kalender melalui setEventHandler()
. Berikut adalah contoh penggunaan dari `MainActivity.java':
@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(); } }); }
Menekan lama suatu hari akan mengaktifkan acara yang ditekan lama yang ditangkap dan ditangani oleh GridView
dan dilaporkan dengan memanggil onDayLongPress()
dalam implementasi yang disediakan, yang, pada gilirannya, akan menampilkan tanggal hari yang ditekan di layar:
Cara lain yang lebih canggih untuk menangani ini adalah dengan menggunakan Intents
dan BroadcastReceivers
Android. Ini sangat membantu ketika beberapa komponen perlu diberi tahu tentang acara kalender. Misalnya, jika menekan satu hari di kalender memerlukan teks untuk ditampilkan dalam Activity
dan file untuk diunduh oleh Service
latar belakang.
Menggunakan pendekatan sebelumnya akan mengharuskan Activity
untuk menyediakan EventHandler
ke komponen, menangani acara dan kemudian meneruskannya ke Service
. Alih-alih, meminta komponen menyiarkan Intent
dan Activity
serta Service
menerimanya melalui BroadcastReceivers
mereka sendiri tidak hanya membuat hidup lebih mudah tetapi juga membantu memisahkan Activity
dan Service
yang dimaksud.
Kesimpulan
Jadi, inilah cara Anda membuat komponen kustom Anda sendiri dalam beberapa langkah sederhana:
- Buat tata letak XML dan gaya sesuai kebutuhan Anda.
- Turunkan kelas komponen Anda dari komponen induk yang sesuai, sesuai dengan tata letak XML Anda.
- Tambahkan logika bisnis komponen Anda.
- Gunakan atribut untuk memungkinkan pengguna mengubah perilaku komponen.
- Untuk mempermudah penggunaan komponen di desainer UI, gunakan fungsi
isInEditMode()
Android.
Dalam artikel ini, kami membuat tampilan kalender sebagai contoh, terutama karena tampilan kalender stok, dalam banyak hal, kurang. Namun, Anda sama sekali tidak terbatas pada jenis komponen apa yang dapat Anda buat. Anda dapat menggunakan teknik yang sama untuk membuat apa pun yang Anda butuhkan, langit adalah batasnya!
Terima kasih telah membaca panduan ini, semoga sukses dalam upaya pengkodean Anda!