Android Özelleştirme: İstediğinizi Yapan Bir UI Bileşeni Nasıl Oluşturulur

Yayınlanan: 2022-03-11

Geliştiricilerin, hedefledikleri platform tarafından sağlanmayan veya gerçekten sağlanan, ancak belirli bir özelliği veya davranışı olmayan bir UI bileşenine ihtiyaç duymaları alışılmadık bir durum değildir. Her iki senaryoya da yanıt, özel bir UI bileşenidir.

Android UI modeli, doğası gereği özelleştirilebilir olup, Android özelleştirme, test etme ve çeşitli şekillerde özel UI bileşenleri oluşturma olanağı sunar:

  • Mevcut bir bileşeni (yani TextView , ImageView , vb.) devralın ve gerekli işlevselliği ekleyin/geçersiz kılın. Örneğin, ImageView öğesini devralan, görüntülenen görüntüyü bir daireyle sınırlamak için CircleImageView () işlevini geçersiz onDraw() ve harici bellekten bir görüntü yüklemek için bir loadFromFile() işlevi ekleyen bir CircleImageView.

  • Birkaç bileşenden bir bileşik bileşen oluşturun . Bu yaklaşım, bileşenlerin ekranda nasıl düzenlendiğini kontrol etmek için genellikle Düzenlerden yararlanır. Örneğin, LabeledEditText yatay yönlendirmeyle devralan ve hem etiket işlevi gören bir TextView hem de metin giriş alanı işlevi gören bir LinearLayout içeren bir EditText .

    Bu yaklaşım öncekini de kullanabilir, yani dahili bileşenler yerel veya özel olabilir.

  • En çok yönlü ve en karmaşık yaklaşım, kendi kendine çizilen bir bileşen oluşturmaktır . Bu durumda, bileşen genel View sınıfını devralır ve düzenini belirlemek için onMeasure( onDraw() , içeriğini görüntülemek için onMeasure() gibi işlevleri geçersiz kılar. Bu şekilde oluşturulan bileşenler genellikle büyük ölçüde Android'in 2D çizim API'sine bağlıdır.

Android Özelleştirme Vaka Çalışması: CalendarView

Android, yerel bir CalendarView bileşeni sağlar. İyi bir performans sergiliyor ve tam bir ayı görüntüleyerek ve geçerli günü vurgulayarak herhangi bir takvim bileşeninden beklenen minimum işlevselliği sağlıyor. Bazıları bunun da iyi göründüğünü söyleyebilir, ancak yalnızca yerel bir görünüm arıyorsanız ve nasıl göründüğünü kişiselleştirmekle ilgilenmiyorsanız.

Örneğin, CalendarView bileşeni, belirli bir günün nasıl işaretlendiğini veya hangi arka plan renginin kullanılacağını değiştirmenin hiçbir yolunu sağlamaz. Ayrıca, örneğin özel bir durumu işaretlemek için herhangi bir özel metin veya grafik eklemenin bir yolu yoktur. Kısacası, bileşen şöyle görünür ve neredeyse hiçbir şey değiştirilemez:

Ekran görüntüsü
AppCompact.Light temasında CalendarView .

Kendin Yap

Peki, kişi kendi takvim görünümünü nasıl oluşturur? Yukarıdaki yaklaşımlardan herhangi biri işe yarayacaktır. Bununla birlikte, pratiklik genellikle üçüncü seçeneği (2D grafikler) ekarte edecek ve bizi diğer iki yöntemle baş başa bırakacaktır ve bu makalede her ikisinin bir karışımını kullanacağız.

Devam etmek için kaynak kodunu burada bulabilirsiniz.

1. Bileşen Düzeni

İlk olarak, bileşenin nasıl göründüğüyle başlayalım. Her şeyi basitleştirmek için günleri bir ızgarada ve en üstte “gelecek ay” ve “önceki ay” düğmeleriyle birlikte ayın adını görüntüleyelim.

Ekran görüntüsü
Özel takvim görünümü.

Bu düzen, control_calendar.xml dosyasında aşağıdaki gibi tanımlanmıştır. Bazı tekrarlayan işaretlemelerin ... ile kısaltıldığına dikkat edin:

 <?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. Bileşen Sınıfı

Önceki düzen, bir Activity veya Fragment olduğu gibi dahil edilebilir ve iyi çalışır. Ancak bunu bağımsız bir UI bileşeni olarak kapsüllemek, kod tekrarını önleyecek ve her modülün bir sorumluluğu üstlendiği modüler bir tasarıma izin verecektir.

UI bileşenimiz, XML düzen dosyasının köküyle eşleşmesi için bir LinearLayout olacaktır. Koddan yalnızca önemli kısımların gösterildiğine dikkat edin. Bileşenin uygulaması CalendarView.java bulunur:

 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); } }

