Laborator 05. Proiectarea Interfețelor Grafice (II)

Controale pentru colecții de date

În situația în care aplicația Android operează cu un volum semnificativ informații provenind din diverse surse de date, trebuie utilizat un set de obiecte specializate pentru gestiunea lor:

  1. de tip container (derivate din clasa android.widget.AdapterView) responsabil pentru afișarea acestora pe ecran; el conține câte un element de tip android.widget.View pentru fiecare element al listei, oricât de complex;
  2. de tip adaptor (ce implementează interfața android.widget.Adapter) care asigură
    1. legătura dintre resursele care dețin datele respective (fișiere, furnizori de conținut) și controalele de pe suprafața de afișare, cu care interacționează utilizatorul; orice modificare asupra datelor va fi realizată prin intermediul acestei componente;
    2. modul de dispunere a informațiilor pentru fiecare element în parte.

Această arhitectură corespunde șablonului MVC (Model - View - Controller) în care modelul este reprezentat de sursele de date, vizualizarea de componenta grafică în care este afișat conținutul acestora iar controlorul de clasa adaptor.

Tipuri de containere pentru colecții de date

Pentru afișarea colecțiilor de date se folosesc elemente grafice de tip container, referite sub denumirea de liste. Acestea controlează dimensiunile și modul de dispunere al componentelor, structura unei componente fiind însă gestionată de elementul de tip adaptor asociat. Vor fi prezentate inițial particularitățile pe care le pun la dispoziție fiecare dintre aceste controale, detaliile de implementare referitoare la tipurile de adaptoare asociate fiind tratate ulterior.

Cele mai utilizate tipuri de containere pentru colecțiile de date sunt ListView, GridView, Spinner și Gallery.

ListView

Un obiect de tip ListView afișează elementele pe care le conține vertical, existând posibilitatea ca acestea să fie derulate în situația în care se depășește dimensiunea suprafeței de afișare.

Elementele pot fi separate de anumite delimitatoare (resursă grafică sau culoare) specificate prin proprietatea divider (pentru care se poate preciza și o înălțime - atributul dividerHeight). Un astfel de control poate fi precedat, respectiv succedat de un control grafic, existând posibilitatea distingerii acestor elemente prin delimitatoare (proprietățile headerDividersEnabled, respectiv footerDividersEnabled).

Dacă este folosit într-o activitate obișnuită, controlul de tip ListView trebuie să fie specificat în interfața grafică din fișierul XML asociat ferestrei din care face parte, precizându-se și un identificator prin intermediul căruia să poată fi referit:

activity_movies.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"  
  tools:context=".MoviesActivity" >
 
  <ListView
    android:id="@+id/movies_list_view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:divider="@color/lightblue"
    android:dividerHeight="5dp"   />
 
</RelativeLayout>

Elementele (de tip View) pe care le afișează un astfel de obiect sunt furnizate de un ListAdapter care trebuie precizat prin intermediul metodei setAdapter(ListAdapter). În codul sursă, se va specifica un tip de adaptor care va fi inițializat cu datele pe care le va conține obiectul respectiv, după care acesta va fi asociat controlului grafic.

public class MoviesActivity extends Activity {
 
  String[] movies = new String[] { 
    "The Shawshank Redemption", 
    "The Godfather", 
    "The Godfather: Part II", 
    "The Dark Knight", 
    "Pulp Fiction", 
    "The Good, the Bad and the Ugly", 
    "Schindler's List", 
    "12 Angry Men", 
    "The Lord of the Rings: The Return of the King", 
    "Fight Club" 
  };
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_movies);
 
    ListView listview = (ListView)findViewById(R.id.movies_list_view);
    final ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, movies);
    listview.setAdapter(adapter);
  }
}

Activitatea MoviesActivity afișează denumirea unor filme sub forma unei liste în care elementele sunt dispuse unele sub altele.

Se observă că elementul de tip adaptor primește un parametru care indică modul de dispunere a conținutului listei. În acest sens se pot utiliza layout-uri predefinite, corespunzătoare tipului de obiect de tip AdapterView sau se pot folosi layout-uri definite de utilizator (pentru structuri de date mai complexe ce implică afișarea informației într-un format dependent de acest context).

Toate operațiile asupra conținutului elementului de tip ListView se vor realiza prin intermediul obiectului ListAdapter.

În cazul în care se produc anumite evenimente ce implică interacțiunea utilizatorului asupra acestui control, ele vor fi tratate prin clase ascultător distincte pentru fiecare tip de acțiune (apăsarea - de scurtă durată sau de lungă durată - asupra unui element din cadrul listei, selectarea unei intrări).

listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
  @Override
  public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    Toast.makeText(MainActivity.this, "You have clicked on "+adapter.getItem(position)+" item", Toast.LENGTH_SHORT).show();
  }
});
listview.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
  @Override
  public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
    Toast.makeText(MainActivity.this, "You have long clicked on "+adapter.getItem(position)+" item", Toast.LENGTH_SHORT).show();
    return true;
  }
});
listview.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
  @Override
  public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
    Toast.makeText(MainActivity.this, "You have selected the "+adapter.getItem(position)+" item", Toast.LENGTH_SHORT).show();
  }
  @Override
  public void onNothingSelected(AdapterView<?> parent) {
    Toast.makeText(MainActivity.this, "Currently, the list has no item selected", Toast.LENGTH_SHORT).show();
  }
});

Aceste metode de tratare a evenimentelor din clasele ascultător primesc ca parametri:

  • un obiect de tip AdapterView, indicând lista din care a fost accesat un element;
  • un View, reprezentând elementul din cadrul listei care a fost accesat;
  • poziția la care se află în listă elementul care a fost accesat;

Numerotarea elementelor în cadrul listei se face începând cu 0.

  • identificatorul elementului care a fost accesat, conform obiectului de tip adaptor (poate fi obținut prin intermediul metodei getItemId()); această valoare este dependentă atât de obiectul de tip adaptor folosit, cât și de sursa de date

Conținutul listei poate fi precizat și în fișierul de resurse /res/values/strings.xml

strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="app_name">Top 10 Movies</string>
  <string-array name="movies">
    <item>The Shawshank Redemption</item>
    <item>The Godfather</item>
    <item>The Godfather: Part II</item>
    <item>The Dark Knight</item>
    <item>Pulp Fiction</item>
    <item>The Good, the Bad and the Ugly</item>
    <item>Schindler&apos;s List</item>
    <item>12 Angry Men</item>
    <item>The Lord of the Rings: The Return of the King</item>
    <item>Fight Club</item>
  </string-array>
</resources>

și apoi

  • specificat drept conținut al elementului de tip ListView
    <ListView ... android:entries="@array/movies" ... />

    (situație în care obiectul de tip adaptor nu va fi instanțiat, fiind preluat cel creat în mod automat cu intrările din resursa de tip tablou specificată în fișierul XML)

sau

  • încărcat în codul sursă:
    movies = getResources().getStringArray(R.array.movies);

Utilizarea fișierelor de resurse pentru specificarea unei surse de date scalează foarte bine cu situația în care aplicația trebuie să fie localizată.

Dacă se dorește ca elementele din listă să poată fi selectate, în constructorul obiectului de tip adaptor se va specifica un model care suportă o astfel de operație android.R.layout.simple_list_item_checked.

final ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_checked, movies);

Numărul de componente din cadrul listei care pot fi selectate concomitent pot fi:

  • nici unul (valoare implicită): ListView.CHOICE_MODE_NONE
  • unul singur: ListView.CHOICE_MODE_SINGLE
  • mai multe: ListView.CHOICE_MODE_MULTIPLE

Aceste constante vor fi transmise ca parametru metodei setChoiceMode() definită de clasa ListView. Pentru a se determina intrările care au fost selectate de utilizator, va fi parcursă întreaga listă (numărul de elemente fiind obținut prin metoda getCount()), starea în care se găsesc acestea fiind verificată prin intermediul meteodei isItemChecked(int) care primește ca parametru poziția din listă pentru care se dorește să se realizeze textul.

String itemsSelected = "You have selected the following items:\n";
for (int position = 0; position < listview.getCount(); position++)
  if (listview.isChecked(position)) {
    itemsSelected += " * "+listview.getItemAtPosition(position)+"\n";
  }

Totodată, există posibilitate de filtrare a conținutului unei liste în funcție de o valoare dată de utilizator, comportament ce poate fi obținut printr-un apel al metodei setFilterEnabled(boolean).

ExpandableListView

Un caz particular de listă este ExpandableListView, care permite asocierea conținuturilor în grupuri, acestea putând fi expandate.

Elementele sunt însoțite de un indicator care specifică starea sa (grup - expandat sau nu, respectiv element al grupului, evidențiindu-se ultimul din sublistă), existând astfel elemente de demarcație în modurile de dispunere implicite, ele putând fi de asemenea precizate de utilizator prin intermediul proprietăților groupIndicator, respectiv childIndicator.

Layout-urile predefinite pentru acest obiect sunt android.R.layout.simple_expandable_list_item_1 și android.R.layout.simple_expandable_list_item_2.

GridView

GridView este un tip de obiect folosit pentru afișarea conținuturilor sub formă tabulară, existând posibilitatea de derulare a acestora în cazul în care se depășește dimensiunea suprafeței de afișare.

Se poate preciza numărul de coloane prin proprietatea numColumns, aceasta având de regulă valoarea AUTO_FIT, astfel încât să se determine în mod automat acest atribut în funcție de conținutul tabelului și de spațiul avut la dispoziție. Dimensiunea fiecărei coloane poate fi specificată explicit (prin columnWidth - dându-se o valoare însoțită de o unitate de măsură). De asemenea, în condițiile în care dimensiunile spațiului de afișare permit, tabelul poate fi extins în funcție de valoarea indicată pentru proprietatea stretchMode (spacingWidth / spacingWidthUniform pentru extinderea spațiului dintre coloane, respectiv columnWidth pentru extinderea coloanelor). Spațierea dintre rânduri și coloane este controlată prin horizontalSpacing și verticalSpacing.

activity_contacts.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".ContactsActivity" >
 
  <GridView
    android:id="@+id/contacts_grid_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:horizontalSpacing="10dp"
    android:verticalSpacing="10dp"
    android:numColumns="auto_fit"
    android:gravity="center" >
 
    <!-- grid layout content -->
 
  </GridLayout>
 
</RelativeLayout>
ContactsActivity.java
public class ContactsActivity extends Activity implements LoaderManager.LoaderCallbacks<Cursor> {
 
  GridView gridview;
  SimpleCursorAdapter adapter;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
 
    gridview = (GridView)findViewById(R.id.contacts_grid_view);
    String[] projections = new String[] {Contacts.DISPLAY_NAME};
    int[] views = new int[] {android.R.id.text1}; 
    adapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_1, null, projections, views, 0);
    gridview.setAdapter(adapter);
    getLoaderManager().initLoader(0, null, this);
  }
 
  public Loader<Cursor> onCreateLoader(int id, Bundle data) {
    return new CursorLoader(this, Contacts.CONTENT_URI, null, null, null, Contacts.DISPLAY_NAME);
  }
 
  public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
    adapter.swapCursor(cursor);	
  }
 
  public void onLoaderReset(Loader<Cursor> loader) {
    adapter.swapCursor(null);
  }
}

Activitatea ContactsActivity afișează numele persoanelor din lista de contacte a telefonului sub forma unui tabel. Având în vedere faptul că volumul de informații poate fi semnificativ, încărcarea acestora nu se face în cadrul activității (pentru a nu genera situații de neresponsivitate a interfeței grafice), ci prin intermediul unui obiect de tip Loader.

Pentru a putea accesa lista de contacte a telefonului, aplicația trebuie să specifice permisiunile corespunzătoare în fișierul AndroidManifest.xml:
<uses-permission android:name=“android.permission.READ_CONTACTS” />

Spinner

În cadrul elementului de tip Spinner, nu se afișează întregul conținut al listei (astfel încât să nu se ocupe întregul spațiu disponibil), ci numai valoarea selectată în mod curent, existând posibilitatea expandării pentru vizualizare a tuturor componentelor în momentul accesării acesteia.

