Sunteți pe pagina 1din 34

Robert Győrödi

Profesor

Universitatea din Oradea, Departamentul de Calculatoare și Tehnologia Informației


Liste și Grid-uri
ListView
Adapter personalizat
Reciclarea view-urilor
Folosirea tiparului ViewHolder
Folosirea RecyclerView
OnItemClick
OnItemLongClick
Chiar dacă RecyclerView este mai eficient și mai flexibil decât
ListView în anumite scenarii, totuși, cum vom vedea, necesită
mai mult cod pentru a fi implementat
Pentru liste simple scurte este încă recomandată folosirea de
ListView
Pentru exemplificare vom folosi un AlertDialog care va fi afișat
la apăsarea unui buton, în fragmentul SettingsFragment
Deci creăm un buton în settings_fragment.xml
Setăm un click listener pe acest buton
În acest caz alegem ca fragmentul să implementeze interfața de tratare a click-
ului, deoarece probabil că vor fi multe astfel de butoane în fragmentul de setări
Pentru a deosebi diversele surse, în metoda onClick se primește un view, care
este view-ul pe care s-a dat click și care are un ID
Verificând acest ID putem să executăm codul aferent butonului respectiv
public class SettingsFragment extends Fragment
public void showListDialog(){
implements View.OnClickListener {
AlertDialog.Builder builder =
new AlertDialog.Builder(getActivity());
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
final ArrayAdapter<String> arrayAdapter =
Bundle savedInstanceState) {
new ArrayAdapter<String>(
// Inflate the layout for this fragment
getActivity(),
View view = inflater.inflate(R.layout.fragment_settings,
android.R.layout.simple_list_item_1);
container, false);
arrayAdapter.add("Option 0");
view.findViewById(R.id.settingsButtonListExample)
arrayAdapter.add("Option 1");
.setOnClickListener(this);
arrayAdapter.add("Option 2");
return view;
builder.setTitle("Choose an option");
}
builder.setAdapter(arrayAdapter,
@Override
new DialogInterface.OnClickListener() {
public void onClick(View view) {
@Override
switch (view.getId()){
public void onClick(DialogInterface dialogInterface,int i) {
case (R.id.settingsButtonListExample) :
Toast.makeText(getActivity(),"Option choosen "+i,
showListDialog();
Toast.LENGTH_SHORT).show();
break;
dialogInterface.dismiss();
}
}
}
});

builder.show();
}
}
android.R.layout.activity_list_item
android.R.id.icon (ImageView)
android.R.id.text1 (TextView)

android.R.layout.simple_expandable_list_item_1
android.R.id.text1 (TextView)

android.R.layout.simple_expandable_list_item_2
android.R.id.text1 (TextView)
android.R.id.text2 (TextView)
http://www.javajirawat.com/2013/03/built-in-android-listview-layouts-part-1.html
android.R.layout.simple_list_item_1
android.R.id.text1 (TextView)

android.R.layout.simple_list_item_2
android.R.id.text1 (TextView)
android.R.id.text2 (TextView)

http://www.javajirawat.com/2013/03/built-in-android-listview-layouts-part-1.html
android.R.layout.simple_list_item_activated_1
android.R.id.text1 (TextView)

android.R.layout.simple_list_item_activated_2
android.R.id.text1 (TextView)
android.R.id.text2 (TextView)

http://www.javajirawat.com/2013/03/built-in-android-listview-layouts-part-2.html
android.R.layout.simple_list_item_checked
android.R.id.text1 (CheckedTextView)

android.R.layout.simple_list_item_multiple_choice
android.R.id.text1 (CheckedTextView)

http://www.javajirawat.com/2013/03/built-in-android-listview-layouts-part-2.html
android.R.layout.simple_list_item_single_choice
android.R.id.text1 (CheckedTextView)

android.R.layout.two_line_list_item
android.R.id.text1 (TextView)
android.R.id.text2 (TextView)

