2024. április 19., péntek

Gyorskeresés

Útvonal

Cikkek » Számtech rovat

Android alkalmazásfejlesztés 3. rész: egy hasznos példa

A következőkben néhány hasznos dolgot fogok nektek bemutatni egy egyszerű, mégis komplex példán keresztül.

[ ÚJ TESZT ]

Kapcsolatok - Recycler View

A RecyclerView zanzásítva egy görgethető lista nagy adat szerkezetekhez. Mindig csak az épp megjelenítendő adat van kirajzolva és görgetéskor renderelődik újra. Ennek a megvalósításához két új layoutre és 3 új komponensre van szükségünk.

A két új layout a contacts_list és a contacts_list_item névre fog hallgatni. Az előbbi tartalmazza magát a RecyclerView-t, míg az utóbbi az egy listaelem kinézetét.

Egyszerűen a contacts_list-be (, vagy legyen bármi a neve) csak beszúrjuk a RecyclerView elemet így a fájl így fog kinézni:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.v7.widget.RecyclerView
android:id="@+id/contact_list"
android:layout_height="match_parent"
android:layout_width="match_parent" />

</android.support.constraint.ConstraintLayout>

A contacts_list_item pedig jelen esetben egy sima LinearLayout, de ahogy fentebb említettem, bármi lehet, ez fogja reprezentálni egy sorát a listának. Tehát nekünk szükségünk van egy képre, a kapcsolat nevére és a telefonszámára. Ezek szépen egymás mellé sorakozhatnak, így nem igazán kell a formázással törődni.
Ha szépen felvettük egymás mellé a szükséges tageket, akkor valami ilyesmit kell kapni:

<?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"
android:orientation="horizontal"
android:padding="5dp">

<ImageView
android:id="@+id/contact_profile"
android:layout_width="40dp"
android:layout_height="40dp" />

<TextView
android:id="@+id/contact_label"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="10dp"
android:gravity="center_vertical" />

<TextView
android:id="@+id/contact_number"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="10dp"
android:gravity="center_vertical" />

</LinearLayout>

A lista kinézete kész, már csak az adatszerkezet hozzákötése van hátra.
Ahogy említettem, három új komponensre lesz szükség: Model, ViewHolder és egy Adapter.

Kezdjük a kézenfekvővel, a modellel. Egy sima POJO osztályról van szó. Létrehozhatunk neki külön csomagot a jobb átláthatóság érdekében. Az entitás neve legyen egyértelmű, Contact. Az adattagjai pedig azok, amiket majd meg szeretnék jelenteni. Tehát a kép, a név és a telefonszám. A képet sima stringként kezeljük, majd a bindolásnál lesz belőle Uri. Az adattagok természetesen privát módosítót kapnak és getter/setter segítségével kezeljük őket.
Tehát az elkészült model:

public class Contact {

private String profilePic;
private String name;
private String contactNumber;

public Contact() { }

public String getProfilePic() {
return profilePic;
}

public void setProfilePic(String profilePic) {
this.profilePic = profilePic;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getContactNumber() {
return contactNumber;
}

public void setContactNumber(String contactNumber) {
this.contactNumber = contactNumber;
}
}

A következő két osztály szorosan kapcsolódik egymáshoz. Létrehozhatunk nekik is egy saját csomagot nyugodtan.
Az első komponensünk a ViewHolder. A ViewHolder fogja kezelni az adatok megjelenését a lista elemeken belül. Tehát nem a listát kezeli, hanem annak az elemeit. Ez az osztály konkrétan a leszármazottja a RecyclerView.ViewHolder-nek, így alapból kiválaszthatjuk ős osztálynak.

Kötelező megadni a konstruktorát az osztálynak, tehát kiegészítjük az osztályt, vagy pedig az IDE-vel legeneráltatjuk a szükséges konstruktort.

Ahogy említettem, ez a listaelemeket kezeli, tehát szükségünk van a 3 adatra, amit meg akarunk jeleníteni.

protected ImageView contactimage;
protected TextView contactname,contactnum;

Protected módosítót használok nekik, mert csak a saját csomagjából akarom elérni őket és csak az adapter lesz még az adott csomagban.

Most pedig beállítjuk a konstruktort. Nemes egyszerűséggel csak hozzákötjük az adott elemeket az adott változókhoz, hogy bindoláskor tudjunk rájuk hivatkozni.

contactimage = itemView.findViewById(R.id.contact_profile);
contactname = itemView.findViewById(R.id.contact_label);
contactnum = itemView.findViewById(R.id.contact_number);

Ami még ebben az osztályban kerül megvalósításra, az az érintésre való felhívása az adott kapcsolatnak. Ezt is egyszerűen az itemView-ra állított OnClickListener-rel megoldható. A mechanizmus az előzőekben már látott ACTION_CALL intent segítségével történik. Ha pedig nincs meg a jogosultság, akkor dobunk egy Toast üzenetet. Ezt így az eddigiek alapján könnyen megírható, vagy esetleg átemelhető az előzőekből, ha pedig egyik sem tetszik, akkor itt látható a teljes konstruktor:

public ContactViewHolder(final View itemView) {
super(itemView);

contactimage = itemView.findViewById(R.id.contact_profile);
contactname = itemView.findViewById(R.id.contact_label);
contactnum = itemView.findViewById(R.id.contact_number);

itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View View) {
Context context = View.getContext();
Intent intent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + contactnum.getText()));
if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
context.startActivity(intent);
} else {
Toast.makeText(context, "A művelethez szükséges jogosultság hiányzik!",Toast.LENGTH_LONG).show();
}
}
});
}