Modul în care sunt afișate intrările dintre care se poate realiza selecția este controlat prin proprietatea spinnerMode care poate lua valorile dialog dacă se dorește afișarea unei ferestre de dialog, respectiv dropdown dacă lista va fi expandată sub controlul care conține valoarea curentă.

  • pentru modul de dispunere dialog, se poate indica un mesaj suplimentar care va fi afișat în cadrul ferestrei ce prezintă toate opțiunile ce pot fi selectate prin intermediul atributului prompt
  • pentru modul de dispunere dropdown pot fi specificate în plus următoarele proprietăți
    • dropDownHorizontalOffset / dropDownVerticalOffset indică valoarea cu care va fi decalată pe orizontală / verticală lista derulante ce conține celelalte opțiuni
    • dropDownSelector este o referință către o resursă pentru a indica modul de selecție a unui element din cadrul listei
    • dropDownWidth reprezintă lățimea listei derulante ce conține celelalte opțiuni (poate avea valorile match_parent sau wrap_content)
    • popupBackground specifică o resursă ce va fi desenată pe fundal atâta vreme cât este afișată lista derulantă ce conține celelalte opțiuni
<Spinner
  android:id="@+id/spinner"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:entries="@array/device_manufacturer"
  android:spinnerMode="dialog"
  android:prompt="@string/prompt" />
<Spinner
  android:id="@+id/spinner"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:entries="@array/device_manufacturer"
  android:spinnerMode="dropdown"
  android:dropDownHorizontalOffset="10dp"
  android:dropDownVerticalOffset="10dp"
  android:popupBackground="@drawable/mobiledevice"
  android:dropDownSelector="@color/lightblue" />

Pentru un astfel de obiect vor trebui specificate două mecanisme de afișare a conținutului:

  1. pentru afișare în momentul în care lista nu este expandată, parametru care va fi specificat în momentul în care se construiește obiectul de tip adaptor; valoarea predefinită este android.R.layout.simple_spinner_item;
  2. pentru afișare în momentul în care lista este expandată, atribut care va fi indicată prin intermediul metodei setDropDownViewResource a obiectului adaptor; valoarea predefinită este android.R.layout.simple_spinner_dropdown_item.
Spinner spinner = (Spinner)findResourceById(R.id.spinner);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, deviceManufacturers);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);

Elementul de tip Gallery este un tip de control folosit pentru afișarea conținutului în cadrul unei liste dispuse orizontal, în care obiectul selectat este întotdeauna centrat. De obicei, este folosit pentru resurse multimedia de tip imagine.

Începând cu API 16, utilizarea acestui tip de conținut este descurajată, recomandându-se în schimb utilizarea altor elemente care suportă derularea componentelor pe orizontală, cum ar fi HorizontalScrollView și ViewPager.

Pentru un Gallery pot fi specificate următoarele atribute:

  • animationDuration - durata animației de tranziție de la un element la altul, exprimată în milisecunde
  • spacing - spațierea dintre elementele galeriei; această proprietate se folosește mai ales atunci când nu se specifică un anumit stil
Stiluri asociate unei galerii
  • unselectedAlpha - nivelul de transparență pentru elementele care nu sunt selectate, exprimată ca număr zecimal, cuprins între 0 și 1
activity_faculty.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical"
  tools:context=".FacultyActivity" >
  <TextView
    android:id="@+id/textview"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:text="@string/app_name"
    android:textSize="16sp" />
  <Gallery
    android:id="@+id/gallery"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp" 
    android:animationDuration="500"
    android:spacing="5dp"
    android:unselectedAlpha="0.5" />
  <ImageView
    android:id="@+id/imageview"
    android:layout_gravity="center"
    android:padding="10dp"
    android:layout_width="320dp"
    android:layout_height="240dp"
    android:scaleType="fitXY"
    android:contentDescription="@string/content_description" />
</LinearLayout>

În situația în care conținutul redat de acest element nu este de tip text, va trebui utilizat un tip de adaptor definit de utilizator, care specifică modul în care vor fi afișate astfel de elemente. În cadrul acesteia, pentru fiecare obiect conținut trebuie să se specifice un mecanism de dispunere prin intermediul clasei Gallery.LayoutParams (specificându-se mai ales dimensiunile elementului care se dorește a fi afișat).

FacultyActivity.java
public class FacultyActivity extends Activity {
 
  Integer[] facultyImages = {
    R.drawable.faculty01,
    R.drawable.faculty02,
    R.drawable.faculty03
    // add resources for other faculties
  };	
 
  class GalleryAdapter extends BaseAdapter {
    Context context;
 
    public ImageAdapter(Context context) {
      this.context = context;
    }
 
    public int getCount() {
      return facultyImages.length;
    }
 
    public Object getItem(int position) {
      return position;
    }
 
    public long getItemId(int position) {
      return position;
    }
 
    public View getView(int position, View convertView, ViewGroup parent) {
      ImageView imageview;
      if (convertView == null) {
        imageview = new ImageView(context);
        imageview.setImageResource(facultyImages [position]);
        imageview.setScaleType(ImageView.ScaleType.FIT_XY);
        imageview.setLayoutParams(new Gallery.LayoutParams(160, 120));
      } else {
        imageview = (ImageView)convertView;
      }
      return imageview;
    }
  }
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_faculty);
 
    Gallery gallery = (Gallery)findViewById(R.id.gallery);
    gallery.setAdapter(new GalleryAdapter(this));
    gallery.setOnItemClickListener(new AdapterView.OnItemClickListener() {
      @Override
      public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        ImageView imageview = (ImageView)(findViewById(R.id.imageview));
        imageview.setImageResource(facultyImages [position]);
      }
    });
  }
}

Tipuri de adaptoare pentru colecții de date

În cazul unei colecții de date, rolul unui adaptor este de a gestiona modelul de date transpunându-l (adaptându-l) la fiecare element din cadrul interfeței grafice (listă sau tabel). Toate adaptoarele (predefinite sau definite de utilizator) sunt derivate din clasa BaseAdapter.

În funcție de proveniența datelor, se vor utiliza tipuri de adaptoare specializate:

  • ArrayAdapter<T> - pentru date reținute sub forma unui tablou (de tipul T[] sau java.util.List<T>)
  • SimpleCursorAdapter - pentru date preluate de la un furnizor de conținut (de exemplu, o bază de date)

Acestea au asociate anumite moduri de dispunere a conținutului, predefinite în android.R.layout, adaptate la tipul de control grafic utilizat. De acele mai multe ori, pentru structuri de date complexe ce trebuie afișate folosind mecanisme de afișare particularizate pentru asigurarea unei anumite funcționalități, trebuie utilizat un tip de adaptor definit de utilizator, ce suprascrie metodele clasei BaseAdapter.

ArrayAdapter<T>

Clasa ArrayAdapter este utilizată pentru gestiunea unui tablou sau a unei liste de obiecte Java.

Fiecare element din cadrul acesteia va corespunde unui element din cadrul controlului grafic, conținutul acestuia fiind generat de rezultatul metodei toString() a clasei din care fac parte obiectele componente a vectorului sau a listei respective. Identificatorul textului de tip TextView în cadrul căruia se va afișa conținutul referit de adaptor poate fi specificat în constructorul acestuia, în caz contrar folosindu-se un element predefinit (al cărui identificator este android.R.id.text1).

Instanțierea unui obiect de tip ArrayAdapter<T> se face prin specificarea cel puțin a contextului în care acesta va fi afișat (activitate, fragment) și a identificatorului unei resurse de tip layout care va fi utilizată pentru dispunerea conținutului său (aceasta putând fi predefinită sau definită de utilizator). În plus, se pot preciza identificatorul unui control de tip TextView în cadrul căruia va fi plasat fiecare element al colecției, precum și obiectele propriu-zise, sub formă de tablou sau de listă.

ArrayAdapter(Context context, int resource);
ArrayAdapter(Context context, int resource, int textViewResourceId);
ArrayAdapter(Context context, int resource, T[] objects);
ArrayAdapter(Context context, int resource, int textViewResourceId, T[] objects);
ArrayAdapter(Context context, int resource, List<T> objects);
ArrayAdapter(Context context, int resource, int textViewResourceId, List<T> objects);

În situația în care conținutul sursei de date se regăsește în cadrul unei resurse, instanțierea unui obiect de tip ArrayAdapter<CharSequence> se face prin intermediul metodei statice createFromResource(Context, int, int) ce primește ca parametri identificatorul resursei care conține colecția de date (de tipul R.array….) și identificatorul modului de dispunere al elementelor.

Conținutul colecției de date poate fi modificat prin intermediul obiectului de tip ArrayAdapter<T> numai în situația în care tipul sursei de date suportă acest tip de operații (tablourile nu suportă modificarea prin intermediul adaptorului, în timp ce obiectele de tip listă parametrizată implementează această acțiune).

  1. apelul metodelor puse la dispoziție de clasa ArrayAdapter<T>
    • add(T) - adaugă un obiect
    • addAll(Collection<? extends T>) - adaugă o colecție de obiecte
    • clear() - șterge toate obiectele
    • insert(T, int) - adaugă un obiect la o anumită poziție
    • remove(T) - șterge un anumit obiect
  2. modificarea elementelor din sursa de date cu notificarea obiectului adaptor asociat prin intermediul metodei notifyDataSetChanged(), astfel încât operațiile realizate asupra sursei de date să se propage și la nivelul controlului grafic care îi afișează conținutul.

Metodele pentru modificarea colecției de date prin intermediul obiectului de tip adaptor (add, addAll, clear, insert, remove) apelează în mod automat notifyDataSetChanged(). Acest comportament poate fi suprascris prin intermediul metodei setNotifyOnChange(boolean).

Ordonarea elementelor din sursa de date poate fi realizată prin obiectul de tip adapter, pe baza unui criteriu de comparare, prin intermediul metodei sort(Comparator<? super T>).

Pentru o listă ce afișează coordonatele unor puncte pe ecran în formatul (x, y) (dat de implementarea metodei toString() a clasei Point), gestiunea conținutului se poate face atât prin intermediul colecției de date (de tip ArrayList<Point>) cât și prin intermediul obiectului adator, de tip ArrayAdapter<Point>.

class Point {
    double x, y;
    public Point() { x = 0; y = 0; }
    public Point(double x, double y) { this.x = x; this.y = y; }
    public double getX() { return x; }
    public void setX(double x) { this.x = x; }
    public double getY() { return y; }
    public void setY(double y) { this.y = y; }
    public String toString() { return "("+x+", "+y+")"; }
}
ListView listview = (ListView)findViewById(R.id.listview);
ArrayList<Point> points = new ArrayList<Point>();
ArrayAdapter<Point> adapter = new ArrayAdapter<Point>(this, android.R.layout.simple_list_item_1, points);
listview.setAdapter(adapter);
points.add(new Point());
Point point = new Point(1, 2);
points.insert(point, 0);
points.remove(point);
adapter.notifyDataSetChanged();
adapter.add(new Point());
Point point = new Point(1, 2);
adapter.insert(point, 0);
adapter.remove(point);

În cazul în care se dorește afișarea conținutului în cadrul unei intefețe grafice mai complexe decât un simplu TextView, se poate defini o clasă derivată din ArrayAdapter<T> care va suprascrie metoda responsabilă cu element unui obiect al listei, cunoscându-se poziția sa în cadrul acesteia

public View getView (int position, View convertView, ViewGroup parent);

unde

  • position este poziția elementului care este afișat în mod curent
  • convertView este un obiect de tip View (dintre cele create anterior, dar care nu mai este vizibil) ce poate fi reutilizat pentru construirea elementului curent
  • parent este un obiect de tip ViewGroup ce reprezintă container-ul în care va fi conținut elementul

În cadrul acestei metode, se va construi un obiect de tip View (sau se va reutiliza parametrul convertView, pentru optimizarea memoriei folosite) având modul de dispunere dorit care va fi returnat ca rezultat al metodei.