http://www.javajirawat.com/2013/03/built-in-android-listview-layouts-part-2.html
Pentru exemplul nostru, am putea crea o listă de contacte care
să aibă două feluri de rânduri:
Header cu steagul și numele țării
Detalii despre companie
Vom cere de la server contactele și vom construi o listă de
elemente care va fi transmisă la adaptor pentru a construi lista
de afișat.
Mai jos aveți implementarea unor clase exemple
public class JobContact {
public class Country {
public JobContact() {
// A default constructor is required.
String countryCode;
}
String name;
private int id;
private String name;
public Country(String countryCode) {
private String email;
this.countryCode = countryCode.toLowerCase();
private String description;
this.name = getNameByCountryCode(countryCode);
private String country;
}
public int getId() {
public String getName(){
return id;
return name;
}
}
public String getName() {
return name;
public int getImageRes(Context ctx){
}
return ctx.getResources().getIdentifier(countryCode, "drawable", ctx.getPackageName());
public String getDescription() {
}
return description;
}
public static String getNameByCountryCode(String countryCode){
public String getCountry() {
if (countryCode.equals("ES")){
return country;
return "Spain";
}
} else if (countryCode.equals("GB")){
public String getEmail() {
return "Great Britain";
return email;
} else {
}
return "Unknown";
}
}
}
}
Putem folosi Volley
StringRequest stringRequest = new StringRequest(
Request.Method.GET, url,
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
Gson gson = new Gson();
Type listType = new TypeToken<List<JobContact>>(){}.getType();
List<JobContact> listContacts = gson.fromJson(response, listType);

mListItems = new ArrayList<Object>();


String currentCountry = "";
for (JobContact jobContact: listContacts) {
if (!currentCountry.equals(jobContact.getCountry())){
currentCountry = jobContact.getCountry();
mListItems.add(new Country(currentCountry));
}
mListItems.add(jobContact);
}
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.i("test", error.toString());
}
}
);

MAApplication.getInstance().getRequestQueue().add(stringRequest);
public class JobContactsAdapter extends BaseAdapter {
private List<Object> mItemsList;

Cel puțin următoarele metode trebuie private Context mContext;


public JobContactsAdapter(List<Object> list, Context context){
mItemsList = list;

implementate }
mContext = context;

@Override
public int getCount() {
Avem nevoie de Context pentru încărcarea }
return mItemsList.size();

view-urilor (elementele din listă) @Override


public Object getItem(int i) {
return mItemsList.get(i);
}
@Override
public long getItemId(int i) {
//Not needed
return 0;
}
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
return null;
}
}

În cazul nostru vom dori să afișăm două


tipuri de view-uri @Override
public int
return
getItemViewType(int position) {
mItemsList.get(position) instanceof Country ? 0 : 1;

Pentru asta trebuie să mai implementăm două }


@Override
public int getViewTypeCount() {
metode: }
return 2;
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
View rowView = null;
switch (getItemViewType(i)){
case (0) :
rowView = View.inflate(mContext, R.layout.row_job_country,null);
Country country = (Country) mItemsList.get(i);
((TextView) rowView.findViewById(R.id.rowJobCountryTitle)).
setText(country.getName());
((ImageView) rowView.findViewById(R.id.rowJobCountryImage)).
setImageResource(country.getImageRes(mContext));
break;
case (1) :
rowView = View.inflate(mContext, R.layout.row_job_contact,null);
JobContact company = (JobContact) mItemsList.get(i);
((TextView) rowView.findViewById(R.id.rowJobContactName)).
setText(company.getName());
((TextView) rowView.findViewById(R.id.rowJobContactEmail)).
setText(company.getEmail());
((TextView) rowView.findViewById(R.id.rowJobContactDesc)).
setText(company.getDescription());
}
return rowView;
}

public int getImageRes(Context ctx){


return ctx.getResources().getIdentifier(countryCode, "drawable",
ctx.getPackageName());
}
public class ContactFragment extends android.support.v4.app.ListFragment {

List<Object> mListItems;

Avem două posibilități public ContactFragment() {

}
// Required empty public constructor

Putem crea un ListView în @Override


public void onViewCreated(View view, Bundle bundle) {
super.onViewCreated(view,bundle);

fragmentul nostru și să îi setăm }


retrieveJobContacts();

adaptorul la lista încărcată


public void retrieveJobContacts(){
String url = “...";
StringRequest stringRequest = new StringRequest(
Request.Method.GET, url,

Putem folosi new Response.Listener<String>() {


@Override
public void onResponse(String response) {
Gson gson = new Gson();
android.support.v4.ListFragment Type listType = new TypeToken<List<JobContact>>(){}.getType();
List<JobContact> listContacts = gson.fromJson(response, listType);
mListItems = new ArrayList<Object>();

și să derivăm fragmentul nostru din String currentCountry = "";


for (JobContact jobContact: listContacts) {
if (!currentCountry.equals(jobContact.getCountry())){

el }
currentCountry = jobContact.getCountry();
mListItems.add(new Country(currentCountry));

mListItems.add(jobContact);
Acesta deja încarcă un view având un }
setListAdapter(new JobContactsAdapter(mListItems,getActivity()));

ListView și conține setListAdapter() },


}

new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.i("test", error.toString());
}
}
);