Kod oldukça basittir. Oluşturulduktan sonra bileşen, XML düzenini şişirir ve bu yapıldığında, daha sonra daha kolay erişim için dahili kontrolleri yerel değişkenlere atar.

3. Biraz Mantık Gerekli

Bu bileşenin gerçekten bir takvim görünümü gibi davranmasını sağlamak için bazı iş mantığı sırayla. İlk başta karmaşık görünebilir, ama aslında çok fazla bir şey yok. Onu parçalayalım:

  1. Takvim görünümü yedi gün genişliğindedir ve tüm ayların ilk satırda bir yerden başlaması garanti edilir.

  2. Önce ayın hangi pozisyondan başladığını bulmamız, ardından 0 pozisyonuna ulaşana kadar ondan önceki tüm pozisyonları bir önceki aydan (30, 29, 28.. vb.) sayılarla doldurmamız gerekiyor.

  3. Ardından içinde bulunulan aya ait günleri (1, 2, 3… vb.) dolduruyoruz.

  4. Bundan sonra bir sonraki ayın günleri gelir (yine 1, 2, 3.. vb), ancak bu sefer sadece gridin son satır(lar)ında kalan pozisyonları dolduruyoruz.

Aşağıdaki diyagram bu adımları göstermektedir:

Ekran görüntüsü
Özel takvim görünümü iş mantığı.

Izgaranın genişliği zaten yedi hücre olarak belirtilmiş, haftalık bir takvimi ifade ediyor, peki ya yüksekliği? Izgara için en büyük boyut, ilk satırdaki son hücre olan ve tam olarak görüntülenmesi için 5 satıra daha ihtiyaç duyacak olan Cumartesi günü başlayan 31 günlük bir ayın en kötü durum senaryosu ile belirlenebilir. Bu nedenle, takvimi altı satır (toplam 42 gün) gösterecek şekilde ayarlamak tüm durumları ele almak için yeterli olacaktır.

Ancak tüm ayların 31 günü yoktur! Android'in yerleşik tarih işlevini kullanarak, gün sayısını kendimiz bulma ihtiyacını ortadan kaldırarak bundan kaynaklanan komplikasyonları önleyebiliriz.

Daha önce de belirtildiği gibi, Calendar sınıfı tarafından sağlanan tarih işlevleri, uygulamayı oldukça basit hale getirir. Bileşenimizde updateCalendar() işlevi şu mantığı uygular:

 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. Kalpten Özelleştirilebilir

Bireysel günleri görüntülemekten sorumlu bileşen bir GridView olduğundan, günlerin nasıl görüntüleneceğini özelleştirmek için iyi bir yer Adapter , çünkü verileri tutmaktan ve bireysel ızgara hücreleri için görünümleri şişirmekten sorumludur.

Bu örnek için, CalendearView aşağıdakileri isteyeceğiz:

  • Günümüz koyu mavi metinle yazılmalıdır .
  • Geçerli ayın dışındaki günler gri renkte olmalıdır.
  • Etkinliğin olduğu günler özel bir simge göstermelidir.
  • Takvim başlığı mevsime göre (Yaz, Sonbahar, Kış, İlkbahar) renkleri değiştirmelidir.

İlk üç gereksinim, metin niteliklerini ve arka plan kaynaklarını değiştirerek elde etmek kolaydır. Bu görevi gerçekleştirmek için bir CalendarAdapter uygulayalım. CalendarView bir üye sınıf olabilmesi yeterince basittir. getView() işlevini geçersiz kılarak yukarıdaki gereksinimleri elde edebiliriz:

 @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; }

Nihai tasarım gereksinimi biraz daha fazla çalışma gerektirir. İlk olarak, /res/values/colors.xml dört mevsim için renkleri ekleyelim:

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