Astfel, dacă se dorește ca pentru fiecare punct să se afișeze o imagine corespunzătoare cadranului din care face parte, se va defini un tip de layout pentru fiecare element al listei:

pointlayout.xml
<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent" >
  <ImageView
    android:id="@+id/imageview"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:padding="2dp"
    android:contentDescription="@string/content_description" />
  <TextView
    android:id="@+id/textview"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
</LinearLayout>

Implementarea clasei extinse din ArrayAdapter<Point> va trebui să redefinească metoda getView(int, View, ViewGroup) care va întoarce un control grafic conținând un element al listei, format dintr-o imagine sugestivă pentru tipul de cadran (obiect de tip ImageView) și un câmp text conținând coordonatele punctului (obiect de tip TextView).

class PointArrayAdapter extends ArrayAdapter<Point> {
  private Context context;
  private int resource;
  private ArrayList<Point> content;
 
  public PointArrayAdapter(Context context, int resource, ArrayList<Point> content) {
    super(context, resource, content);
    this.context = context;
    this.resource = resource;
    this.content = content;
  }
 
  @Override
  public View getView(int position, View convertView, ViewGroup parent) {
    View view;
    if (convertView == null) {
      LayoutInflater layoutinflator = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
      view = layoutinflator.inflate(resource, null);
    } else {
      view = convertView;
    }
    ImageView imageview = (ImageView)view.findViewById(R.id.imageview);
    TextView textview = (TextView)view.findViewById(R.id.textview);
    Point point = content.get(position);
    if (point.getX() >= 0 && point.getY() >= 0) {
      imageview.setImageResource(R.drawable.firstquadrant);
    } else if (point.getX() < 0 && point.getY() >= 0) {
      imageview.setImageResource(R.drawable.secondquadrant);
    } else if (point.getX() < 0 && point.getY() < 0) {
      imageview.setImageResource(R.drawable.thirdquadrant);
    } else if (point.getX() >= 0 && point.getY() < 0) {
      imageview.setImageResource(R.drawable.forthquadrant);
    }
    textview.setText(point.toString());
    return view;
  }
}

De remarcat reutilizarea parametrului convertView în cazul în care acesta nu este null. Conținutul său este cel al unui control grafic instanțiat anterior care nu mai este vizibil pe ecran. În felul acesta se asigură optimizarea memoriei folosite dar și a timpului de execuție, întrucât nu se mai creează obiecte de tip View în cazul în care acestea există deja.

În cadrul activității va fi definit un obiect de tip adaptor care va primi ca parametri contextul (fereastra), identificatorul modului de dispunere pentru componentele listei (R.layout.pointlayout) și conținutul acesteia.

setContentView(R.layout.activity_main);
ListView listview = (ListView)findViewById(R.id.listview);
ArrayList<Point> content = new ArrayList<Point>();
// add points to content
PointArrayAdapter adapter = new PointArrayAdapter(this, R.layout.pointlayout, content);
listview.setAdapter(adapter);

SimpleCursorAdapter

În situația în care se lucrează cu furnizori de conținut sau cu baza de date din cadrul sistemului de operare Android, este recomandat să se utilizeze un obiect de tipul SimpleCursorAdapter, care asociază rezultatele obținute în urma interogării unor astfel de resurse la controale grafice de tip TextView sau ImageView existente în layout-uri predefinite sau definite de utilizator.

Maparea între cele două tipuri se resurse este realizată în două etape:

  1. dacă există un obiect de tip SimpleCursorAdapter.ViewBinder disponibil, se apelează metoda setViewValue(View, Cursor, int)
    1. dacă rezultatul întors este true, asocierea a fost realizată
    2. dacă rezultatul întors este false
      1. se încearcă să se realizeze maparea cu un control de tip TextView se apelează metoda setViewText(TextView, String)
      2. se încearcă să se realizeze maparea cu un control de tip ImageView se apelează metoda setViewImage(ImageView, String)
  2. dacă nu se identifică nici un mod de asociere corespunzător, se generează o excepție de tipul IllegalStateException

Constructorul clasei SimpleCursorAdapter

public SimpleCursorAdapter (Context context, int layout, Cursor cursor, String[] from, int[] to, int flags);

primește următorii parametri:

  • context - activitatea / fragmentul în contextul căruia este afișat controlul grafic conținând rezultatul interogării conținut de cursor
  • layout - identificatorul către o resursă predefinită sau definită de utilizator care descrie modul în care sunt vizualizate elementele din cadrul cursorului
  • cursor - cursorul conținând rezultatul interogării din sursa de date

Cursorul poate avea valoarea null dacă nu este încă disponibil.

  • from - denumirile coloanelor din cadrul cursorului (proiecția) care vor fi afișate în cadrul controlului grafic
  • to - tablou conținând identificatorii elementelor grafice (predefinite / definite de utilizator) în cadrul cărora vor fi afișate valorile extrase din interogare

Elementele tablourilor from și to identifică maparea între cursor și view, relația între componentele acestora fiind de 1:1.

  • flags - stabilesc comportamentul elementului de tip adaptor, putând avea valorile
    • FLAG_AUTO_REQUERY - utilizarea sa este descurajată începând cu API 11, forțează cursorul să realizeze interogarea din nou atunci când se primește o notificare cu privire la modificarea conținutului sursei de date; întrucât operația se realizează în contextul interfeței grafice, poate determina o diminuare a responsivității aplicației
    • FLAG_REGISTER_CONTENT_OBSERVER - determină asocierea unui obiect pentru monitorizarea conținutului sursei de date.

De exemplu, dacă se dorește afișarea unor informații din calendarul asociat contului cu care a fost înregistrat dispozitivul mobil, va fi interogată resursa respectivă (specificându-se URI-ul prin care poate fi accesată, lista cu denumirile coloanelor ce trebuie obținute, condiția de selecție a rezultelor și parametrii acesteia (în cazul unei interogări parametrizate) precum și criteriile de sortare a informațiilor). Rezultatul va fi plasat într-un obiect de tip Cursor. Se instanțiază apoi un obiect de tip SimpleCursorAdapter ce primește ca parametrii contextul curent (activitatea), modalitatea de dispunere a informațiilor (predefinită, listă având conținutul afișat pe două linii), cursorul ce conține rezultatul interogarii (obținut anterior), precum și listele cu denumirile coloanelor, respectiv identificatorii controalelor grafice în care va fi plasat conținutul acestora.

Accesarea calendarului este posibilă numai prin specificarea permisiunilor necesare în fișierul AndroidManifest.xml
<uses-permission android:name=“android.permission.READ_CALENDAR” />

CalendarActivity.java
public class CalendarActivity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Cursor cursor = getContentResolver().query(
        Calendars.CONTENT_URI, 
        new String[]{Calendars._ID, Calendars.CALENDAR_DISPLAY_NAME, Calendars.CALENDAR_TIME_ZONE}, 
        null, 
        null,
        null);
    SimpleCursorAdapter adapter = new SimpleCursorAdapter(
        this,
        android.R.layout.two_line_list_item,
        cursor,
        new String[]{Calendars.CALENDAR_DISPLAY_NAME, Calendars.CALENDAR_TIME_ZONE},
        new int[] {android.R.id.text1, android.R.id.text2}, 
	0);
    ListView listview = (ListView)findViewById(R.id.listview);
    listview.setAdapter(adapter);
  }
}

SimpleAdapter

Obiectul de tip SimpleAdapter este utilizat atunci când se realizează asocieri între date statice și elemente ale interfeței grafice, definite în cadrul unor moduri de dispunere (predefinite sau definite de utilizator). Datele ce vor fi afișate au structura unei liste (List) al cărei conținut constă în mapări (Map) de tip (atribut, valoare). Arhitectura corespunde unui tablou de entități caracterizate prin anumiți parametri. Fiecare astfel de obiect va fi afișat în cadrul unui element din cadrul controlului cu care va interacționa utilizatorul.

Mecanismul de asociere între sursa de date și interfața grafică se realizează de asemenea în două etape, folosind clasa internă SimpleAdapter.ViewBinder, astfel:

  • printr-un apel al metodei generice setViewValue(View, Object, String)
  • prin încercarea de mapare a conținutului cu un View de tip Checkbox, TextView, respectiv ImageView

Constructorul clasei SimpleAdapter se evidențiază prin structura sursei de date care va fi afișată, având forma List<? extends Map<String, ?>> (este obligatoriu ca tipul cheii din asociere să fie șir de caractere).

public SimpleAdapter (Context context, List<? extends Map<String, ?>> data, int resource, String[] from, int[] to);

Valorile specificate pentru proiecție (câmpul from) trebuie să corespundă cheilor din asocierea conținută în listă (obiectul de tip Map<String, ?>).

De exemplu, pentru entitatea StaffMember cu atributele name și position se dorește afișarea elementelor de acest tip (care se pot regăsi local, în /assets/staffmember.xml sau la distanță, pe un server) în cadrul unei liste, se poate folosi un obiect de tip SimpleAdapter. Utilitarul care va parsa fișierul XML va întoarce un obiect ArrayList<HashMap<String, String>>, unde fiecare element este o proprietate a entității (nume sau poziție). Cu un astfel de rezultat se poate construi un obiect de tip SimpleAdapter, specificându-se totodată un identificator al tipului de layout, atributele care vor fi afișate precum și controalele cărora le vor fi repartizate.

StaffMemberActivity.java
public class StaffMemberActivity extends Activity {
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_staff_member);
 
    try {
      InputStream inputstream = getAssets().open("staffmember.xml");
      StaffXmlParser staffxmlparser = new StaffXmlParser();
      List<Map<String,?>> staffmembers = staffxmlparser.parse(inputstream);
      SimpleAdapter adapter = new SimpleAdapter(
          this,
          staffmembers,
          android.R.layout.simple_list_item_2,
          new String[] {"name", "position"},
          new int[] {android.R.id.text1, android.R.id.text2});
      ListView listview = (ListView)findViewById(R.id.listview)
      listview.setAdapter(adapter);
    } catch (Exception exception) {
      Log.println(Log.ERROR, "exception", exception.getMessage());
    }
  }
}

Obiecte de tip adaptor definite de utilizator

Există situații în care funcționalitatea pusă la dispoziție de obiectele standard de tip adaptor nu este adecvată cerințelor unei aplicații, atât din punctul de vedere al gestiunii datelor cât și al interfeței grafice în contextul căreia trebuie să fie afișate informațiile.

În acest scop, este pusă la dispoziția programatorilor clasa abstractă BaseAdapter care poate fi extinsă cu operațiile impuse de aplicație. Definirea unui astfel de obiect de tip adaptor implică de fapt suprascrierea unor metode, definite de interfețele ListAdapter și SpinnerAdapter:

public int getCount();
public Object getItem(int position);
public long getItemId(int position);
public View getView(int position, View convertView, ViewGroup parent);
  • metoda getCount() furnizează numărul de elemente din cadrul listei;
  • metoda getItem() întoarce elementul care se află pe o anumită poziție în cadrul listei;
  • metoda getItemId() returnează identificatorul elementului de pe o anumită poziție în cadrul listei;
  • metoda getView() construiește un control grafic corespunzător elementului de pe o anumită poziție în cadrul listei; acesta poate fi o instanță nouă, existând totodată posibilitatea de a se reutiliza parametrul convertView (care reprezintă o componentă a listei creată anterior, ce nu mai este vizibilă, putând fi astfel reciclată); obiectul parent indică elementul de tip container în care este afișat conținutul sursei de date.

Clasa care implementează tipul de adaptor definit de utilizator trebuie să conțină (intern) și modelul de date, operând asupra acestuia.

În plus, în cazul în care elementele listei conțin moduri de dispunere diferite (în funcție de poziția pe care se găsesc, spre exemplu - rândurile impare și rândurile pare), vor fi suprascrise încă alte două metode:

public int getViewTypeCount();
public Object getItemViewType(int position);
  • metoda getViewTypeCount() va întoarce numărul tipurilor de layout-uri care vor fi afișate în cadrul listei;
  • metoda getItemViewType() returnează - pentru elementul aflat pe o anumită poziție - mecanismul de dispunere care va fi utilizat.