MAApplication.getInstance().getRequestQueue().add(stringRequest);
}
}
Numărul de rânduri dintr-o listă poate fi variabil, chiar foarte
mare
Când derulăm o listă, view-urile care nu sunt vizibile, pe fiecare
parte a ecranului, se recomandă a fi refolosite
Astfel se salvează timpii de încărcare a view-urilor
Poate fi diferența dintre o listă fluentă și una sacadată
Metoda getView() primește ca un parametru, o vedere care se
poate recicla sau null dacă nu există view-uri de reciclat
Dacă avem mai multe tipuri de view-uri, Android verifică intern
tipul corect și trimite o instanță corespunzătoare, dacă există,
altfel trimite null
@Override
public View getView(int i, View view, ViewGroup viewGroup) {

switch (getItemViewType(i)){

case (0) :
if (view == null) {
view = View.inflate(mContext, R.layout.row_job_country, null);
}
Country country = (Country) mItemsList.get(i);
((TextView) view.findViewById(R.id.rowJobCountryTitle)).
setText(country.getName());
((ImageView) view.findViewById(R.id.rowJobCountryImage)).
setImageResource(country.getImageRes(mContext));
break;
case (1) :
if (view == null) {
view = View.inflate(mContext, R.layout.row_job_contact, null);
}
JobContact company = (JobContact) mItemsList.get(i);
((TextView) view.findViewById(R.id.rowJobContactName)).
setText(company.getName());
((TextView) view.findViewById(R.id.rowJobContactEmail)).
setText(company.getEmail());
((TextView) view.findViewById(R.id.rowJobContactDesc)).
setText(company.getDescription());
}
return view;
}
Pentru a elimina căutarea de fiecare dată a elementelor din
view-urile reciclate se poate aplica un tipar numit ViewHolder
Se creează o clasă numită ViewHolder care va menține
referințele la componente
Putem asocia o instanță de clasă (în acest caz ViewHolder) cu un rând
prin apelul la metoda setTag()
Putem accesa acel tag prin metoda getTag()
Putem asocia mai multe tag-uri la un view, specificând o cheie pentru
respectivul tag setTag(key, obj)
Putem accesa acel tag prin specificarea cheii: getTag(key)
@Override private class CountryViewHolder{
public View getView(int i, View view, ViewGroup viewGroup) {
switch (getItemViewType(i)){ public TextView name;
case (0) : public ImageView flag;
CountryViewHolder holderC;
if (view == null){ public void bindView(Country country){
view = View.inflate(mContext, R.layout.row_job_country,null); this.name.setText(country.getName());
holderC = new CountryViewHolder(); this.flag.setImageResource(country.getImageRes(mContext));
holderC.name = (TextView) view.findViewById(R.id.rowJobCountryTitle); }
holderC.flag = (ImageView) view.findViewById(R.id.rowJobCountryImage);
view.setTag(holderC); }
} else {
holderC = (CountryViewHolder) view.getTag(); private class CompanyViewHolder{
}
holderC.bindView((Country)mItemsList.get(i)); public TextView name;
break; public TextView email;
case (1) : public TextView desc;
CompanyViewHolder holder;
if (view == null){ public void bindView(JobContact company){
view = View.inflate(mContext, R.layout.row_job_contact,null); this.name.setText(company.getName());
holder = new CompanyViewHolder(); this.email.setText(company.getEmail());
holder.name = (TextView) view.findViewById(R.id.rowJobContactName); this.desc.setText(company.getDescription());
holder.email = (TextView) view.findViewById(R.id.rowJobContactEmail); }
holder.desc = (TextView) view.findViewById(R.id.rowJobContactDesc);
view.setTag(holder); }
} else {
holder = (CompanyViewHolder) view.getTag();
}
holder.bindView((JobContact)mItemsList.get(i));
}
return view;
}
Pentru operații de durată, cum ar fi încărcarea de imagini în
fiecare view, trebuie să creăm un AsyncTask în getView()
Pentru încărcarea de imagini putem implementa de exemplu o clasă
LoadImageAsyncTask derivată din AsyncTask
Aceasta va primi ca și parametri:
URL-ul imaginii
Referință la viewHolder-ul corespunzător

