Android-Anpassung: So erstellen Sie eine UI-Komponente, die tut, was Sie wollen

Veröffentlicht: 2022-03-11

Es ist nicht ungewöhnlich, dass Entwickler eine UI-Komponente benötigen, die entweder nicht von der Plattform bereitgestellt wird, auf die sie abzielen, oder die tatsächlich bereitgestellt wird, der jedoch eine bestimmte Eigenschaft oder ein bestimmtes Verhalten fehlt. Die Antwort auf beide Szenarien ist eine benutzerdefinierte UI-Komponente.

Das Android-UI-Modell ist von Natur aus anpassbar und bietet die Möglichkeit, Android anzupassen, zu testen und benutzerdefinierte UI-Komponenten auf verschiedene Weise zu erstellen:

  • Erben Sie eine vorhandene Komponente (z. B. TextView , ImageView usw.) und fügen Sie die erforderliche Funktionalität hinzu bzw. überschreiben Sie sie. Beispielsweise eine CircleImageView, die CircleImageView erbt, die Funktion ImageView onDraw() überschreibt, um das angezeigte Bild auf einen Kreis zu beschränken, und eine Funktion loadFromFile() hinzufügt, um ein Bild aus dem externen Speicher zu laden.

  • Erstellen Sie eine zusammengesetzte Komponente aus mehreren Komponenten. Dieser Ansatz nutzt normalerweise Layouts, um zu steuern, wie die Komponenten auf dem Bildschirm angeordnet werden. Beispielsweise ein LabeledEditText , das LinearLayout mit horizontaler Ausrichtung erbt und sowohl eine TextView enthält, die als Beschriftung fungiert, als auch eine EditText , die als Texteingabefeld fungiert.

    Dieser Ansatz könnte auch den vorherigen verwenden, dh die internen Komponenten könnten nativ oder kundenspezifisch sein.

  • Der vielseitigste und komplexeste Ansatz besteht darin , ein selbst gezeichnetes Bauteil zu erstellen . In diesem Fall erbt die Komponente die generische View -Klasse und überschreibt Funktionen wie onMeasure() , um ihr Layout zu bestimmen, onDraw() , um ihren Inhalt anzuzeigen usw. Auf diese Weise erstellte Komponenten hängen normalerweise stark von der 2D-Zeichnungs-API von Android ab.

Fallstudie zur Android-Anpassung: Die CalendarView

Android bietet eine native CalendarView Komponente. Es funktioniert gut und bietet die Mindestfunktionalität, die von jeder Kalenderkomponente erwartet wird, indem es einen ganzen Monat anzeigt und den aktuellen Tag hervorhebt. Einige mögen sagen, dass es auch gut aussieht, aber nur, wenn Sie sich für ein natives Aussehen entscheiden und kein Interesse daran haben, das Aussehen überhaupt anzupassen.

Die CalendarView -Komponente bietet beispielsweise keine Möglichkeit, die Markierung eines bestimmten Tages oder die zu verwendende Hintergrundfarbe zu ändern. Es gibt auch keine Möglichkeit, benutzerdefinierte Texte oder Grafiken hinzuzufügen, um beispielsweise einen besonderen Anlass zu markieren. Kurz gesagt, die Komponente sieht so aus, und es kann fast nichts geändert werden:

Bildschirmfoto
CalendarView im AppCompact.Light .

Mach dein eigenes

Wie also erstellt man seine eigene Kalenderansicht? Jeder der oben genannten Ansätze würde funktionieren. Aus praktischen Gründen wird die dritte Option (2D-Grafik) jedoch normalerweise ausgeschlossen und uns mit den beiden anderen Methoden belassen, und wir werden in diesem Artikel eine Mischung aus beiden verwenden.

Um mitzumachen, finden Sie hier den Quellcode.

1. Das Komponentenlayout

Beginnen wir zunächst damit, wie die Komponente aussieht. Um die Sache einfach zu halten, lassen Sie uns Tage in einem Raster anzeigen und oben den Namen des Monats zusammen mit den Schaltflächen „nächster Monat“ und „vorheriger Monat“.

Bildschirmfoto
Benutzerdefinierte Kalenderansicht.

Dieses Layout ist in der Datei control_calendar.xml wie folgt definiert. Beachten Sie, dass einige sich wiederholende Markups mit ... abgekürzt wurden:

 <?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. Die Komponentenklasse