Spre exemplu, pentru gestiunea unei liste formată dintr-un ImageView și două TextView, dispuse diferit în funcție de rândul pe care se găsesc, vor trebui definite două layout-uri (în /res/layout) - denumite odd_layout.xml și even_layout.xml ce vor fi încărcate de metoda getView() în funcție de poziția respectivă:

public class CustomAdapter extends BaseAdapter {
 
  Activity context;
  List<CustomObject> data;
 
  // ...
 
  @Override
  public View getView(int position, View convertView, ViewGroup parent) {
    View customView;
    CustomObject customObject = data.get(position);
    LayoutInflater layoutInflater = (LayoutInflater)context.getLayoutInflater();
    if (position % 2 == 0) {
       customView = layoutInflater.inflate(R.layout.odd_layout, parent, false);
    } else {
       customView = layoutInflater.inflate(R.layout.even_layout, parent, false);
    }
    ImageView imageView = (ImageView)customView.findViewById(R.id.imageView);
    imageView.setImageResource(customObject.getImageId());
    TextView textView1 = (TextView)customView.findViewById(R.id.textView1);
    textView1.setText(customObject.getText1());
    TextView textView2 = (TextView)customView.findViewById(R.id.textView2);
    textView2.setText(customObject.getText2());
    return customView;
  }
}

Clasa adaptor definită de utilizator va trebui să rețină referințe către context (activitatea în cadrul căreia este afișată lista) și către sursa de date.

Metoda getView() construiește un element de pe o anumită poziție din cadrul listei pe care îl va întoarce apoi ca rezultat.

Inițial, trebuie să se obțină conținutul intrării din listă ce trebuie afișată (considerându-se o corespondență 1:1 între conținutul sursei de date și elementele listei) și să se instanțieze obiectul responsabil cu expandarea layout-ului. Ulterior se obține obiectul de tip View pe baza mecanismului de dispunere (corespunzător poziției curente) specificat în fișierul XML. Acesta va furniza referințe către controalele din cadrul său (prin intermediul identificatorilor unici) pentru care se va putea specifica conținutul, preluat din obiectul curent al sursei de date.

Tehnici pentru optimizarea performanțelor

Implementarea unor tehnici pentru optimizarea performanțelor este foarte importantă atunci când se dezvoltă aplicații pentru dispozitive mobile. Având în vedere resursele limitate (în special în privința memoriei și a capacității de procesare) de care dispun acestea, un program care nu le gestionează în mod eficient poate determina o depreciere a timpului de răspuns.

Fiecare element din cadrul unei liste este un View obținut prin expandarea layout-ului, atât construirea obiectului pentru gestiunea modurilor de dispunere (metoda getLayoutInflater()) cât și operația de instanțiere a unei intrări din listă pe baza specificației XML (metoda inflate()) fiind operații extrem de costisitoare. De asemenea, obținerea unei referințe către un control din cadrul interfeței grafice pe baza identificatorului său (metoda findViewById()) poate avea afecta performanțele aplicației.

Printre cele mai utilizate tehnici pentru optimizarea performanțelor se numără:

  • instanțierea obiectului responsabil cu expandarea mecanismului de dispunere a unui element din cadrul listei pe constructorul clasei adaptor
    public CustomAdapter(Activity context, List<CustomObject> data) {
      this.data = data;
      layoutInflater = (LayoutInflater)context.getLayoutInflater();
    }

    În acest fel, se evită apelul acestei metode - costisitoare din punctul de vedere al utilizării resurselor - de fiecare dată când trebuie creat un element al listei.

  • reutilizarea elementelor din cadrul listei care nu mai sunt vizibile
    De regulă, o listă conține mai multe elemente decât pot fi reprezentate pe suprafața de afișare a dispozitivului mobil, parcurgerea lor implicând folosirea unui mecanism de derulare. Obiectele corespunzătoare unor intrări care nu sunt vizibile la un moment dat pot fi reutilizate, ele fiind transmise în metoda getView() prin intermediul parametrului convertView, prevenindu-se astfel operația de expandare a layout-ului.
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
      View customView;
      CustomObject customObject = data.get(position);
      if (convertView == null) {
        if (position % 2 == 0) {
          customView = layoutInflater.inflate(R.layout.odd_layout, parent, false);
        } else {
          customView = layoutInflater.inflate(R.layout.even_layout, parent, false);
        }
      }
      else {
        customView = convertView;
      }
      // ...
      return customView;
    }

    Prin această metodă se evită apelarea metodei inflate() de fiecare dată când trebuie să se afișeze un element al listei.

Nu întotdeauna este posibilă reutilizarea obiectelor ce reprezintă elemente ale listei, situație în care parametrul convertView are valoarea null. De aceea, în metoda getView() trebuie verificată întotdeauna valoarea acestui parametru.

În cazul în care există mai multe posibilități de dispunere în cadrul listei, există riscul ca tipul de element care va fi reutilizat să nu fie cel necesar. Pentru a se asigura evitarea unei astfel de situații, este necesară suprascrierea metodelor getViewTypeCount(), respectiv getItemViewType().

public final static int LIST_VIEW_TYPES ​    ​= 2;
public final static int LIST_VIEW_TYPE_ODD ​ = 0;
public final static int LIST_VIEW_TYPE_EVEN = 1;
 
publicint getViewTypeCount() {
  return LIST_VIEW_TYPES; 
}
public Object getItemViewType(int position) {
  if (position % 2 == 0)return LIST_VIEW_TYPE_ODD;
  return ​LIST_VIEW_TYPE_EVEN;
}

  • reținerea unei referințe către structura layout-ului prin intermediul unor etichete
    În acest scop se utilizează un șablon denumit ViewHolder prin intermediul căruia pot fi accesate controalele grafice din cadrul unui element al listei, fără a fi necesară utilizarea identificatorului din cadrul documentului XML.

    ViewHolder este o clasă internă din cadrul adaptorului, definind exact structura pe care o are layout-ul unei intrări din cadrul listei. În momentul în care un obiect corespunzător unui element al listei este creat, în atributele clasei ViewHolder se rețin referințe către controalele grafice, acestea fiind atașate, ca etichetă, prin intermediul metodei setTag(). Ulterior, ele vor fi preluate (prin metoda getTag()), utilizându-se referințele către elementele componente în loc să se apeleze metoda findViewById() de fiecare data, rezultatul fiind o apreciere a performanțelor cu circa 15%.
    public class CustomAdapter extends BaseAdapter {
     
      public static class ViewHolder {
        ImageView imageView;
        TextView textView1, textView2;
      };
     
      // ...
     
      @Override
      public View getView(int position, View convertView, ViewGroup parent) {
        View customView;
        ViewHolder viewHolder;
        CustomObject customObject = (CustomObject)data.get(position);
        if (convertView == null) {
          if (position % 2 == 0) {
            customView = layoutInflater.inflate(R.layout.odd_layout, parent, false);
          } else {
            customView = layoutInflater.inflate(R.layout.even_layout, parent, false);
          }
          viewHolder = new ViewHolder();
          viewHolder.imageView = (ImageView)customView.findViewById(R.id.imageView);
          viewHolder.textView1 = (TextView)customView.findViewById(R.id.textView1);
          viewHolder.textView2 = (TextView)customView.findViewById(R.id.textView2);
          customView.setTag(viewHolder);
        } else {
          customView = convertView;
        }
        viewHolder = (ViewHolder)customView.getTag();
        viewHolder.imageView.setImageResource(customObject.getImageeId());
        viewHolder.textView1.setText(customObject.getText1());
        viewHolder.textView2.setText(customObject.getText2());
     
        return customView;
      }
    }

Componente specializate pentru gestiunea listelor

Întrucât de regulă în cadrul unei interfețe grafice nu există decât o singură listă (care ocupă tot spațiul disponibil și se găsește centrată pe suprafața de afișare), în Android au fost definite componente speciale pentru gestiunea acesteia: ListActivity, respectiv ListFragment. Nu este necesar să se utilizeze un layout în această situație, lista fiind declarată în mod implicit.

În cazul în care se foloșește un layout definit de utilizator, este absolut necesar ca lista să fie declarată explicit, având identificatorul @android:id/list, în caz contrar generându-se o excepție.

<ListView
  android:id="@android:id/list"
  android:layout_width="match_parent"
  android:layout_height="wrap_content" />

În layout-urile definite de utilizator, se mai poate folosi un control grafic (de orice tip), având identificatorul @android:id/empty care va fi afișat pe ecran numai în cazul în care lista este vidă (aceasta fiind ascunsă în acest caz).

Clasele ListActivity și ListFragment funcționează doar cu obiecte de tip ListView. În cazul în care se dorește folosirea altor tipuri particulare de liste, utilizarea acestora nu va fi posibilă.

ListActivity

Clasa ListActivity definește o activitate ce încapsulează un obiect de tip ListView, conectat la o sursă de date (cursor sau tablou/listă), expunând către programatori metode de tratare a evenimentului de selectare a unui element.

Gestiunea adaptorului pentru lista implicită din ListActivity se face prin intermediul metodelor:

  • ListAdapter getListAdapter() - întoarce obiectul de tip adaptor asociat listei
  • void setListAdapter(ListAdapter adapter) - asociază listei un obiect de tip adaptor

Referința către lista implicită poate fi obținută prin intermediul metodei ListView getListView().

Metoda ce va fi apelată automat în momentul în care utilizatorul selectează un element din cadrul listei este:

protected void onListItemClick(ListView l, View v, int position, long id);

parametrii având următoarea semnificație:

  • l - lista pe care s-a produs evenimentul
  • v - elementul din cadrul listei care a fost apăsat
  • position - poziția elementului care a fost apăsat în cadrul listei
  • id - identificatorul elementului care a fost apăsat în cadrul listei (întors de metoda getItemId() a clasei adaptor)

Clasa ListActivity nu definește însă și o metodă pentru tratarea apăsărilor de lungă durată (eng. long-click), în acest sens fiind necesară implementarea unei clase ascultător ce implementează interfața AdapterView.OnItemLongClickListener, asociată obiectului de tip ListView din cadrul ferestrei.

public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id);

Semnificația parametrilor este aceeași ca în cazul metodei precedente. Rezultatul întors este true în cazul în care evenimentul de tip apăsare lungă a unui eveniment a fost consumat și false altfel.

ListFragment

Clasa ListFragment definește un fragment ce încapsulează un obiect de tip ListView, conectat la o sursă de date (cursor sau tablou/listă), expunând către programatori metode de tratare a evenimentului de selectare a unui element.

Metodele pe care le pune la dispoziție ListFragment sunt similare cu cele oferite de ListActivity.

În cazul în care se dorește gestiunea unei aplicații cu mai multe fragmente utilizând liste, acestea vor fi incluse într-o fereastră ce extinde FragmentActivity și nu ListActivity, în caz contrar evenimentele de tip apăsare a unui element fiind consumate de clasa părinte.

Pentru utilizarea de layout-uri definite de utilizatori, lista implicită trebuie referită prin android:id/list, mecanismul de dispunere fiind încărcat pe metoda onCreateView().

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) {
  View view = inflater.inflate(R.layout.fragment_custom, container, false);
  return view;
}

Accesarea controalelor grafice din cadrul fragmentului nu poate fi realizată decât în metoda onActivityCreated(), când există siguranța că activitatea care îl conține a fost creată. Specificarea unui tip de adaptor definit de utilizator se va face tot în contextul acestei metode.

@Override
public void onActivityCreated(Bundle state) {
  super.onActivityCreated(state);
  CustomAdapter customAdapter = new CustomAdapter(getActivity(), data);
  setListAdapter(customAdapter);	
}

Se observă faptul că referința către activitatea care conține fragmentul se face prin metoda getActivity(). Numai după ce activitatea a fost creată se garantează că rezultatul acestei metode este relevant.

