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?

Co dalej?

  • Zapisz się na newsletter aby otrzymywać jeszcze więcej materiałów
  • 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 🙂
Menu