Miután megvan a ViewHolder, ideje az utolsó komponenst is megcsinálni, az Adaptert. Maga az adapter az ami összeköti az adatokat és a megjelenítést. Ez foglalja magában a ViewHolder-t és ebben történik az első oldalon említett Cursor kezelése is. Maga az osztályunk a RecyclerView.Adapter osztály leszármazottja. Ez megint beállítható az IDE-ből.

Ahogy látható több Adapter létezik, ügyeljünk arra, hogy jót válasszunk. A kiválasztáskor is feltűnhet, hogy megadható neki egy ViewHolder (VH). Remek, hisz ezért készítettük el az imént a sajátunkat. Tehát mielőtt implementálnánk, vagy generáltatnánk az IDE-vel a szükséges metódusokat, ki kell egészíteni az osztály definíciót:

public class ContactsAdapter extends RecyclerView.Adapter<ContactViewHolder> {
...
}

Ezek után először generáltatható az IDE-vel a szükséges metódusok, majd nekikezdhetünk a kódolásnak!
Három kötelező metódust kell felülírni, ezeket az IDE legenerálta nekünk. Nézzük sorba!

ContactViewHolder onCreateViewHolder

Ez a nevéből is adódóan a ViewHolder-t fogja beállítani. Egyszerűen implementálható, mindössze be kell neki állítani az előre elkészített contact_list_item layout-ot, hasonlóan a Fragment készítéshez.

@NonNull
@Override
public ContactViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View listItemView = LayoutInflater.from(parent.getContext()) .inflate(R.layout.contact_list_item, parent, false);
return new ContactViewHolder(listItemView);
}

A másik két (onBindViewHolder, getItemCount) metódushoz kicsit gondolkozni kell. Alapvetően szükségünk lesz egy adatszerkezetre, ami tárolja a kapcsolatokat. Ez pedig egy lista lesz, amit már az osztály elején létrehozunk. private List<Contact> contactList;

Innentől már a getItemCount is egyértelművé válik.

@Override
public int getItemCount() {
return contactList.size();
}

Vegyük szemügyre a másik, a bindolást megvalósító metódust:

@Override
public void onBindViewHolder(@NonNull ContactViewHolder holder, int position) {
...
}

Úgy tűnik, a bindoláshoz is csak a listára van szükség, hisz a holder-t és a pozíciót is megkapja a metódus.

Tehát a feladat: az adott elemet beállítani a layoutra. Szerencsére a saját ViewHolder-ben már kiszedtük a layout elemeket, így azokra csak hivatkozni kell:

TextView contact_name = holder.contactname;
TextView contact_num = holder.contactnum;
ImageView contact_image = holder.contactimage;

Most pedig kiszedjük a szükséges elemet: Contact contacts = contactList.get(position);
Innentől a név és a telefonszám beállítása eléggé adja magát