Specificarea unui fragment în cadrul unei activități poate fi realizată:

  1. static, dacă fragmentul respectiv nu va fi înlocuit în cadrul interfeței grafice (sau în situația în care este înlocuit, el este păstrat pe stivă prin apelul metodei addToBackStack(), pentru a se putea reveni la el)
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:tools="http://schemas.android.com/tools"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      tools:context=".MainActivity" >
     
      <fragment
        android:id="@+id/fragment1"
        android:name="ro.pub.cs.systems.pdsd.lab05.Fragment1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
     
    </RelativeLayout>

    Un astfel de fragment nu poate fi niciodată distrus, dar poate fi înlocuit cu un alt conținut, plasându-se conținutul său pe stivă:

    Fragment2 fragment2 = new Fragment2();
    FragmentManager fragmentManager = getFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
    fragmentTransaction.replace(android.R.id.content, fragment2);
    fragmentTransaction.addToBackStack(null);
    fragmentTransaction.hide(this);
    fragmentTransaction.commit();

    De remarcat faptul că pentru fragment au fost specificate următoarele proprietăți:

    • android:id prin intermediul căruia fragmentul poate fi identificat în mod unic (alternativ se poate specifica android:tag)
    • android:name reprezentând clasa corespunzătoare fragmentului, care va fi instanțiată în momentul în care este expandat layoutul corespunzător activității (în locul elementului <fragment> va fi plasat rezultatul metodei onCreateView())
  2. dinamic, în situația în care conținutul fragmentului va fi diferit, în funcție de anumite operații realizate de utilizator
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:tools="http://schemas.android.com/tools"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      tools:context=".CartoonActivity" >
     
      <FrameLayout 
        android:id="@+id/fragment1"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />
     
    </RelativeLayout>

    În această situație se utilizează un mecanism de dispunere de tip FrameLayout care permite afișarea unui singur conținut la un moment dat. În cazul în care în cadrul acestui container se găsea anterior un alt fragment, acesta este distrus (spre diferență de cazul anterior).

    Fragment2 fragment2 = new Fragment2();
    FragmentManager fragmentManager = getFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
    fragmentTransaction.replace(R.id.fragment1, fragment2);
    fragmentTransaction.commit();

Meniuri

În Android, meniurile sunt implementate folosind clasa android.view.Menu, fiecare activitate având asociat un astfel de obiect.

Așa cum se poate observa, un meniu poate conține:

  • 0 sau mai multe submeniuri (reprezentate de clasa android.view.SubMenu)
  • 0 sau mai multe intrări propriu-zise (reprezentate de clasa android.view.MenuItem), caracterizate prin următoarele atribute:
    • denumire reprezintă valoarea care este afișată în cadrul interfeței grafice;
    • identificator generat de obicei în mod automat pentru meniurile declarate prin intermediul fișierelor XML;
    • identificator al grupului de meniuri prin intermediul căruia mai multe opțiuni din cadrul meniului pot fi grupate împreună;
    • număr de ordine specifică importanța intrării, determinând o sortare a acestora, fiind folosit pentru a se indica ordinea de afișare a meniurilor sau pentru a preciza faptul că întregul grup aparține unei categorii (identificatorul grupului de meniuri fiind același cu numărul de ordine); unele intervale sunt rezervate pentru anumite categorii de meniuri
CATEGORIE TIP INTERVAL DESCRIERE
container Menu.CATEGORY_CONTAINER 0x10000 intrări de meniu legate de modul de dispunere al interfeței grafice
sistem Menu.CATEGORY_SYSTEM 0x20000 intrări de meniu create de sistemul de operare Android, descriind funcționalități valabile pentru toate activitățile
secundare Menu.CATEGORY_SECONDARY 0x30000 intrări de meniu de o importanță mai redusă decât celelalte (mai puțin utilizate)
alternative Menu.CATEGORY_ALTERNATIVE 0x40000 intrări de meniu create de aplicații externe, oferind modalități alternative de a gestiona datele

Astfel de valori pot fi însă utilizate pentru identificatorul intrării sau pentru identificatorul grupului de meniuri, neexistând nici un fel de restricții în acest sens.

Un meniu ce conține operațiile relevante pentru o anumită activitate se numește meniu de opțiuni, acesta fiind accesibil:

  • pentru aplicațiile destinate platformelor anterioare Android 2.3.x (API 10) - inclusiv, la apăsarea tastei Menu, afișându-se o porţiune a meniului ce cuprinde șase poziții disponibile; în situația în care meniul respectiv este format din mai multe elemente, va exista și o opțiune More prin care pot fi accesate şi celelalte opţiuni
  • pentru aplicaţiile destinate platformelor ulterioare Android 3.0 (API 11) - inclusiv, prin intermediul barei de acțiuni în care sunt plasate pictogramele corespunzătoare acțiunilor, în funcție de prioritățile specificate; dacă nu pot fi afișate toate opțiunile, va exista o pictogramă pentru accesarea celorlalte opțiuni, acestea putând fi accesate și prin apăsarea tastei Menu, în cazul în care acesta există

El este instanțiat în momentul apelării metodei:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
  // populate menu, via XML resource file or programatically
  return true;
}

Pentru sistemele Android anterioare versiunii 2.3.x, aceasta este apelată în mod automat atunci când utilizatorul accesează meniul, iar pentru sistemele Android anterioare versiunii 3.0 în momentul instanțierii activității, de vreme ce opțiunile meniului trebuie să fie accesibile și prin intermediul barei de acțiuni.

Este foarte important ca rezultatul metodei onCreateOptionsMenu() să fie true, pentru a face meniul vizibil. În situația în care metoda întoarce false, meniul va fi invizibil.

Mai multe informații cu privire la meniuri pot fi consultate aici.

Mecanisme de gestiune a meniurilor

Un meniu poate fi definit:

1. prin intermediul unui fișier de resurse XML plasat în res/menu - variantă preferată datorită faptului că:

  • meniul poate fi identificat prin intermediul unei denumiri (generată în mod automat în clasa R.menu);
  • intrările propriu-zise din cadrul meniului vor fi ordonate în mod automat, generarea identificatorilor corespunzători acestora realizându-se de asemenea automat;
  • textele conținute vor fi localizate automat (prin referințele la fișierele ce conțin șiruri de caractere);
  • structura poate fi vizualizată mai clar, separându-se partea de prezentare (conținutului meniului) de logica aplicației (mecanismul de răspuns la acțiunile utilizatorului vis-a-vis de intrările ale meniului);
  • permite definirea de configurații diferite pentru diverse platforme (dimensiuni ale suprafeței de afișare).
/res/menu/main.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
  <item
    android:id="@+id/operations"
    android:title="@string/operations" >
    <menu>
      <item
        android:id="@+id/create"
	android:icon="@drawable/create"
        android:orderInCategory="100"
	android:showAsAction="ifRoom"
	android:title="@string/create" />
      <!-- add other submenu entries -->	    
    </menu>
  </item>
  <group
    android:id="@+id/generic_actions"
    android:visible="true"
    android:enabled="true"
    android:checkableBehavior="none" >
    <item
      android:id="@+id/settings"
      android:icon="@drawable/settings"
      android:orderInCategory="100"
      android:showAsAction="ifRoom"
      android:title="@string/settings" />
    <!-- add other menu entries -->
  </group>
  <item
    android:id="@+id/help"
    android:icon="@drawable/help"
    android:orderInCategory="100"
    android:showAsAction="ifRoom"
    android:title="@string/help" />
  <!-- add other menu entries --> 	
</menu>

Așa cum se observă, elementul rădăcină este un obiect de tip <menu>, definește un meniu (de tip android.view.Menu). Acesta poate conține elemente de tip:

a. <item> care creează

  • un submeniu (de tip android.view.Submenu) în cazul în care are imbricat un alt element de tip <menu>;
  • o intrare propriu-zisă a meniului (de tip android.view.MenuItem), dacă nu conține alte elemente;

Atributele elementului <item> sunt:

  • android:id - un identificator unic pentru resursă care va fi utilizat ca referință pentru elementul respectiv
  • android:icon - o resursă de tip drawable folosită pe post de pictogramă, atunci când elementul este inclus în cadrul barei de acțiuni
  • android:orderInCategory - numărul de ordine ce definește importanța intrării meniului în cadrul grupului
  • android:showAsAction - precizează criteriile în funcție de care elementul respectiv va fi inclus în cadrul barei de acțiuni, putând avea valorile:
    • ifRoom - va fi plasat în bara de acțiuni numai dacă este spațiu disponibil
    • withText - în bara de acțiuni se va afișa atât pictograma intrării cât și denumirea intrării din cadrul meniului
    • never - nu va fi plasat niciodată în bara de acțiuni
    • collapseActionView - elementul grafic asociat intrării din cadrul meniului poate fi rabatat
    • always - va fi plasat întotdeauna în bara de acțiuni

În situația în care există mai multe intrări ale meniului care definesc această valoare decât spațiul de afișare pus la dispoziție de bara de acțiuni, elementele respective se vor suprapune. Această opțiune nu trebuie utilizată decât în cazul în care intrarea meniului care o definește este critică în cadrul aplicației.

  • android:title - denumirea elementului, așa cum este afișată către utilizator (dacă valoarea este un șir de caractere prea lung, poate fi folosită valoarea indicată de proprietatea android:titleCondensed)
  • android:alphabeticShortcut, android:numericShortcut - indică o scurtătură (de tip caracter sau număr) prin care poate fi accesată intrarea din cadrul meniului

b. <group>, un container pentru intrări propriu-zise ale meniului (elemente <item>) care partajează același comportament, acesta putând fi specificat ca proprietate a grupului (în loc de a fi precizat pentru fiecare element în parte)

  • android:visible - vizibile sau invizibile
  • android:enabled- activate sau dezactivate
  • android:checkableBehavior - selectate sau deselectate (valori posibile fiind none, all sau single după cum pot fi selectate nici una, toate sau o singură intrare a meniului din cadrul grupului)

Pentru ambele tipuri de elemente, poate fi specificată proprietatea android:menuCategory ce indică prioritatea corespunzătoare categoriei de meniu (container, system, secondary, respectiv alternative).

Încărcarea meniului în cadrul activității (sau fragmentului) este realizată prin intermediul metodei onCreateOptionsMenu(Menu):

@Override
public boolean onCreateOptionsMenu(Menu menu) {
  getMenuInflater().inflate(R.menu.main, menu);
  return true;
}

2. programatic, în codul sursă, de regulă în cadrul metodei onCreateOptionsMenu() Metodele prin intermediul cărora sunt adăugate intrări în cadrul meniului sunt:

public SubMenu addSubmenu(int titleRes);
public SubMenu addSubmenu(CharSequence title);
public SubMenu addSubMenu(int groupId, int itemId, int order, int titleRes);
public SubMenu addSubMenu(int groupId, int itemId, int order, CharSequence title);
 
public MenuItem add(int titleRes);
public MenuItem add(CharSequence title);
public MenuItem add(int groupId, int itemId, int order, int titleRes);
public MenuItem add(int groupId, int itemId, int order, CharSequence title);

Astfel, funcționalitatea descrisă prin intermediul fișierelor resursă XML poate fi obținută și prin intermediul codului sursă:

final public static int GROUP_ID_NONE            = 0;
final public static int GROUP_ID_GENERIC_ACTIONS = 1;
 
final public static int SUBMENU_ID_OPERATIONS    = 0;
 
final public static int MENU_ID_CREATE           = 1;
final public static int MENU_ID_SETTINGS         = 2;
final public static int MENU_ID_ABOUT            = 3;
 
final public static int MENU_ORDER               = 100;
 