Ardından, her ay için mevsimi tanımlamak için bir dizi kullanalım (basitlik için kuzey yarımküreyi varsayarak; üzgünüm Avustralya!). CalendarView aşağıdaki üye değişkenleri ekliyoruz:

 // 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};

Bu şekilde, uygun bir renk seçimi, uygun mevsim ( monthSeason[currentMonth] ) seçilerek ve ardından ilgili renk seçilerek ( rainbow[monthSeason[currentMonth] ) yapılır, bu, uygun rengin seçildiğinden emin olmak için updateCalendar() eklenir. takvim her değiştiğinde.

 // 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));

Bununla, aşağıdaki sonucu elde ederiz:

Android Özelleştirme
Başlık rengi mevsime göre değişir.

Önemli Not , HashSet nesneleri karşılaştırma biçiminden dolayı, updateCalendar updateCalendar() ) içindeki yukarıdaki eventDays.contains(date) kontrolü, tam olarak aynı olmadıkça tarih nesneleri için doğru sonuç vermeyecektir. Date veri türü için herhangi bir özel kontrol yapmaz. Bu soruna geçici bir çözüm bulmak için bu denetim aşağıdaki kodla değiştirilir:

 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. Tasarım Zamanında Çirkin Görünüyor

Tasarım zamanında yer tutucular için Android'in seçimi sorgulanabilir. Neyse ki, Android aslında bileşenimizi UI tasarımcısında işlemek için başlatır ve bunu bileşen yapıcısında updateCalendar() çağırarak kullanabiliriz. Bu şekilde bileşen, tasarım zamanında gerçekten anlamlı olacaktır.

Ekran görüntüsü

Bileşenin başlatılması çok fazla işlem gerektiriyorsa veya çok fazla veri yüklüyse, IDE'nin performansını etkileyebilir. Bu durumda, Android, bileşen gerçekten UI tasarımcısında başlatıldığında kullanılan veri miktarını sınırlamak için kullanılabilecek isInEditMode() adlı şık bir işlev sağlar. Örneğin, CalendarView içine yüklenecek çok sayıda olay varsa, tasarım modunda boş/sınırlı bir olay listesi sağlamak için updateCalendar() isInEditMode() kullanabilir ve aksi takdirde gerçek olanı yükleyebiliriz.

6. Bileşeni Çağırma

Bileşen, XML düzen dosyalarına dahil edilebilir (bir örnek kullanım, activity_main.xml içinde bulunabilir):

 <samples.aalamir.customcalendar.CalendarView android: android:layout_width="match_parent" android:layout_height="wrap_content"/>

Düzen yüklendikten sonra etkileşim kurulmak üzere alındı:

 HashSet<Date> events = new HashSet<>(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events);

Yukarıdaki kod bir HashSet etkinlik oluşturur, buna geçerli günü ekler ve ardından onu CalendarView öğesine iletir. Sonuç olarak, CalendarView geçerli günü koyu mavi renkle gösterecek ve üzerine olay işaretçisini koyacaktır:

Ekran görüntüsü
Bir olayı gösteren CalendarView

7. Nitelik Ekleme

Android tarafından sağlanan bir diğer özellik, özel bir bileşene nitelik atamaktır. Bu, bileşeni kullanan Android geliştiricilerinin, CalendarView çalışma zamanında nasıl göründüğünü beklemek ve görmek yerine, XML düzeni aracılığıyla ayarları seçmelerine ve sonucu hemen UI tasarımcısında görmelerine olanak tanır. Bileşendeki tarih biçimi görüntüsünü değiştirme yeteneğini ekleyelim, örneğin üç harfli kısaltma yerine ayın tam adını heceleyerek.

Bunu yapmak için aşağıdaki adımlar gereklidir:

  • Özelliği bildirin. Buna dateFormat ve string veri tipini verelim. /res/values/attrs.xml ekleyin:
 <resources> <declare-styleable name="CalendarDateElement"> <attr name="dateFormat" format="string"/> </declare-styleable> </resources>
  • Bileşeni kullanan düzende özniteliği kullanın ve ona "MMMM yyyy" değerini verin:
 <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"/>
  • Son olarak, bileşenin öznitelik değerinden yararlanmasını sağlayın:
 TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CalendarView); dateFormat = ta.getString(R.styleable.CalendarView_dateFormat);

] Projeyi oluşturun ve "Temmuz 2015" gibi ayın tam adını kullanmak için UI tasarımcısında görüntülenen tarih değişikliklerini fark edeceksiniz. Farklı değerler sağlamayı deneyin ve ne olduğunu görün.