Das vorherige Layout kann unverändert in eine Activity oder ein Fragment werden und funktioniert problemlos. Die Kapselung als eigenständige UI-Komponente verhindert jedoch Codewiederholungen und ermöglicht ein modulares Design, bei dem jedes Modul eine Verantwortung übernimmt.

Unsere UI-Komponente wird ein LinearLayout , um dem Stamm der XML-Layoutdatei zu entsprechen. Beachten Sie, dass nur die wichtigen Teile des Codes angezeigt werden. Die Implementierung der Komponente befindet sich in 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); } }

Der Code ist ziemlich einfach. Bei der Erstellung bläst die Komponente das XML-Layout auf, und wenn dies erledigt ist, weist sie die internen Steuerelemente lokalen Variablen zu, um später leichter darauf zugreifen zu können.

3. Etwas Logik ist erforderlich

Damit sich diese Komponente tatsächlich wie eine Kalenderansicht verhält, ist eine gewisse Geschäftslogik angebracht. Es mag auf den ersten Blick kompliziert erscheinen, aber es ist wirklich nicht viel dahinter. Lassen Sie es uns aufschlüsseln:

  1. Die Kalenderansicht ist sieben Tage breit und es ist garantiert, dass alle Monate irgendwo in der ersten Reihe beginnen.

  2. Zuerst müssen wir herausfinden, an welcher Position der Monat beginnt, und dann alle Positionen davor mit den Zahlen des Vormonats (30, 29, 28 usw.) füllen, bis wir die Position 0 erreichen.

  3. Dann füllen wir die Tage für den aktuellen Monat aus (1, 2, 3 … usw.).

  4. Danach kommen die Tage für den nächsten Monat (wieder 1, 2, 3 ... usw.), aber dieses Mal füllen wir nur die verbleibenden Positionen in der/den letzten Reihe(n) des Rasters.

Das folgende Diagramm veranschaulicht diese Schritte:

Bildschirmfoto
Geschäftslogik für benutzerdefinierte Kalenderansichten.

Die Breite des Rasters ist bereits auf sieben Zellen festgelegt, was einen Wochenkalender darstellt, aber wie sieht es mit der Höhe aus? Die größte Größe für das Raster kann durch das Worst-Case-Szenario eines 31-Tage-Monats bestimmt werden, der an einem Samstag beginnt, der die letzte Zelle in der ersten Zeile ist, und es werden 5 weitere Zeilen benötigt, um vollständig angezeigt zu werden. Es reicht also aus, den Kalender so einzustellen, dass er sechs Zeilen (insgesamt 42 Tage) anzeigt, um alle Fälle zu behandeln.

Aber nicht alle Monate haben 31 Tage! Wir können Komplikationen vermeiden, die sich daraus ergeben, indem wir die integrierte Datumsfunktion von Android verwenden, sodass wir die Anzahl der Tage nicht selbst herausfinden müssen.

Wie bereits erwähnt, machen die von der Calendar -Klasse bereitgestellten Datumsfunktionen die Implementierung ziemlich einfach. In unserer Komponente implementiert die Funktion updateCalendar() diese Logik:

 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. Im Herzen anpassbar

Da die Komponente, die für die Anzeige einzelner Tage verantwortlich ist, ein GridView ist, ist der Adapter ein guter Ort, um die Anzeige der Tage anzupassen, da er für das Speichern der Daten und das Aufblasen von Ansichten für einzelne Rasterzellen verantwortlich ist.

Für dieses Beispiel benötigen wir Folgendes von unserer CalendearView :

  • Der heutige Tag sollte in fettem blauem Text stehen.
  • Tage außerhalb des aktuellen Monats sollten ausgegraut sein .
  • Tage mit einem Ereignis sollten ein spezielles Symbol anzeigen.
  • Die Kalenderkopfzeile sollte je nach Jahreszeit (Sommer, Herbst, Winter, Frühling) die Farbe ändern.

Die ersten drei Anforderungen sind einfach zu erfüllen, indem Textattribute und Hintergrundressourcen geändert werden. Lassen Sie uns einen CalendarAdapter implementieren, um diese Aufgabe auszuführen. Es ist einfach genug, dass es sich um eine Member-Klasse in CalendarView kann. Durch Überschreiben der Funktion getView() können wir die oben genannten Anforderungen erfüllen:

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

Die endgültige Designanforderung erfordert etwas mehr Arbeit. Zuerst fügen wir die Farben für die vier Jahreszeiten in /res/values/colors.xml :

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

