Ostatnio potrzebowałem stworzyć listę rozwijaną w Androidzie, która będzie podzielona według kategorii. Lista kategorii i ich elementów może zmieniać się dynamicznie. Musiałem to przedstawić na jednym ekranie. Pierwsze co przychodzi do głowy to stworzenie fragmentów według konkretnej kategorii. Jednak nie był to dobry pomysł. A może ViewPager? Też nie do końca mnie przekonywał. Musiałem poszukać czegoś innego. System Android posiada klasę ExpandableListView, która spełniała moje po części oczekiwania. Zobaczmy w jaki sposób stworzyć taką listę.
Potrzebujemy danych
Na początku stwórzmy przykładową klasę z danymi.
public class DataList { private List<Product> mProductList = new ArrayList<Product>() { { add(new Product("Orange", "Fruits")); add(new Product("Apple", "Fruits")); add(new Product("Banana", "Fruits")); add(new Product("Grapes", "Fruits")); add(new Product("Carrot", "Vegetables")); add(new Product("Potato", "Vegetables")); add(new Product("Onion", "Vegetables")); add(new Product("Apple", "Phone")); add(new Product("Google", "Phone")); add(new Product("HTC", "Phone")); add(new Product("Mercedes", "Car")); add(new Product("BMW", "Car")); add(new Product("SEAT", "Car")); } }; private List<String> mCategoryList = new ArrayList<String>() { { add("Fruits"); add("Vegetables"); add("Phone"); add("Car"); } }; public List<Product> getProductList() { return mProductList; } public List<String> getCategoryList() { return mCategoryList; } }
public class Product { private String mName; private String mCategory; public Product(String name, String category) { mName = name; mCategory = category; } public String getName() { return mName; } public String getCategory() { return mCategory; } }
Uzupełnij wiedzę o: Twoja aplikacja – odtwarzacz audio
- Część 1: MediaSession i MediaController
- Cześć 2: AudioFocus
- Cześć 3: Powiadomienia
Nasz wygląd czyli Layout
Poniżej jest przedstawiony wygląd poszczególnych elementów listy czyli kategorii oraz elementów. Na końcu wygląd aktywności.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical" android:background="@color/colorPrimary"> <TextView android:id="@+id/listcategory" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="20dp" android:textColor="@android:color/white" android:padding="8dp"/> </LinearLayout>
<?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="vertical"> <TextView android:id="@+id/listItem" android:layout_width="fill_parent" android:layout_height="wrap_content" android:padding="8dp"/> </LinearLayout>
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <ExpandableListView android:id="@+id/expandableListView" android:layout_height="match_parent" android:layout_width="match_parent"/> </android.support.constraint.ConstraintLayout>
Tworzymy listę ExpandableListView
ExpandableListView to widok pokazujący elementy na przewijanej pionowej liście dwupoziomowej. Jest podobny do ListView z tym, że posiada grupy (group), które mogą być rozbudowane o dzieci (child). Dziwnie brzmi zobaczmy jak stworzyć listę rozwijaną w Androidzie. Nasza klasa może wyglądać tak:
public class ExpListAdapter extends BaseExpandableListAdapter { private Context mContext; private ArrayList<String> mListCategory; private ArrayMap<Integer, List> mListProduts; public ExpListAdapter(Context context) { this.mContext = context; } @Override public Object getChild(int groupPosition, int childPosititon) { return this.mListProduts.get(groupPosition).get(childPosititon); } @Override public long getChildId(int groupPosition, int childPosition) { return childPosition; } @Override public View getChildView(int groupPosition, final int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { if (convertView == null) { LayoutInflater infalInflater = (LayoutInflater) this.mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = infalInflater.inflate(R.layout.list_item, null); } String product = (String) getChild(groupPosition, childPosition); TextView listItem = convertView.findViewById(R.id.listItem); listItem.setText(product); return convertView; } @Override public int getChildrenCount(int groupPosition) { return this.mListProduts.get(groupPosition).size(); } @Override public Object getGroup(int groupPosition) { return this.mListCategory.get(groupPosition); } @Override public int getGroupCount() { return this.mListCategory.size(); } @Override public long getGroupId(int groupPosition) { return groupPosition; } @Override public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { if (convertView == null) { LayoutInflater infalInflater = (LayoutInflater) this.mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = infalInflater.inflate(R.layout.list_category, null); } String category = (String) getGroup(groupPosition); TextView listcategory = convertView.findViewById(R.id.listcategory); listcategory.setTypeface(null, Typeface.BOLD); listcategory.setText(category); return convertView; } @Override public boolean hasStableIds() { return false; } @Override public boolean isChildSelectable(int groupPosition, int childPosition) { return true; } public void setData(List<String> mCategory, ArrayMap<Integer, List> mProducts) { this.mListCategory = (ArrayList<String>) mCategory; this.mListProduts = mProducts; } }
public class MainActivity extends AppCompatActivity { private String DEBUG_TAG = "DEBUG_TAG"; private List<String> mCategoryList; private List<Product> mProductList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); DataList data = new DataList(); mCategoryList = data.getCategoryList(); mProductList = data.getProductList(); expandableLeistView(); } private ArrayMap<Integer, List> splitProduct(List<String> category, List<Product> product){ ArrayMap<Integer, List> mItem = new ArrayMap(); for (int c=0; c<category.size();c++) { List<String> array = new ArrayList<>(); for (int p = 0; p < product.size(); p++) { if (product.get(p).getCategory().contains(category.get(c))) array.add(product.get(p).getName()); } mItem.put(c,array); } return mItem; } private void expandableLeistView() { ExpandableListView expandableLeistView = findViewById(R.id.expandableListView); ArrayMap<Integer, List> mProducts = splitProduct(mCategoryList, mProductList); ExpListAdapter mAdapter = new ExpListAdapter(this); mAdapter.setData(mCategoryList, mProducts); expandableLeistView.setAdapter(mAdapter); expandableLeistView.setOnChildClickListener(new ExpandableListView.OnChildClickListener() { @Override public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { Log.d(DEBUG_TAG, "groupPosition= "+ groupPosition + " childPosition= " + childPosition ); return false; } }); } }
Oto kilka ważnych metod używanych przez klasę ExpandableListView:
- getGroupView() – zwraca widok nagłówka grupy listy
- getChildView() – zwraca widok dla elementu potomnego listy
- setData – do ustawienia danych
Sama klasa nie jest skomplikowana. A co z klasą aktywności? W MainActivity mamy metodę expandableLeistView(), która tworzy nam listę wraz z adapterem. Oprócz tego mamy wywołanie zwrotne. Metoda setOnChildClickListener implementuje nam informacje o tym co zostało kliknięte. Dowiesz się która kategoria i który element z listy został kliknięty.
Zauważ też, że do adaptera przekazujemy listę typu ArrayMap – jest to odpowiednik HashMap w Javie. Metoda ta jest bardziej wydajniejsza i zalecana w programowaniu na system Android. Metoda splitProduct() własnie tworzy nam taką listę ArrayMap. Dzięki temu mamy listę podzieloną na kategorie, a w niej odpowiednie elementy tej kategorii.
Uzupełnij wiedzę o Lombok w Androidzie
Rozbudowujemy listę rozwijaną w Androidzie.
Co jeśli nam taka lista nie wystarcza i potrzebujemy bardziej elastycznej i rozbudowanej listy rozwijanej w Androidzie? Skorzystajmy z RecyclerView.
public class MainActivity extends AppCompatActivity { private List<String> mCategoryList; private List<Product> mProductList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); DataList data = new DataList(); mCategoryList = data.getCategoryList(); mProductList = data.getProductList(); xpandableRecyclerView(); } private void xpandableRecyclerView() { RecyclerView recyclerView = findViewById(R.id.recyclerview); recyclerView.setLayoutManager(new LinearLayoutManager(this)); ExpandableRecyclerAdapter adapter = new ExpandableRecyclerAdapter(); adapter.setData(mCategoryList, mProductList); recyclerView.setAdapter(adapter); } }
public class ExpandableRecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements CategoryViewHolder.CategoryViewHolderCallback { private static final int CATEGORY_TYPE = 0; private static final int ITEM_TYPE = 1; private List<String> mListCategory; private List<Product> mListProduct; private int[] mListProductSize; private SparseArray<ViewType> mViewType; private SparseIntArray categoryExpand; @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view; switch (viewType) { case CATEGORY_TYPE: view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_category, parent, false); return new CategoryViewHolder(view, this); case ITEM_TYPE: view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item, parent, false); return new ItemViewHolder(view); } return null; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { int itemViewType = getItemViewType(position); ViewType viewType = mViewType.get(position); if (itemViewType == CATEGORY_TYPE) { bindCategoryViewHolder(holder,position,viewType); } else { bindProductViewHolder(holder,viewType); } } private void bindCategoryViewHolder(RecyclerView.ViewHolder holder, int position, ViewType viewType) { int dataIndex = viewType.getDataIndex(); CategoryViewHolder categoryViewHolder = (CategoryViewHolder) holder; categoryViewHolder.mCategory.setText(mListCategory.get(dataIndex)); if (isExpanded(position)) { categoryViewHolder.mCategory.setCompoundDrawablesWithIntrinsicBounds(null, null, categoryViewHolder.arrowUp, null); } else { categoryViewHolder.mCategory.setCompoundDrawablesWithIntrinsicBounds(null, null, categoryViewHolder.arrowDown, null); } } private void bindProductViewHolder(RecyclerView.ViewHolder holder, ViewType viewType) { int dataIndex = viewType.getDataIndex(); ((ItemViewHolder) holder).mItem.setText(mListProduct.get(dataIndex).getName()); } @Override public int getItemCount() { int count = 0; if (mListCategory != null && mListProduct != null) { mViewType.clear(); int collapsedCount = 0; for (int c = 0; c < mListCategory.size(); c++) { mViewType.put(count, new ViewType(c, CATEGORY_TYPE)); count += 1; int childCount = mListProductSize[c]; if (categoryExpand.get(c) != 0) { // Expanded State for (int p = 0; p < childCount; p++) { mViewType.put(count, new ViewType(count - (c + 1) + collapsedCount, ITEM_TYPE)); count += 1; } } else { // Collapsed collapsedCount += childCount; } } } return count; } @Override public int getItemViewType(int position) { if (mViewType.get(position).getType() == CATEGORY_TYPE) { return CATEGORY_TYPE; } else { return ITEM_TYPE; } } public void setData(List<String> category, List<Product> product) { if (category != null && product != null) { mListCategory = category; mListProduct = product; getProductListSize(); mViewType = new SparseArray<>(); categoryExpand = new SparseIntArray(product.size()); notifyDataSetChanged(); } } private void getProductListSize() { mListProductSize = new int[mListCategory.size()]; for (int c=0; c<mListCategory.size();c++) { for (int p = 0; p < mListProduct.size(); p++) { if (mListProduct.get(p).getCategory().contains(mListCategory.get(c))) mListProductSize[c] += 1; } } } @Override public void onCategoryClick(int position) { int dataIndex = mViewType.get(position).getDataIndex(); int childCount = getChildCount(dataIndex); if (categoryExpand.get(dataIndex) == 0) { // Collapsed. Now expand it categoryExpand.put(dataIndex, 1); notifyItemRangeInserted(position + 1, childCount); } else { // Expanded. Now collapse it categoryExpand.put(dataIndex, 0); notifyItemRangeRemoved(position + 1, childCount); } } @Override public boolean isExpanded(int position) { int dataIndex = mViewType.get(position).getDataIndex(); return categoryExpand.get(dataIndex) == 1; } }
public class CategoryViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { private final CategoryViewHolderCallback callback; public TextView mCategory; public Drawable arrowUp; public Drawable arrowDown; public CategoryViewHolder(View view, CategoryViewHolderCallback callback) { super(view); this.callback = callback; arrowUp = ContextCompat.getDrawable(itemView.getContext(), android.R.drawable.arrow_up_float); arrowDown = ContextCompat.getDrawable(itemView.getContext(), android.R.drawable.arrow_down_float); mCategory = view.findViewById(R.id.listcategory); view.setOnClickListener(this); } @Override public void onClick(View v) { int position = getAdapterPosition(); callback.onCategoryClick(position); if (callback.isExpanded(position)) { mCategory.setCompoundDrawablesWithIntrinsicBounds(null, null, arrowUp, null); } else { mCategory.setCompoundDrawablesWithIntrinsicBounds(null, null, arrowDown, null); } } public interface CategoryViewHolderCallback { void onCategoryClick(int position); boolean isExpanded(int position); } }
public class ItemViewHolder extends RecyclerView.ViewHolder { public TextView mItem; public ItemViewHolder(View view) { super(view); mItem = view.findViewById(R.id.listItem); } }
class ViewType { private int dataIndex; private int type; public ViewType(int dataIndex, int type) { this.dataIndex = dataIndex; this.type = type; } public int getDataIndex() { return dataIndex; } public int getType() { return type; } }
Powyższe klasy są dość proste, ale wyjaśnijmy kilka rzeczy. W MainActivity mamy typowe tworzenie listy RecyclerView. Przekazujemy dwie listy jedna z lista kategorii, a druga z elementami. Tutaj już mamy zwykłe ArrayList. W adapterze ExpandableRecyclerAdapter metoda onCreateViewHolder() tworzy odpowiedni widok w zależności czy jest kategoria czy element kategorri. Następnie jest wywoływane onBindViewHolder() wypełnia widoki odpowiednimi danymi.
Ważna klasą jest ViewType. To własnie ona wyznacza który element z listy jest kategorią, a który podrzędną wartością. Wszystko dzieje się w metodzie getItemCount(). Właśnie ona na „bieżąco” zarządza widokami.
Ciekawą metodą jest getProductListSize() oblicza ile w danej kategorii występuje rekordów. Ta metoda została stworzona aby dynamicznie liczyć elementy, Jeżeli wiemy z góry ile dana kategoria posiada wartości to możemy pominąć tą funkcje i przerobić metodą getItemCount().
Uzupełnij wiedzę o: Zaawansowany monitoring akumulatora – Battery Historian
Oczywiście mamy odpowiedni callback aby widok kategorii odpowiednio rozwijał się i zwijał się. Nic nie szkodzi aby wywołać setOnClickListener() w klasie ItemViewHolder.
Jako, że jest to lista typu RecyclerView możemy ją jeszcze bardziej rozbudować o kolejne widoki czy inne akcje. Efekt powyższego kodu:
Podsumowanie
Założenie było proste. Stworzenie listy rozwijanej w Androidzie, która będzie reagować na kliknięcie. O ile wytyczne nie są jakieś skomplikowane o tyle implementacji może wydawać się dość skomplikowana. Na szczęście udało się stworzyć prostą i bardziej rozbudowaną listę. Teraz już wiesz jak stworzyć listę rozwijaną. Na pewno powyższy kod da się jeszcze jakoś zoptymalizować. Masz pomysł jak?
Jeżeli chcesz wiedzieć więcej na temat RecycleView, zajerzyj do mojego kursu.