@Override
public boolean onCreateOptionsMenu(Menu menu) {
  MenuItem menuItem;
 
  SubMenu subMenu = menu.addSubMenu(GROUP_ID_NONE, SUBMENU_ID_OPERATIONS, MENU_ORDER, R.string.operations);
  menuItem = subMenu.add(GROUP_ID_NONE, MENU_ID_CREATE, MENU_ORDER, R.string.create);
  menuItem.setIcon(R.drawable.create);
  menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
  // add other submenu entries
 
  menuItem = menu.add(GROUP_ID_GENERIC_ACTIONS, MENU_ID_SETTINGS, MENU_ORDER, R.string.settings);
  menuItem.setIcon(R.drawable.settings);
  menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
  // add other menu entries
 
  menu.setGroupVisible(GROUP_ID_GENERIC_ACTIONS, true);
  menu.setGroupEnabled(GROUP_ID_GENERIC_ACTIONS, true);
  menu.setGroupCheckable(GROUP_ID_GENERIC_ACTIONS, false, false);
 
  menuItem = menu.add(GROUP_ID_NONE, MENU_ID_SETTINGS, MENU_ORDER, R.string.about);
  menuItem.setIcon(R.drawable.about);
  menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
  // add other menu entries
 
  return true;
}

Aceste metode pot fi utilizate și în situația în care meniul este definit prin intermediul unor fișiere resursă de tip XML, dorindu-se ulterior modificarea dinamică a acestuia.

Alte metode utile puse la dispoziție de clasa Menu sunt:

  • findItem(int) - întoarce un obiect de tip MenuItem, dându-se identificatorul acestuia;
  • getItem(int) - întoarce un obiect de tip MenuITem, dându-se poziția acestuia;
  • removeGroup(int) - șterge toate elementele din cadrul unui grup, furnizându-se identificatorul acestuia;
  • clear() - șterge toate intrările intrărilor din cadrul meniului, acesta rămânând vid;
  • removeItem(int) - șterge o intrare din cadrul meniului, dându-se identificatorul acestuia;
  • size() - furnizează numărul de intrări din cadrul meniului;
  • hasVisibleItems() - precizează dacă există intrări din cadrul meniului care sunt vizible sau nu;
  • close() - determină închiderea meniului, în cazul în care acesta este deschis.

În situația în care sunt definite meniuri atât pentru activitate cât și pentru fragmentele pe care le conține, acestea sunt combinate, fiind incluse inițial opțiunile din activitate și ulterior acțiunile din fragmente, în ordinea în care acestea sunt declarate. De asemenea, se poate realiza și sortarea prin intermediul numărului de ordine definit de fiecare intrare propriu-zisă a meniului în parte.

În cazul în care aplicația conține mai multe activități care folosesc același meniu de opțiuni, se va crea o clasă Activity care va implementa numai metodele onCreateOptionsMenu(), respectiv onOptionsItemSelected(), aceasta fiind extinsă pentru a se prelua funcționalitatea de instanțiere a meniului și de tratare a evenimentelor de apăsare.

Tratarea evenimentelor asociate meniurilor

Tratarea evenimentelor de accesare a unei intrări a meniului se face în cadrul metodei onOptionsItemSelected(MenuItem), apelată în mod automat în momentul producerii unui astfel de eveniment. Aceasta primește ca parametru opțiunea din cadrul meniului care a fost aleasă de utilizator, recunoașterea sa realizându-se prin intermediul identificatorului unic asociat fiecărei intrări:

@Override
public boolean onOptionsItemSelected(MenuItem item) {
  switch(item.getItemId()) {
    case R.id.create:
    case MENU_ID_CREATE:
      // ...
      return true;
    // ...           
  }
  return super.onOptionsItemSelected(item);
}

Este important ca ulterior tratării evenimentului de accesare a unei intrări din cadrul meniului, rezultatul întors să fie true. În situația în care intrarea care a fost selectată nu este gestionată, se recomandă să se apeleze metoda părinte care returnează în mod implicit false.

Acesta este mecanismul recomandat pentru tratarea evenimentelor legate de meniuri. Există însă și alte alternative:

1. definirea unei clase ascultător dedicate care implementează interfața MenuItem.OnMenuItemClickListener și metoda onMenuItemClick(MenuItem); atașarea obiectului ascultător se face prin intermediul metodei setOnMenuItemClickListener().

public class CustomMenuItemClickListener implements MenuItem.OnMenuItemClickListener {
  @Override
  public boolean onMenuItemClick(MenuItem item) {
    switch(item.getItemId()) {
      case R.id.create:
      case MENU_ID_CREATE:
        // ...
        return true;
      // ...           
    }
    return true;
  }
}
 
// ..
 
CustomMenuItemClickListener customMenuItemClickListener = new CustomMenuItemClickListener();
menuItem.setOnMenuItemClickListener(customMenuItemClickListener);

Această metodă trebuie să fie apelată pentru fiecare intrare de meniu în parte. Ea are însă precedență în fața altor metode, astfel încât în situația în care rezultatul pe care îl întoarce este true, nu mai sunt apelate și alte metode, cum ar fi onOptionsItemSelected().

2. utilizarea unei intenții asociate la o intrare din cadrul meniului prin intermediul metodei setIntent(Intent), lansându-se în execuție activitatea corespunzătoare în condițiile în care:

  • nu este suprascrisă metoda de tratare a evenimentului de selectare a unei intrări din cadrul meniului onOptionsItemSelected();
  • în situația în care metoda de tratare a evenimentului de selectare a unei intrări din cadrul meniului onOptionsItemSelected() este suprascrisă, trebuie să se apeleze metoda părinte (super.onOptionsItemSelected(menu)) pentru acele intrări ale meniului ce nu sunt procesate.

În mod implicit, nici o intrare din cadrul unui meniu nu are asociată o intenție.

Alte tipuri de meniuri în Android

Meniuri contextuale

Un meniu contextual (clasa ContextMenu) este asociat de regulă unui control din cadrul interfeței grafice (spre diferență de meniurile de opțiuni care sunt asociate activităților). De regulă, acesta este accesat la operația de apăsare prelungită pe obiectul respectiv (de tip View), având aceeași structură ca orice meniu, metodele utilizate pentru construirea sa fiind aceleași.

În cazul meniurilor contextuale, nu sunt suportate submeniurile, pictogramele sau furnizarea unor scurtături.

O activitate poate avea asociat un singur meniu de opțiuni, însă mai multe meniuri contextuale, asociate fiecare câte unui element din interfața sa grafică.

Instanțierea unui meniu contextual este un proces realizat în mai multe etape:

  1. indicarea faptului că obiectul din cadrul interfeței grafice va avea asociat un meniu contextual
    registerForContextMenu(view);

    O astfel de operație este necesară datorită faptului că numai unele dintre elementele interfeței grafice din cadrul activității (sau fragmentului) vor avea asociat un astfel de meniu.
    Apelul metodei va atașa activității un obiect ascultător de tip View.OnCreateContextMenuListener, astfel încât operația de apăsare prelungită pe obiectul respectiv va declanșa apelul metodei onCreateContextMenu(ContextMenu, View, ContextMenuInfo) care trebuie definită de programator.
    În cazul în care se dorește atașarea unui meniu contextual pentru elementele unui container pentru colecții de date (ListView, GridView) este suficient ca acesta să fie transmis ca parametru al metodei pentru ca meniul contextual să fie afişat pentru fiecare dintre intrările pe care le conţine.

  2. implementarea metodei onCreateContextMenu(ContextMenu, View, ContextMenuInfo) în cadrul activității (sau fragmentului)
    @Override
    public void onCreateContextMenu (ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
      super.onCreateContextMenu(menu, v, menuInfo);
      // option 1
      MenuInflater inflater = getMenuInflater();
      inflater.inflate(R.menu.context_menu, menu);
      // option 2
      menu.setHeaderTitle(R.string.operations);
      menu.add(GROUP_ID_NONE, MENU_ID_CREATE, MENU_ORDER, R.string.create);
    }

    Metoda primește următorii parametri (ce pot fi utilizați pentru a se determina ce obiect din cadrul interfeței grafice a fost accesat, pentru a se instanția meniul contextual corespunzător):

    • menu - meniul contextual care se dorește a fi afișat;
    • view - elementul din cadrul interfeței grafice căruia îi este asociat meniul contextual;
    • menuInfo - un obiect care oferă informații suplimentare cu privire la elementul selectat.
  3. implementarea metodei onContextItemSelected(MenuItem) care gestionează acțiunea de selectare a unei intrări din cadrul meniului contextual
    @Override
    public boolean onContextItemSelected(MenuItem item) {
      AdapterContextMenuInfo adapterContextMenuInfo = (AdapterContextMenuInfo)item.getMenuInfo();
      // or
      ExpandableContextMenuInfo expandableContextMenuInfo = (ExpandableContextMenuInfo)item.getMenuInfo();
      switch(item.getItemId()) {
        case R.id.create:
        case MENU_ID_CREATE:
          // ...
          return true;
        // ...           
      }
      return super.onContextItemSelected(item);
    }

    Tratarea acțiunii de selectare a unei intrări din cadrul meniului contextual trebuie semnalată prin întoarcerea unui rezultat al metodei de tip true, în caz contrar trebuind apelată metoda părinte super.onContextItemSelected(item) care va determina apelarea metodelor corespunzătoare eventualelor fragmente pe care le poate conține activitatea (în ordinea în care acestea au fost declarate) până în momentul în care una dintre acestea va întoarce un rezultat (implementarea implicită a metodei întoarce false atât pentru clasa Activity cât și pentru clasa Fragment).

Meniuri alternative

Prin intermediul unui meniu alternativ ce poate face parte dintr-un meniu de opțiuni (inclusiv dintr-unul din submeniurile sale) sau dintr-un meniu contextual, pot fi lansate în execuție activități din cadrul aplicației curente sau din cadrul altor activități, dacă acestea sunt disponibile, pentru a gestiona datele curente. În situația în care nu sunt identificate astfel de activități, meniul contextual respectiv nu va conține nici o intrare.

Instanțierea unui meniu alternativ se realizează în cadrul metodei onCreateOptionsMenu(Menu) și este un proces realizat în mai multe etape:

  1. definirea unei intenții care va indica care sunt cerințele referitoare la activitatea care urmează a fi invocată prin intermediul meniului contextual; intenția va specifica o categorie ce are valoarea Intent.CATEGORY_ALTERNATIVE sau Intent.CATEGORY_SELECTED_ALTERNATIVE (dacă activitatea se referă la intrarea selectată din cadrul meniului, intenția fiind definită în cadrul metodei onCreateContextMenu())
    Intent intent = new Intent(null, getIntent().getData()); // the URI of the data application is working with; getIntent() may return null!!!
    intent.addCategory(Intent.CATEGORY_ALTERNATIVE);

    O activitate poate fi invocată prin intermediul unui meniu contextual, dacă specifică valorile respective în câmpul ce indică filtrul pentru intenții în fișierul AndroidManifest.xml:

    <intent-filter>
     ...
     <category android:name="android.intent.category.ALTERNATIVE" />
     <category android:name="android.intent.category.SELECTED_ALTERNATIVE" />
     ...
    </intent-filter>
  2. căutarea unor activități care pot fi invocate folosind intenția respectivă prin intermediul metodei Menu.addIntentOptions (apelată pe obiectul transmis ca parametru metodei onCreateOptionsMenu())
    menu.addIntentOptions(
      Menu.CATEGORY_ALTERNATIVE, // the group the contextual menu will be attached to
      Menu.CATEGORY_ALTERNATIVE, // menu item ID for all of the menu items
      Menu.CATEGORY_ALTERNATIVE, // order number for each of the menu items
      this.getComponentName(),   // the name of the current activity
      null,                      // no specific items to place with priority
      intent,                    // intent that can invoke the corresponding activities (previously defined)
      0,                         // no additional flags to manage the menu items
      null);                     // no array of menu items to correlate with specific items

    Metoda furnizează o listă de activități care corespund criteriilor precizate de intenție populând cu acestea intrările din cadrul meniului, în cadrul unui anumit grup, alocându-le identificatori și numere de ordine începând cu o anumită valoare.
    Semnătura metodei este:

    public abstract int addIntentOptions (int groupId, int itemId, int order, ComponentName caller, Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems)

    semnificația parametrilor fiind:

    • groupId - grupul din cadrul meniului din care vor face parte intrările meniului contextual; se poate folosi valoarea Menu.NONE în situația în care elementele respective nu trebuie asociate unui grup
    • itemId - identificator unic folosit pentru elementele meniului contextual; în cazul în care acesta nu va mai fi utilizat ulterior, se poate furniza valoarea Menu.NONE
    • order - număr de ordine utilizat pentru sortarea intrărilor din cadrul meniului contextual; dacă acest criteriu nu va fi utilizat, poate fi specificată valoarea Menu.NONE
    • caller - denumirea componentei activității curente (denumirea pachetului și denumirea clasei) folosită de sistemul de operare pentru a se identifica resursa care a invocat altă activitate
    • specifics - elemente specifice care se doresc a fi plasate cu prioritate
    • intent - intenție care descrie tipurile de elemente cu care va fi populat meniul contextual
    • flags - opțiuni suplimentare care controlează mecanismul de atașare al intrărilor (spre exemplu, crearea unui meniu contextual numai cu activitățile specificate de intenția curentă sau folosirea lor împreună cu cele existente anterior - valoarea 0 indică faptul că doar valorile identificate în cadrul apelului curent al metodei ar trebui folosite)
    • outSpecificItems - un tablou în care se plasează intrările din cadrul meniului contextual care au fost generate pentru fiecare dintre elementele specifice (indicate ca parametru); pentru acțiunile în cazul cărora nu a fost identificată nici o activitate, se va furniza un rezultat null