contact_name.setText(contacts.getName());
contact_num.setText(contacts.getContactNumber());

A profilkép, ahogy már fentebb említettem, Uri-ra lesz alakítva, már amennyiben létezik. Ha nem létezik, akkor egy bármilyen képet be lehet állítani. Ezt legegyszerűbben egy új drawable létrehozásával tehetjük meg. res/drawable > new / Vector Asset.

Kiválasztunk egy ikont, ami megfelel a feladatra, majd a varázslóval létrehozzuk a drawable-t.

Most, hogy megvan az alapértelmezett kép, beállítható a profilkép is:

if (contacts.getProfilePic() != null) {
Uri myuri = Uri.parse(contacts.getProfilePic());
contact_image.setImageURI(myuri);
} else {
contact_image.setImageDrawable(R.drawable.default_profile);
}

Itt van a csapda, ugyanis a resources innen nem érhető el, tehát a drawable sem. Szükségünk van a kontextusra, hogy elérjük az erőforrásokat. Tehát egy private Context mContext; változó létrehozása is szükséges. Az értékét pedig a konstruktorban állítjuk be, tehát létrehozunk egy konstruktort:

public ContactsAdapter(Context context) {
mContext = context;
}

Innentől már elérhető lesz az erőforrás a következő módon:

contact_image.setImageDrawable(mContext.getResources().getDrawable(R.drawable.default_profile));

Elgondolkozhatunk, hogy honnan fogja megkapni a kontextust az adapter. A válasz egyszerű: a fragmentből, ahol inicializálni fogjuk. Tehát mehetünk is a ContactsFragment-re. Jelenleg nincs benne semmi izgalmas, csak egy üres fragment egy layouttal. Elsősorban szükség lesz egy metódusra, ami ellenőrzi, hogy megvan-e még a jogosultság a kapcsolatok olvasására, vagy kérelmezni kell újra. Ehhez, ahogy eddig is történt, kell egy konstans. private static final int PERMISSIONS_REQUEST_READ_CONTACTS = 1;

A metódus meg valahogy így néz ki:

private void getContacts() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && getActivity().checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
// TODO
} else {
requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, PERMISSIONS_REQUEST_READ_CONTACTS);
}
}

A layoutot természetesen át kell írnunk a contact_list-re, mivel most már az lesz az alapja. Beláthatjuk, hogy ez nem elég. Nem véletlenül van egy TODO a metódusban. Ott még be kell állítani három dolgot a RecyclerView-nak, viszont ehhez először meg kell szereznünk magát a View-t. Kell egy változó: private RecyclerView contactListView;

Aztán az onCreateView-t is át kell alakítani, hisz meg kell kapnunk a View-t.

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.contacts_list, container, false);
contactListView = root.findViewById(R.id.contact_list);

return root;
}

Megvan a szükséges változónk, beállíthatjuk rajta az adaptert a layout manager-t valamint az animator-t a TODO helyén.
Ezek a következőként néznek ki:

contactListView.setAdapter(new ContactsAdapter(getActivity()));
contactListView.setLayoutManager(new LinearLayoutManager(getActivity()));
contactListView.setItemAnimator(new DefaultItemAnimator());

És át is adtuk a kontextust, valamint be lettek állítva a szükséges dolgok. Az onCreateView -ban a return előtt meghívjuk a metódust és kész is a fragment!

Az utolsó lépés már csak a tényleges kapcsolatok lekérdezése az adapterben.

Az adapter konstruktorában eddig csak a kontextus van. Szükségünk van még itt változókra. Először is a contactList-et kell példányosítani, contactList = new ArrayList<>();, utána pedig a már a megszokott 3 változónkra lesz szükség a kapcsolat adattagjainak kezelésére.

String name = null;
String number = null;
String image_uri = null;

Innentől pedig bonyolódik a helyzet. Lentebb megtalálható az egész kód, aki a nem rajong túl mély vízért, a magyarázkodást kihagyná, csak másolja be.

Szükségünk lesz egy ContentResolver-re. Ennek a segítségével fogunk tudni query-zni az eszközünk adatbázisából.

ContentResolver cr = mContext.getContentResolver();

A query pedig a következőként épül fel:

- Projekció
- Szelekció
- Szelekciós argumentumok
- Rendezés