new LoadImageAsyncTask(list.get(position).getImageUrl, holder)


.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null);
A fost introdus în Android 5.0 Lollipop
O versiune mai flexibilă și mai avansată a ListView
Bazat pe o clasă Adapter similar ca pentru ListView
Forțează folosirea clasei ViewHolder pentru îmbunătățirea performanțelor și a modularității
Permite animări, decorații ale elementelor, managere de layout
Animările sunt realizate folosind:
RecyclerView.ItemAnimator – se poate deriva pentru animări personalizate
Când se schimbă, adaugă, șterg date/elemente pentru declanșarea animațiilor
putem apela:
notifyItemInserted()
notifyItemRemoved()
Pentru a adăuga separatoare, grupări sau să evidențiem un element, putem folosi
RecyclerView.ItemDecoration
https://android-pratap.blogspot.ro/2015/01/using-linearlayoutmanager.html

Implicit există 3 layout manageri:


LinearLayoutManager
GridLayoutManager
StaggeredLayoutManager
setGapStrategy()
Clasa RecyclerView este inclusă în librăria suport v7
Putem include această librărie în proiectul nostru dacă:
deschidem structura proiectului (File  Project Structure),
selectăm aplicația (app),
dăm click pe tab-ul de dependințe,
de aici dăm click pe semnul + (add),
alegem Library dependency și
căutăm după RecyclerView (com.android.support.recyclerview-v7
Acest mod este echivalent cu adăugarea liniei corespunzătoare în
build.gradle (Module: app):
dependencies {
...
implementation 'com.android.support:recyclerview-v7:27.1.1'
...
}
În fișierul de layout fragment_list.xml putem înlocui ListView cu
RecyclerView <android.support.v7.widget.RecyclerView
android:id="@+id/my_recycler_view"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
Următorul pas este crearea adaptorului
În loc să extindem BaseAdapter, va trebui să extindem
RecyclerView.Adapter<RecyclerView.ViewHolder>
Aceasta este un adapter care implementează tiparul ViewHolder
Primul pas este crearea clasei adapter (JobOffersAdapter), în interiorul căruia se va crea o clasă
internă (MyViewHolder) care extinde RecyclerView.ViewHolder
După acest pas se specifică faptul că clasa adapter extinde
RecyclerView.Adapter<JobOffersAdapter.MyViewHolder>, deci trebuie implementate:
onCreateViewHolder
onBindViewholder
getItemCount
public class JobOffersAdapter extends RecyclerView.Adapter<JobOffersAdapter.MyViewHolder> {
private List<JobOffer> mOfferList;

public JobOffersAdapter(List<JobOffer> offersList) {


this.mOfferList = offersList;
}

@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_job_offer, parent, false);
return new MyViewHolder(v);
}

@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
holder.textViewName.setText(mOfferList.get(position).getTitle());
holder.textViewDescription.setText(mOfferList.get(position).getDescription());
}

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

public class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener{

public TextView textViewName;


public TextView textViewDescription;

public MyViewHolder(View v){


super(v);
textViewName = (TextView)v.findViewById(R.id.rowJobOfferTitle);
textViewDescription = (TextView)v.findViewById(R.id.rowJobOfferDesc);
}
}
}
Acțiuni care trebuie efectuate:
Inițializarea RecyclerView-ului
În onCreateView
Setarea unui layout manager
Tot în onCreateView
Setarea adapter-ului
Adapter-ul trebuie inițializat cu lista de elemente
Acestea pot fi preluate eventual de la un serviciu de pe web
Din această cauză metoda care preia elementele listei va fi asincronă și va
inițializa adapter-ul după ce va termina preluarea elementelor listei
public class ListFragment extends android.support.v4.app.Fragment {

public List<JobOffer> mListItems;


public RecyclerView mRecyclerView;

public ListFragment() {
// Required empty public constructor
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_list, container, false);

mRecyclerView = (RecyclerView) view.findViewById(R.id.my_recycler_view);

// use this setting to improve performance if you know that changes


// in content do not change the layout size of the RecyclerView
mRecyclerView.setHasFixedSize(true);

// use a linear layout manager


mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));