Titlul intrărilor din cadrul meniului contextual este preluat din valoarea android:label specificată pentru elementul <intent-filter> corespunzător activității respective din cadrul fișierului AndroidManifest.xml.

Meniuri de tip pop-up

Un meniu de tip pop-up (clasa PopupMenu) este asociat unui element din cadrul interfeței grafice, afișat deasupra sau sub aceasta în momentul în care se produce un eveniment legat de acesta.

El se distinge însă față de celelalte tipuri de meniuri, față de care ar părea să implementeze o funcționalitate comună:

  • spre diferență de meniurile de opțiuni, acesta este creat în mod manual și doar atunci când este accesat elementul căruia îi este atașat;
  • spre diferență de meniurile contextuale, acesta nu are nici un impact asupra controlului grafic căruia îi este atașat;

Un meniu de tip pop-up poate fi creat - ca orice meniu - atât prin intermediul unui fișier XML de resurse cât și programatic.

Utilizarea unui meniu de tip pop-up presupune definirea unei metode care va fi apelată (manual, de către programator) atunci când se realizează o acțiune legată de controlul grafic căruia i se atașază:

public void showPopupMenu(View view) {
 
  PopupMenu popupMenu = new PopupMenu(this, menu);
 
  // option 1: defining a popup menu via a XML resource file
 
  // android 3.0
  MenuInflater menuInflater = popupMenu.getMenuInflater();
  menuInflater.inflate(R.menu.popup_menu, popupMenu.getMenu());
 
  // android 4.0
  popupMenu.inflate(R.menu.popup_menu);
 
  // option 2: defining a popup menu programatically
  Menu menu = popupMenu.getMenu();
  menu.add(GROUP_ID_NONE, MENU_ID_CREATE, MENU_ORDER, R.string.create);
  // ...
 
  popup.show();
}

Se observă că un obiect de tipul PopupMenu poate fi instanțiat transmițând ca parametri contextul (activitatea) în care acesta urmează să fie afișat precum și controlul grafic căruia îi va fi atașat (existând posibilitatea de a se preciza și modul de dispunere față de acesta).

public PopupMenu (Context context, View anchor);
public PopupMenu (Context context, View anchor, int gravity);

Tratarea evenimentului de accesare a unui element din cadrul meniului de tip pop-up se face prin implementarea interfeței PopupMenu.OnMenuItemClickListener:

popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
    @Override
    public boolean onMenuItemClick(MenuItem item) {
      switch(item.getItemId()) {
        case R.id.create:
        case MENU_ID_CREATE:
          // ...
          return true;
        // ...           
      }
      return true;
    }
});

Activitate de Laborator

Se dorește implementarea unei aplicații Android care să afișeze lista tuturor contactelor din agenda telefonică, împreună cu unele informații despre aceștia (imaginea asociată, numele, numărul de accesări, cea mai recentă accesare, apartenența la grupul de favoriți). De asemenea, în momentul în care este selectat un element din listă, se vor afișa toate numerele telefonice și toate adresele de poștă electronică stocate cu privire la acesta, împreună cu tipul lor. Totodată, va exista o facilitate de căutare pe măsură ce se introduce un șir de caractere, care poate fi regăsit în oricare dintre detaliile contactului respectiv.

0. Se recomandă ca în situația în care se folosește un emulator, să se adauge în aplicația People cel puțin două contacte care să poată fi gestionate prin intermediul aplicației.

1. În contul Github personal, să se creeze un depozit denumit 'Laborator05'. Inițial, acesta trebuie să fie gol (NU trebuie să bifați nici adăugarea unui fișier README.md, nici a fișierului .gitignore sau a a fișierului LICENSE).

2. Să se cloneze în directorul de pe discul local conținutul depozitului la distanță de la https://www.github.com/pdsd2015/Laborator05. În urma acestei operații, directorul Laborator05 va trebui să se conțină în subdirectorul labtasks proiectul Eclipse denumit AddressBook, fișierul README.md și un fișier .gitignore care indică tipurile de fișiere (extensiile) ignorate.

student@pdsd2015:~$ git clone https://www.github.com/pdsd2015/Laborator05

3. Să se încarce conținutul descărcat în cadrul depozitului 'Laborator05' de pe contul Github personal.

student@pdsd2015:~$ cd Laborator05/labtasks
student@pdsd2015:~/Laborator05/labtasks$ git remote add Laborator05_perfectstudent https://github.com/perfectstudent/Laborator05
student@pdsd2015:~/Laborator05/labtasks$ git push Laborator05_perfectstudent master

4. Să se încarce în mediul integrat de dezvoltare Eclipse proiectul AddressBook, folosind opțiunea FileImport….

5. Să se afișeze contactele din agenda telefonică sub forma unei liste, indicând, pentru fiecare dintre acestea:

  • numele persoanei (atributul Contact.DISPLAY_NAME_PRIMARY);
  • pictograma asociată (atributul Contact.PHOTO_THUMBNAIL_URI) - în cazul în care aceasta lipsește, se va afișa imaginea din resursa R.drawable.contact_photo;
  • frecvența cu care a fost contact (numărul de apeluri telefonice / mesaje electronice - SMS-uri, poștă electronică): atributul Contact.TIMES_CONTACTED;
  • data calendaristică și timpul la care a fost contactat cel mai recent (atributul Contact.LAST_TIME_CONTACTED);
  • o imagine în funcție de apartenența contactului la grupul de favoriți (atributul Contacts.STARRED):
    • R.drawable.star_set - dacă persoana face parte din grupul de favoriți;
    • R.drawable.star_unset - dacă persoana NU face parte din grupul de favoriți.

a. Se va defini fișierul contact_view.xml în care se vor declara controalele grafice necesare expunerii informațiilor legate de contacte.

contact_view.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="wrap_content" >
 
  <ImageView
    android:id="@+id/contact_photo_image_view"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_gravity="left|center_vertical"
    android:layout_margin="10dp"
    android:layout_weight="2"
    android:contentDescription="@string/photo_content_description"/>
 
  <LinearLayout
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:layout_weight="12"   
    android:orientation="vertical" >
 
    <TextView
      android:id="@+id/contact_name_text_view"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content" />
 
    <TextView
      android:id="@+id/contact_times_contacted_text_view"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/times_contacted" />
 
    <TextView
      android:id="@+id/contact_last_time_contacted_text_view"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@string/last_time_contacted" />
 
  </LinearLayout>
 
  <ImageView
    android:id="@+id/contact_starred_image_view"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_gravity="right|center_vertical"
    android:layout_margin="10dp"
    android:layout_weight="1"       
    android:contentDescription="@string/starred_description"/> 
 
</LinearLayout>

Așa cum se poate observa, pentru ca atributul android:layout_weight să fie luat în considerare, dimensiunea pe orientarea respectivă (android:layout_width / android:layout_height) trebuie să aibă valoarea 0dp.

b. În clasa ContactAdapter din pachetul ro.pub.cs.systems.lab05.addressbook.controller se vor implementa metodele necesare definirii unui obiect adaptor (de tip BaseAdapter) particularizat.

  • int getCount() - întoarce numărul de înregistrări din sursa de date afișată în cadrul listei;
  • Object getItem(int position) - întoarce sursa de date corespunzătoare unui contact de la o anumită poziție a listei;
  • long getItemId(int position) - întoarce identificatorul interfeței grafice corespunzătoare unui contact de la o anumită poziție a listei;

De regulă, metoda long getItemId(int position) poate întoarce valoarea 0 în situația în care nu este necesară accesarea interfeței grafice prin intermediul metodei View findViewById(int id).

  • View convertView(int position, View convertView, ViewGroup parent) - întoarce interfața grafică corespunzătoare unui contact de la o anumită poziție a listei.
ContactAdapter.java
public class ContactAdapter extends BaseAdapter {
  @Override
  public int getCount() {
    return data.size();
  }
 
  @Override
  public Object getItem(int position) {
    return data.get(position);
  }
 
  @Override
  public long getItemId(int position) {
    return 0;
  }
 
  @Override
  public View getView(int position, View convertView, ViewGroup parent) {
    Contact contact = data.get(position);
    LayoutInflater inflater = (LayoutInflater)context.getLayoutInflater();
    View contactView = inflater.inflate(R.layout.contact_view, parent, false);
    ImageView contactPhotoImageView = (ImageView)contactView.findViewById(R.id.contact_photo_image_view);
    TextView contactNameTextView = (TextView)contactView.findViewById(R.id.contact_name_text_view);
    TextView contactTimesContactedTextView = (TextView)contactView.findViewById(R.id.contact_times_contacted_text_view);
    TextView contactLastTimeContactedTextView = (TextView)contactView.findViewById(R.id.contact_last_time_contacted_text_view);
    ImageView contactStarredImageView = (ImageView)contactView.findViewById(R.id.contact_starred_image_view);
 
    if (getCheckedItemPosition() == position) {
      contactView.setBackgroundColor(context.getResources().getColor(R.color.light_blue));
    } else {
      contactView.setBackgroundColor(context.getResources().getColor(R.color.light_gray));
    }
 
    AssetFileDescriptor assetFileDescriptor = null;
    try {
      Uri contactPhotoUri = contact.getPhoto();
      if (contactPhotoUri == Uri.EMPTY) {
        contactPhotoImageView.setImageResource(R.drawable.contact_photo);
      } else {
        assetFileDescriptor = context.getContentResolver().openAssetFileDescriptor(contactPhotoUri, "r");
	FileDescriptor fileDescriptor = assetFileDescriptor.getFileDescriptor();
	if (fileDescriptor != null) {
	  contactPhotoImageView.setImageBitmap(BitmapFactory.decodeFileDescriptor(fileDescriptor, null, null));
	}
      }
    } catch (FileNotFoundException fileNotFoundException) {
      Log.e(Constants.TAG, "An exception has occurred: "+fileNotFoundException.getMessage());
      if (Constants.DEBUG) {
	fileNotFoundException.printStackTrace();
      }
    } finally {
      if (assetFileDescriptor != null) {
        try {
          assetFileDescriptor.close();
	} catch (IOException ioException) {
	  Log.e(Constants.TAG, "An exception has occurred: "+ioException.getMessage());
          if (Constants.DEBUG) {
            ioException.printStackTrace();
          }
        }
      }
    }
 
    contactNameTextView.setText(contact.getName());
    contactTimesContactedTextView.setText(context.getResources().getString(R.string.times_contacted)+" "+contact.getTimesContacted());
    long value = contact.getLastTimeContacted();
    if (value != 0) {
      contactLastTimeContactedTextView.setText(context.getResources().getString(R.string.last_time_contacted)+" "+Utilities.displayDateAndTime(value));
    } else {
      contactLastTimeContactedTextView.setText(context.getResources().getString(R.string.last_time_contacted)+" -");
    }
    if (contact.getStarred() == 1) {
      contactStarredImageView.setImageResource(R.drawable.star_set);
    } else {
      contactStarredImageView.setImageResource(R.drawable.star_unset);
    }
    return contactView;
  }
}