Ekran görüntüsü
CalendarView özniteliklerini değiştirme.

8. Bileşenle Etkileşim

Belirli bir günde basmayı denediniz mi? Bileşenimizdeki iç UI öğeleri, normal beklenen şekilde davranmaya devam eder ve kullanıcı eylemlerine yanıt olarak olayları tetikler. Peki, bu olayları nasıl ele alıyoruz?

Cevap iki kısımdan oluşur:

  • Bileşen içindeki olayları yakalayın ve
  • Olayları bileşenin üst öğesine bildirin (bir Fragment , bir Activity veya hatta başka bir bileşen olabilir).

İlk kısım oldukça basit. Örneğin, uzun basılan ızgara öğelerini işlemek için bileşen sınıfımıza karşılık gelen bir dinleyici atarız:

 // 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; } });

Olayları raporlamak için birkaç yöntem vardır. Doğrudan ve basit olanı, Android'in yaptığı gibi kopyalamaktır: bileşenin üst öğesi (yukarıdaki kod parçacığında eventHandler ) tarafından uygulanan bileşenin olaylarına bir arabirim sağlar.

Arayüzün işlevleri, uygulamayla ilgili herhangi bir veri iletilebilir. Bizim durumumuzda, arabirimin, basılan gün için tarihi geçen bir olay işleyicisini ortaya çıkarması gerekir. Aşağıdaki arayüz CalendarView 'da tanımlanmıştır:

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

Ebeveyn tarafından sağlanan uygulama, bir setEventHandler() aracılığıyla takvim görünümüne sağlanabilir. İşte `MainActivity.java'dan örnek kullanım:

 @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(); } }); }

Bir güne uzun süre basmak, GridView tarafından yakalanan ve işlenen ve sağlanan uygulamada onDayLongPress() çağrılarak raporlanan ve sırayla basılan günün tarihini ekranda gösterecek olan uzun süreli bir basış olayını tetikleyecektir:

Ekran görüntüsü

Bunu halletmenin bir başka, daha gelişmiş yolu, Android'in Intents ve BroadcastReceivers kullanmaktır. Bu, özellikle birkaç bileşenin takvim etkinliğinden haberdar edilmesi gerektiğinde yararlıdır. Örneğin, takvimde bir güne basmak, bir Activity bir metnin görüntülenmesini ve bir arka plan Service tarafından bir dosyanın indirilmesini gerektiriyorsa.

Önceki yaklaşımın kullanılması, Activity bileşene bir EventHandler sağlamasını, olayı işlemesini ve ardından bunu Service geçirmesini gerektirecektir. Bunun yerine, bileşenin bir Intent yayınlaması ve hem Activity hem de Service bunu kendi BroadcastReceivers aracılığıyla kabul etmesi yalnızca hayatı kolaylaştırmakla kalmaz, aynı zamanda söz konusu Activity ve Service ayırmaya da yardımcı olur.

Çözüm

Android özelleştirmesinin müthiş gücünü görün!
Cıvıldamak

Böylece, birkaç basit adımda kendi özel bileşeninizi bu şekilde oluşturabilirsiniz:

  • XML düzenini oluşturun ve ihtiyaçlarınıza göre biçimlendirin.
  • XML düzeninize göre, bileşen sınıfınızı uygun üst bileşenden türetin.
  • Bileşeninizin iş mantığını ekleyin.
  • Kullanıcıların bileşenin davranışını değiştirmesini sağlamak için öznitelikleri kullanın.
  • UI tasarımcısında bileşeni kullanmayı kolaylaştırmak için Android'in isInEditMode() işlevini kullanın.

Bu makalede, temel olarak stok takvim görünümü birçok yönden eksik olduğu için örnek olarak bir takvim görünümü oluşturduk. Ancak, ne tür bileşenler oluşturabileceğiniz konusunda hiçbir şekilde sınırlı değilsiniz. Aynı tekniği ihtiyacınız olan her şeyi yaratmak için kullanabilirsiniz, sınır gökyüzünde!

Bu kılavuzu okuduğunuz için teşekkür eder, kodlama çalışmalarınızda size iyi şanslar dilerim!