În Tips & Tricks

Optimizarea interfeței cu utilizatorul în aplicațiile Android

android_interfata

O interfață grafică performantă este foarte importantă pentru o aplicație, mai ales dacă ne gândim că aceasta este primul punct de contact cu utilizatorul. Având în vedere că toate operațiile care țin de interfață se execută pe main thread (din acest motiv, denumit și „UI thread”), o aplicație care nu beneficiază de un UI optimizat poate rula foarte încet, poate sacada sau chiar se poate bloca. Optimizarea interfeței cu utilizatorul este deci esențială.

În acest articol vom vedea modalitățile prin care Android se ocupă de acest aspect, modul în care view-urile sunt ierarhizate, cum se face desenarea lor și ce tool-uri avem la dispoziție pentru optimizare.

Ierarhia de view-uri

Android își așază componentele grafice într-o ierarhie de layout-uri, view-uri și widget-uri. Layout-urile pot conține view-uri, dar și alte layout-uri care la rândul lor conțin alte view-uri și layout-uri. Această îmbricare a componentelor duce la realizarea unei structuri arborescente. Mai jos puteți vedea un exemplu de astfel de structură.

Android_view_hierarchy_diagram

View Rendering

Prima traversare este cea de măsurare. Fiecare view trebuie să determine cât de mari sunt descendenții lui. Aceasta se face prin apelarea metodei de măsurare a fiecărui descendent în parte. Acest apel presupune și trimiterea de către părinte a restricțiilor de dimensiune către descendenți. Fiecare descendent își va calcula dimensiunea maximă, ținând cont de restricțiile impuse de părinte. Rezultatele sunt returnate părintelui la finalul calculului.

A doua traversare este cea de poziționare și are loc imediat ce se termină măsurarea descendenților. Odată ce părinții au datele de dimensiune a descendenților, se vor ocupa de poziționarea lor în cadrul spațiului pe care îl au disponibil.

Aceste traversări sunt complet transparente pentru dezvoltator și au loc indiferent dacă implementăm view-uri proprii sau folosim view-uri din SDK. Dacă trebuie să implementăm view-urile noastre, atunci trebuie să implementăm metodele de onMeasure și onLayout.

După cum se poate observa, desenarea view-urilor este destul de costisitoare și Android va petrece o parte semnificativă din timpul de execuție a aplicației ocupându-se de ele. În general, fiecare modificare de interfață va declanșa traversarea ierarhiei pentru măsurare și poziționare.

Optimizare

Primul impuls atunci când creăm layout-ul unui Activity, poate fi să folosim componentele cele mai simple de layout pentru a simplifica codul și modul în care le repartizăm pe ecran. În realitate este exact invers. Dacă folosim structuri simple, este foarte probabil ca numărul lor să fie mare, iar ierarhia care rezultă să fie una foarte adâncă, cu multe noduri și nivele. Acest lucru este mai ales valabil la folosirea excesivă a LinearLayout.

Fiecare părinte încearcă să afle de la descendenții lui propietățile acestora: dimensiunea și poziția. Aceste calcule se propagă pe întreg arborele ierarhic. Astfel, cu cât acesta este mai adânc, propagarea va traversa mai multe nivele, crescând timpul de procesare. Altă problemă apare atunci când un anumit layout este încărcat de mai multe ori, cum ar fi, de exemplu, layout-ul pentru un item în cadrul unui ListView.

Prima soluție pentru optimizarea layout-urilor este să folosim cât mai mult posibil RelativeLayout, pentru a reduce din nivelele de ierarhie. Desigur asta nu înseamnă folosirea lui exclusivă. Există situații când un TableLayout ar putea fi mai de folos. Dar, în general, este de preferat să folosim RelativeLayout decât să îmbricăm prea multe LinearLayout-uri. Dacă undeva în cod avem două LinearLayout-uri îmbricate, trebuie analizat dacă nu se poate obține același rezultat cu un singur RelativeLayout.

RelativeLayout pleacă de la o premiză simplă: toate componentele sunt așezate relativ una la cealaltă. Implicit, toate sunt poziționate în colțul din stânga sus. Pentru fiecare descendent în parte, putem menționa cum să fie poziționat față de ceilalți descendenți.

Iată un exemplu:
Android

Pentru a implementa layout-ul din imagine, am putea fi tentați să folosim un LinearLayout vertical și un LinearLayout orizontal pentru rândul din mijloc, ceea ce ar duce la o ierarhie cu cel puțin două nivele. Dar ea poate fi implementată folosind un singur layout relativ.

Codul mai jos:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:paddingLeft="16dp"
    android:paddingRight="16dp" >
    <EditText
        android:id="@+id/name"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:hint="@string/reminder" />
    <Spinner
        android:id="@+id/dates"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_below="@id/name"
        android:layout_alignParentLeft="true"
        android:layout_toLeftOf="@+id/times" />
    <Spinner
        android:id="@id/times"
        android:layout_width="96dp"
        android:layout_height="wrap_content"
        android:layout_below="@id/name"
        android:layout_alignParentRight="true" />
    <Button
        android:layout_width="96dp"
        android:layout_height="wrap_content"
        android:layout_below="@id/times"
        android:layout_alignParentRight="true"
        android:text="@string/done" />
</RelativeLayout>

Altă metodă de optimizare este refolosirea componentelor. Pentru view-urile care sunt desenate des, instațele lor se pot cache-ui și refolosi dacă tot ce diferă este informația din ele, nu și modul de afișare.

Modul în care este proiectată interfața cu utilizatorul ține și ea de optimizare. Dacă proiectăm o interfață prea bogată în elemente grafice, care înghesuie prea multe componente, putem aveam probleme din două direcții: interfața este congestionată și utilizatorul își pierde atenția de la ceea ce vrea să facă, iar desenarea ei devine foarte costisitoare. În cazul acesta, simplitatea nu ajută doar la claritate, ci și la performanță.

Tool-uri pentru optimizarea layout-ului

La final, amintesc de un tool pe care Android îl oferă pentru analiza ierarhiei componentelor și a performanței interfeței: HierarchyViewer. Acesta permite analiza performanței chiar în timpul rulării aplicației și ajută la descoperirea zonelor problemă. Acesta este disponibil în directorul /tools/.

HierarchyViewer-ul oferă, pe lângă o reprezentare vizuală a structurii componentelor, și timpii consumați pentru cele 3 operații de bază: measure, layout și draw.

Puteți încerca un experiment simplu: realizați o interfață care are cel puțin 3 nivele și 2-3 LinearLayout-uri îmbricate, fiecare cu view-uri simple în ele, TextView sau Button. Rulați aplicația și notați timpii de measure, layout și draw raportați de HierarchyViewer. Implementați apoi același layout folosind un singur RelativeLayout și rulați din nou. Se va putea observa o îmbunătățire a timpilor cu măcar 30-40% față de implementarea anterioară.

Ne oprim aici, cu toate că optimizarea view-urilor este un topic destul de spinos, în special dacă dorim să implementăm view-uri custom. Cele prezentate în acest articol reprezintă baza realizării de layout-uri sănătoase. Ca de obicei, vă aștept cu întrebări și feedback.