Mi most csak a rendezéssel fogunk foglalkozni: betűrendbe rendezzük az adathalmazt. Ehhez a következő konstans szükséges:

private static final String SORT_ORDER = ContactsContract.Contacts.DISPLAY_NAME_PRIMARY + " ASC ";

Majd létrehozzuk a query -t és betöltjük egy Cursor-ba.

Cursor cur = cr.query(ContactsContract.Contacts.CONTENT_URI,
null,
null,
null,
SORT_ORDER);

Amennyiben a cursor mérete nagyobb, mint 0, tehát nem üres, akkor egy-egy ciklusban végiglépkedünk rajta és kiszedjük a számunkra releváns adatokat. Ha a cursor még friss, akkor az első elemére kell lépni először. Az ID, a kapcsolat neve, valamint a profilkép könnyen kinyerhető, viszont a telefonszámhoz egy újabb query-re lesz szükség. Nem megyek bele a mit miértekbe, akit érdekel, nyugodtan ássa bele magát, az a legjobb módszer a fejlődésre.

int i = 0;
if (cur.getCount() > 0) {
while (cur.moveToNext()) {
if ( i == 0) {
cur.moveToFirst();
i++;
}
String id = cur.getString(cur.getColumnIndex(ContactsContract.Contacts._ID));
name = cur.getString(cur.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
image_uri = cur .getString(cur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.PHOTO_URI));

Cursor pCur = cr.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null,
ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?", new String[]{id}, null);

if (pCur.moveToNext()) {
number = pCur.getString(pCur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
}
pCur.close();

Contact contact = new Contact();
contact.setProfilePic(image_uri);
contact.setName(name);
contact.setContactNumber(number);

contactList.add(contact);
}
cur.close();
}

Ezzel el is készültünk! Ha most indítjuk az alkalmazást, valami ilyesmit kell kapnunk:

Kicsit redundáns lett így a vége, de inkább egészben is megmutatom a kódot:

public class ContactsAdapter extends RecyclerView.Adapter<ContactViewHolder> {

private static final String SORT_ORDER = ContactsContract.Contacts.DISPLAY_NAME_PRIMARY + " ASC ";

private List<Contact> contactList;
private Context mContext;

public ContactsAdapter(Context context) {

mContext = context;

contactList = new ArrayList<>();

String name = null;
String number = null;
String image_uri = null;

ContentResolver cr = mContext.getContentResolver();

Cursor cur = cr.query(ContactsContract.Contacts.CONTENT_URI,
null,
null,
null,
SORT_ORDER);

int i = 0;
if (cur.getCount() > 0) {
while (cur.moveToNext()) {
if ( i == 0) {
cur.moveToFirst();
i++;
}
String id = cur.getString(cur.getColumnIndex(ContactsContract.Contacts._ID));
name = cur.getString(cur.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
image_uri = cur .getString(cur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.PHOTO_URI));

Cursor pCur = cr.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null,
ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?", new String[]{id}, null);

if (pCur.moveToNext()) {
number = pCur.getString(pCur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
}
pCur.close();

Contact contact = new Contact();
contact.setProfilePic(image_uri);
contact.setName(name);
contact.setContactNumber(number);

contactList.add(contact);
}
cur.close();
}
}

@NonNull
@Override
public ContactViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View listItemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.contact_list_item, parent, false);

return new ContactViewHolder(listItemView);
}

@Override
public void onBindViewHolder(@NonNull ContactViewHolder holder, int position) {
TextView contact_name = holder.contactname;
TextView contact_num = holder.contactnum;
ImageView contact_image = holder.contactimage;

Contact contacts = contactList.get(position);

contact_name.setText(contacts.getName());
contact_num.setText(contacts.getContactNumber());
if (contacts.getProfilePic() != null) {
Uri myuri = Uri.parse(contacts.getProfilePic());
contact_image.setImageURI(myuri);
} else {
contact_image.setImageDrawable(mContext.getResources().getDrawable(R.drawable.default_profile));
}
}

@Override
public int getItemCount() {
return contactList.size();
}
}

A cikk még nem ért véget, kérlek, lapozz!

Előzmények

Hirdetés

Copyright © 2000-2024 PROHARDVER Informatikai Kft.