//Retrieve the list of offers


retrieveJobOffers();

return view;
}
public void retrieveJobOffers(){
String url = “...";
StringRequest stringRequest = new StringRequest(
Request.Method.GET, url,
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
Gson gson = new Gson();
Type listType = new TypeToken<List<JobOffer>>(){}.getType();
mListItems = gson.fromJson(response, listType);
JobOffersAdapter adapter = new JobOffersAdapter(mListItems);
mRecyclerView.setAdapter(adapter);
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
}
}
);

MAApplication.getInstance().getRequestQueue().add(stringRequest);
}
Putem implementa tratarea de evenimente pe elementele RecyclerView
Click listener – pentru a deschide view-ul de detalii
Longclick listener – pentru a șterge un element
Pe ListView era destul de simplu
ListView.setOnItemClickListener
ListView.setOnItemLongClickListener
Pentru RecyclerView nu este chiar așa de simplu, există două abordări
Crearea unei clase care implementează RecyclerView.OnItemTouchListener și care
apelează metoda addOnItemTouchListener de pe RecyclerView
Avantajul acestei abordări este posibilitatea refolosirii, respectiv implementarea logicii la nivelul
activității sau a fragmentului și nu a view-ului
Gestionarea evenimentelor în interiorul ViewHolder-ului
Avantajul este că putem detecta evenimente pe diferite componente din interiorul view-ului
asociat cu un rând
mRecyclerView.addOnItemTouchListener( public class MyRecyclerItemClickListener implements RecyclerView.OnItemTouchListener {
new MyRecyclerItemClickListener( public static interface OnItemClickListener {
getActivity(), mRecyclerView, public void onItemClick(View view, int position);
new MyRecyclerItemClickListener.OnItemClickListener() { public void onItemLongClick(View view, int position);
@Override }
public void onItemClick(View view, int position){
// ... private OnItemClickListener mListener;
} private GestureDetector mGestureDetector;
@Override
public void onItemLongClick(View view, int position){ public MyRecyclerItemClickListener(Context context, final RecyclerView recyclerView, OnItemClickListener listener) {
// ... mListener = listener;
} mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener()
} {
) @Override
); public boolean onSingleTapUp(MotionEvent e) {
return true;

Această abordare este }

@Override

puțin mai complicată public void onLongPress(MotionEvent e) {


View child = recyclerView.findChildViewUnder(e.getX(), e.getY());
if(child != null && mListener != null) {
mListener.onItemLongClick(child, recyclerView.getChildPosition(child));
Necesită tratarea }
}

gesturilor }
});

@Override
public boolean onInterceptTouchEvent(RecyclerView view, MotionEvent e) {
View child = view.findChildViewUnder(e.getX(), e.getY());
if(child != null && mListener != null && mGestureDetector.onTouchEvent(e)) {
mListener.onItemClick(child, view.getChildPosition(child));
}
return false;
}

@Override
public void onTouchEvent(RecyclerView view, MotionEvent motionEvent) {
//Empty
}
}
public class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener{

public TextView textViewName;


public TextView textViewDescription;

public MyViewHolder(View v){


super(v);
textViewName = (TextView)v.findViewById(R.id.rowJobOfferTitle);
textViewDescription = (TextView)v.findViewById(R.id.rowJobOfferDesc);
textViewName.setOnClickListener(this);
v.setOnLongClickListener(this);
}

@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.rowJobOfferTitle :
//Click
break;
}
}

@Override
public boolean onLongClick(View view) {
mOfferList.remove(getPosition());
notifyItemRemoved(getPosition());
return true;
}
}
În cazul în care avem mai multe tipuri de elemente într-o listă, atunci și în
acest caz trebuie implementată metoda getItemViewType()
În loc să specificăm tipul concret de ViewHolder pentru adaptorul nostru,
vom specifica tipul generic
RecyclerView.Adapter<RecyclerView.ViewHolder>
Trebuie implementate atâtea tipuri de ViewHolder-e câte tipuri de
elemente avem
În onCreateViewHolder în funcție de viewType creăm ViewHolder-ul
corespunzător
În onBindViewHolder, pentru a afla tipul ViewHolder-ului putem apela
holder.getItemViewType() și în funcție de acesta să facem cast la tipul
concret și să inițializăm corect elementele de interfață

S-ar putea să vă placă și