Lista rozwijana i podzielona na kategorie w Androidzie

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ę ExpandableListViewktó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

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

Cały efekt wygląda tak:
ExpandableListView

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:

ExpandableRecyclerViewPodsumowanie

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.

Co dalej?

  • Polub stronę MYENV na Facebooku oraz śledź mnie na Twitterze
  • Zachęcam do komentowania i pisania propozycji tematów, o których chcesz przeczytać
  • Poleć ten wpis za pomocą poniższych przycisków. Będę Ci za to bardzo wdzięczny 🙂
  • Życzę Ci miłego dnia i miłego kodowania 🙂