Dann verwenden wir ein Array, um die Jahreszeit für jeden Monat zu definieren (der Einfachheit halber nehmen wir die nördliche Hemisphäre an; sorry Australien!). In CalendarView fügen wir die folgenden Member-Variablen hinzu:

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

Auf diese Weise wird eine geeignete Farbe ausgewählt, indem die entsprechende Jahreszeit ( monthSeason[currentMonth] ) und dann die entsprechende Farbe ( rainbow[monthSeason[currentMonth] ) ausgewählt wird. Diese wird zu updateCalendar() hinzugefügt, um sicherzustellen, dass die richtige Farbe ausgewählt wird immer wenn der Kalender geändert wird.

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

Damit erhalten wir folgendes Ergebnis:

Android-Anpassung
Die Kopffarbe ändert sich je nach Saison.

Wichtiger Hinweis Aufgrund der Art und Weise, wie HashSet Objekte vergleicht, ergibt die obige Prüfung eventDays.contains(date) in updateCalendar() nicht wahr für Datumsobjekte, es sei denn, sie sind genau identisch. Es führt keine besonderen Prüfungen für den Datentyp Date durch. Um dies zu umgehen, wird diese Prüfung durch den folgenden Code ersetzt:

 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. Es sieht in der Designzeit hässlich aus

Die Wahl von Android für Platzhalter in der Entwurfszeit kann fragwürdig sein. Glücklicherweise instanziiert Android unsere Komponente tatsächlich, um sie im UI-Designer zu rendern, und wir können dies ausnutzen, indem wir updateCalendar() im Komponentenkonstruktor aufrufen. Auf diese Weise macht die Komponente in der Entwurfszeit tatsächlich Sinn.

Bildschirmfoto

Wenn die Initialisierung der Komponente viel Verarbeitung erfordert oder viele Daten lädt, kann dies die Leistung der IDE beeinträchtigen. In diesem Fall stellt Android eine raffinierte Funktion namens isInEditMode() , die verwendet werden kann, um die Datenmenge zu begrenzen, die verwendet wird, wenn die Komponente tatsächlich im UI-Designer instanziiert wird. Wenn beispielsweise viele Ereignisse in die CalendarView geladen werden müssen, können wir isInEditMode() innerhalb der Funktion updateCalendar() verwenden, um eine leere/eingeschränkte Ereignisliste im Entwurfsmodus bereitzustellen und andernfalls die echte zu laden.

6. Aufrufen der Komponente

Die Komponente kann in XML-Layoutdateien eingebunden werden (eine Beispielverwendung finden Sie in activity_main.xml ):

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

Und abgerufen, um mit ihm zu interagieren, sobald das Layout geladen ist:

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

Der obige Code erstellt ein HashSet von Ereignissen, fügt ihm den aktuellen Tag hinzu und übergibt ihn dann an CalendarView . Als Ergebnis zeigt die CalendarView den aktuellen Tag in fettem Blau an und setzt auch die Ereignismarkierung darauf:

Bildschirmfoto
CalendarView zeigt ein Ereignis an

7. Hinzufügen von Attributen

Eine weitere von Android bereitgestellte Funktion besteht darin, einer benutzerdefinierten Komponente Attribute zuzuweisen. Dadurch können Android-Entwickler, die die Komponente verwenden, Einstellungen über das Layout-XML auswählen und das Ergebnis sofort im UI-Designer sehen, anstatt abwarten zu müssen, wie die CalendarView zur Laufzeit aussieht. Lassen Sie uns die Möglichkeit hinzufügen, die Anzeige des Datumsformats in der Komponente zu ändern, um beispielsweise den vollständigen Namen des Monats anstelle der dreibuchstabigen Abkürzung zu buchstabieren.

Dazu sind folgende Schritte notwendig:

  • Deklarieren Sie das Attribut. Nennen wir es dateFormat und geben ihm den Datentyp string . Fügen Sie es zu /res/values/attrs.xml :
 <resources> <declare-styleable name="CalendarDateElement"> <attr name="dateFormat" format="string"/> </declare-styleable> </resources>
  • Verwenden Sie das Attribut im Layout, das die Komponente verwendet, und geben Sie ihm den Wert "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"/>
  • Lassen Sie die Komponente schließlich den Attributwert verwenden:
 TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CalendarView); dateFormat = ta.getString(R.styleable.CalendarView_dateFormat);

] Erstellen Sie das Projekt und Sie werden feststellen, dass sich das angezeigte Datum im UI-Designer ändert, um den vollständigen Namen des Monats zu verwenden, z. B. „Juli 2015“. Versuchen Sie, andere Werte anzugeben, und sehen Sie, was passiert.