6. Să se implementeze un meniu, care să ofere următoarele funcționalități:

  • adăugarea informațiilor referitoare la un contact (nume, numere de telefon, adrese de poștă electronică);

  • editarea informațiilor referitoare la un contact selectat în listă (nu va fi posibilă adăugarea de alte numere de telefon / adrese de poștă electronică);

  • ștergerea informațiilor referitoare la un contact selectat în listă.

Pentru interfața grafică corespunzătoare acestor funcționalități se va utiliza activitatea ContactsManagerActivity.

Implementarea funcționalităților legate de furnizori de conținut nu face obiectul acestui laborator și nu trebuie să fie cunoscută în detaliu.

a. în directorul res/menu se va defini un fișier address_book.xml conținând structura fișierului (câte un element de tip <item> pentru fiecare funcționalitate, specificând un text care trebuie afișat și o imagine care va fi plasată în bara de stare (dacă spațiul îl permite);

addressbook.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  tools:context="ro.pub.cs.systems.pdsd.lab05.addressbook.AddressBookActivity" >
 
  <item
    android:id="@+id/action_insert"
    android:icon="@drawable/insert"
    android:orderInCategory="100"
    android:showAsAction="ifRoom"
    android:title="@string/insert"/>
 
  <item
    android:id="@+id/action_update"
    android:icon="@drawable/update"
    android:orderInCategory="100"
    android:showAsAction="ifRoom"
    android:title="@string/update"/>
 
  <item
    android:id="@+id/action_delete"
    android:icon="@drawable/delete"
    android:orderInCategory="100"
    android:showAsAction="ifRoom"
    android:title="@string/delete"/>    
 
</menu>

b. În metoda boolean onOptionsItemSelected(MenuItem item) din clasa AddressBookActivity (pachetul ro.pub.cs.systems.pdsd.lab05.addressbook.view) să se realizeze operațiile de adăugare, modificare, ștergere, în funcție de identificatorul intrării din meniu care a fost accesată.

@Override
public boolean onOptionsItemSelected(MenuItem item) {
 
  ListView addressBookListView = (ListView)findViewById(R.id.address_book_list_view);
  ContactAdapter addressBookContactAdapter = (ContactAdapter)addressBookListView.getAdapter();
 
  int id = item.getItemId();
  if (id == R.id.action_insert) {
    Intent intentAdd = new Intent(this, ContactsManagerActivity.class);
    intentAdd.putExtra(Constants.OPERATION, Constants.OPERATION_INSERT);
    startActivityForResult(intentAdd, Constants.OPERATION_INSERT);
    return true;
  }
 
  if (id == R.id.action_update) {
    if (addressBookContactAdapter.getCheckedItemPosition() == -1) {
      Toast.makeText(this, "There is no selection made", Toast.LENGTH_LONG).show();
      return false;
    }
    Intent intentUpdate = new Intent(this, ContactsManagerActivity.class);
    intentUpdate.putExtra(Constants.OPERATION, Constants.OPERATION_UPDATE);
    intentUpdate.putExtra(Constants.CONTACT_NAME, addressBookContactAdapter.getSelectedContact().getName());
    int index;
    TextView contactPhonesTextView = (TextView)findViewById(R.id.contact_phones_text_view);
    String contactPhoneNumbers = contactPhonesTextView.getText().toString();
    String[] contactPhoneNumbersParts = contactPhoneNumbers.replaceAll(" ", "").split("[:\n]");
    if ((contactPhoneNumbersParts.length % 2) == 0) {
      ArrayList<PhoneNumber> contactPhones = new ArrayList<PhoneNumber>();
      index = 0;
      while (index < contactPhoneNumbersParts.length) {
        PhoneNumber contactPhoneNumber = new PhoneNumber(contactPhoneNumbersParts[index+1], contactPhoneNumbersParts[index]);
        contactPhones.add(contactPhoneNumber);
        index += 2;
      }
      intentUpdate.putExtra(Constants.CONTACT_PHONES, contactPhones);
    }
    TextView contactEmailsTextView = (TextView)findViewById(R.id.contact_emails_text_view);
    String contactEmailAddresses = contactEmailsTextView.getText().toString();
    String[] contactEmailAddressesParts = contactEmailAddresses.replaceAll(" ", "").split("[:\n]");
    if ((contactEmailAddressesParts.length % 2) == 0) {
      ArrayList<EmailAddress> contactEmails = new ArrayList<EmailAddress>();
      index = 0;
      while (index < contactEmailAddressesParts.length) {
        EmailAddress contactEmailAddress = new EmailAddress(contactEmailAddressesParts[index+1], contactEmailAddressesParts[index]);
        contactEmails.add(contactEmailAddress);
        index += 2;
      }
      intentUpdate.putExtra(Constants.CONTACT_EMAILS, contactEmails);
    }
    intentUpdate.putExtra(Constants.CONTACT_ID, String.valueOf(addressBookContactAdapter.getContactId()));
    startActivityForResult(intentUpdate, Constants.OPERATION_UPDATE);
    return true;
  }
 
  if (id == R.id.action_delete) {
    ArrayList<ContentProviderOperation> contentProviderOperations = new ArrayList<ContentProviderOperation>();
    contentProviderOperations.add(ContentProviderOperation
        .newDelete(ContactsContract.RawContacts.CONTENT_URI)
        .withSelection(
            RawContacts.CONTACT_ID + " = ?", 
            new String[] {
                String.valueOf(addressBookContactAdapter.getContactId())
            })
        .build()
    );
    try {
      getContentResolver().applyBatch(ContactsContract.AUTHORITY, contentProviderOperations);
    } catch (RemoteException remoteException) {
      Log.e(Constants.TAG, "An exception has occurred: "+remoteException.getMessage());
      if (Constants.DEBUG) {
        remoteException.printStackTrace();
      }
      return false;
    } catch (OperationApplicationException operationApplicationException) {
      Log.e(Constants.TAG, "An exception has occurred: "+operationApplicationException.getMessage());
      if (Constants.DEBUG) {
        operationApplicationException.printStackTrace();
      }
      return false;
    }
    FragmentManager fragmentManager = getFragmentManager();
    ContactAdditionalDetailsFragment contactAdditionalDetailsFragment = (ContactAdditionalDetailsFragment)fragmentManager.findFragmentById(R.id.contact_additional_details);
    if (contactAdditionalDetailsFragment != null) {
      FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
      fragmentTransaction.remove(contactAdditionalDetailsFragment);
      fragmentTransaction.commit();
      addressBookContactAdapter.setCheckedItemPosition(-1);
    }	    
    return true;
  }
  return super.onOptionsItemSelected(item);
}

7. Să se realizeze o dispunere alternativă a listei de contacte din agenda telefonică, astfel încât pe rândurile pare respectiv pe rândurile impare, imaginea asociată unei persoane și pictograma indicând dacă aceasta a fost inclusă în lista de favoriți să fie aranjate invers.

  • în directorul res/layout se vor defini două fișiere, contact_view_odd.xml și contact_view_even.xml, conținând cele două moduri de dispunere diferită;
  • în metoda getView() din clasa ContactAdapter se vor încărca cele două fișiere succesiv, în funcție de paritatea rândului pentru care se obține interfața grafică:
    if (position % 2 == 0) {
      contactView = inflater.inflate(R.layout.contact_view_even, parent, false);
    } else {
      contactView = inflater.inflate(R.layout.contact_view_odd, parent, false);
    }
  • în clasa ContactAdapter se vor suprascrie metodele:
    • int getViewTypeCount() - care furnizează numărul de vizualizări diferite ale listei;
    • int getItemViewType(int position) - care întoarce indexul vizualizării, pentru poziția curentă.
      public final static int CONTACT_VIEW_TYPES     = 2;
      public final static int CONTACT_VIEW_TYPE_ODD  = 0;
      public final static int CONTACT_VIEW_TYPE_EVEN = 1;
       
      @Override
      public int getViewTypeCount() {
        return CONTACT_VIEW_TYPES;
      }
       
      @Override
      public int getItemViewType(int position) {
        if (position % 2 == 0) {
          return CONTACT_VIEW_TYPE_ODD;
        }
        return CONTACT_VIEW_TYPE_EVEN;
      }

8. (opțional) Să se implementeze optimizări la nivelul codului sursă pentru a eficientiza timpul de încărcare a listei, în momentul în care utilizatorul realizează operația de derulare prin aceasta:

  • să se instanțieze obiectul responsabil cu expandarea mecanismului de dispunere a unui element din cadrul listei pe constructorul clasei adaptor
    public ContactAdapter(Activity context) {
      this.context = context;
      inflater = (LayoutInflater)context.getLayoutInflater();
      data = new ArrayList<Contact>();
      initData();
    }
  • să se reutilizeze componentele listei, încărcate în memorie, dar care nu mai sunt vizibile (la furnizarea interfeței grafice, în metoda getView())
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
      View contactView;
      Contact contact = data.get(position);
      if (convertView == null) {
        LayoutInflater inflater = (LayoutInflater)context.getLayoutInflater();
        contactView = inflater.inflate(R.layout.contact_view, parent, false);
        // ...
      } else {
        contactView = convertView;
      }
      // ...
      return contactView;
    }
  • să se stocheze structura interfeței grafice într-un obiect de tip ViewHolder prin care controalele pot fi accesate, fără a mai fi necesară utilizarea metodei findViewById()
    public class ContactAdapter extends BaseAdapter {
      public static class ViewHolder {
        ImageView contactPhotoImageView, contactStarredImageView;
        TextView contactNameTextView, contactTimesContactedTextView, contactLastTimeContactedTextView;
      };
     
      // ...
     
      @Override
      public View getView(int position, View convertView, ViewGroup parent) {
        View contactView;
        ViewHolder viewHolder;
        Contact contact = data.get(position);
        if (convertView == null) {
          LayoutInflater inflater = (LayoutInflater)context.getLayoutInflater();
          contactView = inflater.inflate(R.layout.contact_view, parent, false);
          viewHolder = new ViewHolder();
          viewHolder.contactPhotoImageView = (ImageView)contactView.findViewById(R.id.contact_photo_image_view);
          viewHolder.contactNameTextView = (TextView)contactView.findViewById(R.id.contact_name_text_view);
          viewHolder.contactTimesContactedTextView = (TextView)contactView.findViewById(R.id.contact_times_contacted_text_view);
          viewHolder.contactLastTimeContactedTextView = (TextView)contactView.findViewById(R.id.contact_last_time_contacted_text_view);
          viewHolder.contactStarredImageView = (ImageView)contactView.findViewById(R.id.contact_starred_image_view);
          contactView.setTag(viewHolder);
        } else {
          contactView = convertView;
        }
        // ...
        viewHolder = (ViewHolder)contactView.getTag();
     
        viewHolder.contactPhotoImageView.setImageBitmap(...);
        viewHolder.contactNameTextView.setText(...);
        viewHolder.contactTimesContactedTextView.setText(...);			
        viewHolder.contactLastTimeContactedTextView.setText(...);
        viewHolder.contactStarredImageView.setImageResource(...);
      }
      return contactView;
    }

9. Să se încarce modificările realizate în cadrul depozitului 'Laborator05' de pe contul Github personal, folosind un mesaj sugestiv.

student@pdsd2015:~/Laborator05/labtasks$ git add AddressBook/*
student@pdsd2015:~/Laborator05/labtasks$ git commit -m "implemented taks for laboratory 05"
student@pdsd2015:~/Laborator05/labtasks$ git push Laborator05_perfectstudent master

Resurse Utile

laboratoare/laborator05_old.txt · Last modified: 2016/03/23 23:10 by Andrei Roșu-Cojocaru
CC Attribution-Share Alike 3.0 Unported
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0