Bildschirmfoto
Ändern der CalendarView Attribute.

8. Interagieren mit der Komponente

Haben Sie versucht, an einem bestimmten Tag zu drücken? Die inneren UI-Elemente in unserer Komponente verhalten sich weiterhin wie erwartet und lösen Ereignisse als Reaktion auf Benutzeraktionen aus. Wie gehen wir also mit diesen Ereignissen um?

Die Antwort besteht aus zwei Teilen:

  • Erfassen Sie Ereignisse innerhalb der Komponente und
  • Melden Sie Ereignisse an die übergeordnete Komponente der Komponente (könnte ein Fragment , eine Activity oder sogar eine andere Komponente sein).

Der erste Teil ist ziemlich einfach. Um beispielsweise lange gedrückte Grid-Elemente zu handhaben, weisen wir einen entsprechenden Listener in unserer Komponentenklasse zu:

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

Es gibt mehrere Methoden zum Melden von Ereignissen. Eine direkte und einfache Möglichkeit besteht darin, die Art und Weise zu kopieren, wie Android dies tut: Es bietet eine Schnittstelle zu den Ereignissen der Komponente, die vom übergeordneten Element der Komponente implementiert wird ( eventHandler im obigen Codeausschnitt).

Den Funktionen der Schnittstelle können beliebige anwendungsrelevante Daten übergeben werden. In unserem Fall muss die Schnittstelle einen Ereignishandler verfügbar machen, der das Datum für den gedrückten Tag übergibt. Die folgende Schnittstelle ist in CalendarView definiert:

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

Die vom Eltern bereitgestellte Implementierung kann der Kalenderansicht über einen setEventHandler() werden. Hier ist ein Beispiel für die Verwendung von `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(); } }); }

Durch langes Drücken eines Tages wird ein langes Drücken-Ereignis GridView , das von GridView erfasst und verarbeitet und durch Aufrufen von onDayLongPress() in der bereitgestellten Implementierung gemeldet wird, die wiederum das Datum des gedrückten Tages auf dem Bildschirm anzeigt:

Bildschirmfoto

Eine andere, fortgeschrittenere Möglichkeit, dies zu handhaben, ist die Verwendung von Android Intents und BroadcastReceivers . Dies ist besonders hilfreich, wenn mehrere Komponenten über das Kalenderereignis benachrichtigt werden müssen. Wenn Sie beispielsweise auf einen Tag im Kalender drücken, muss ein Text in einer Activity angezeigt und eine Datei von einem Hintergrunddienst Service werden.

Bei Verwendung des vorherigen Ansatzes muss die Activity einen EventHandler für die Komponente bereitstellen, das Ereignis verarbeiten und es dann an den Service übergeben. Stattdessen erleichtert es nicht nur das Leben, wenn die Komponente eine Intent sendet und sowohl die Activity als auch der Service sie über ihre eigenen BroadcastReceivers akzeptieren, sondern hilft auch, die Activity und den betreffenden Service zu entkoppeln.

Fazit

Erblicken Sie die unglaubliche Kraft der Android-Anpassung!
Twittern

So erstellen Sie in wenigen einfachen Schritten Ihre eigene benutzerdefinierte Komponente:

  • Erstellen Sie das XML-Layout und gestalten Sie es nach Ihren Wünschen.
  • Leiten Sie Ihre Komponentenklasse gemäß Ihrem XML-Layout von der entsprechenden übergeordneten Komponente ab.
  • Fügen Sie die Geschäftslogik Ihrer Komponente hinzu.
  • Verwenden Sie Attribute, damit Benutzer das Verhalten der Komponente ändern können.
  • Um die Verwendung der Komponente im UI-Designer zu vereinfachen, verwenden Sie die isInEditMode() Funktion von Android.

In diesem Artikel haben wir als Beispiel eine Kalenderansicht erstellt, hauptsächlich weil die Aktienkalenderansicht in vielerlei Hinsicht fehlt. Sie sind jedoch in keiner Weise darauf beschränkt, welche Art von Komponenten Sie erstellen können. Sie können die gleiche Technik verwenden, um alles zu erstellen, was Sie brauchen, der Himmel ist die Grenze!

Vielen Dank für das Lesen dieses Leitfadens. Ich wünsche Ihnen viel Glück bei Ihren Programmierversuchen!