【问题标题】:Recycleview with nested expandable lists in header标题中带有嵌套可扩展列表的 Recyclerview
【发布时间】:2021-01-20 14:01:04
【问题描述】:

我正在设计一个相对复杂的 UI,我搜索了 stackoverflow 并没有找到类似的设计。可能有很多方法可以解决这个问题,但我想就如何实现这一点询问专家意见,我想分享我的方法并确保我以正确的方式做事。我的方法是我创建了一个带有标题和内部标题 recycleview 的回收视图,我正在使用由 h6ah4i (taken from github) 开发的可扩展回收视图库。如果有更好的方法,请告诉我。

以下图像预览是我想要获得的最终结果的实时模型。这不是实际的屏幕。我的问题是实现这一目标的最佳方法是什么,我应该在 recycleview 标题中使用可扩展的回收视图还是可扩展的列表视图。我感谢任何答案,因为它不必与我的代码相似。欢迎任何建议。我希望这篇文章也能帮助像我这样的其他人寻找类似的解决方案。

RecycleView 适配器

public class RecycleAdapterPlantSearch extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private static final int TYPE_HEADER = 0;
    private static final int TYPE_ITEM = 1;
    private List<Plants> plantsList;
    private Context context;
    private OnItemClickListener onItemClickListener;


    public RecycleAdapterPlantSearch(Context context, List<Plants> plantsList, OnItemClickListener onClickListener) {
        this.context = context;
        this.plantsList = plantsList;
        onItemClickListener = onClickListener;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == TYPE_ITEM) {
            // Here Inflating your recyclerview item layout
            View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_view_plant_search_plant_item, parent, false);
            return new ItemViewHolder(itemView, onItemClickListener);
        } else if (viewType == TYPE_HEADER) {
            // Here Inflating your header view
            View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_view_plant_search_header, parent, false);
            return new HeaderViewHolder(itemView, onItemClickListener);
        } else return null;
    }

    @Override
    public void onBindViewHolder(final RecyclerView.ViewHolder holder, int position) {
        /*
        position 0 is for header
        */

        if (holder instanceof HeaderViewHolder) {
            // setheadersdata_flag = true;
            HeaderViewHolder headerViewHolder = (HeaderViewHolder) holder;
            // You have to set your header items values with the help of model class and you can modify as per your needs

            // Setup expandable feature and RecyclerView
            RecyclerViewExpandableItemManager expMgr = new RecyclerViewExpandableItemManager(null);

            SimpleDemoExpandableItemAdapter.OnListItemClickMessageListener clickListener = message -> {
                Toast.makeText(context, message, Toast.LENGTH_SHORT).show();

            };
            List<BadgesVM> badgesVMList = null;

            badgesVMList = new ArrayList() {{
                add(new BadgesVM("447", "Bienenfreundlich", "bienenfreundlich", false));
                add(new BadgesVM("320,322", "Vogelfreundlich", "vogelfreundlich", false));
                add(new BadgesVM("321", "Insektenfreundlich", "insektenfreundlich", false));
                add(new BadgesVM("445", "Ökologisch wertvoll", "oekologisch", false));
                add(new BadgesVM("531", "Schmetterlings freundlich", "schmetterlings", false));
                add(new BadgesVM("530", "Heimische Pflanze'", "heimische Pflanze'", false));
            }};

            // Create wrapped adapter:  MyItemAdapter -> expMgr.createWrappedAdapter -> MyHeaderFooterAdapter
            RecyclerView.Adapter adapter;
            adapter = new SimpleDemoExpandableItemAdapter(context, expMgr,badgesVMList, clickListener);
            adapter = expMgr.createWrappedAdapter(adapter);
            //adapter = new DemoHeaderFooterAdapter(adapter, null);

            headerViewHolder.recyclerViewExpandable.setAdapter(adapter);

            headerViewHolder.recyclerViewExpandable.setLayoutManager(new LinearLayoutManager(context));

            // NOTE: need to disable change animations to ripple effect work properly
            ((SimpleItemAnimator) headerViewHolder.recyclerViewExpandable.getItemAnimator()).setSupportsChangeAnimations(false);

            expMgr.attachRecyclerView(headerViewHolder.recyclerViewExpandable);


        } else if (holder instanceof ItemViewHolder) {

            final ItemViewHolder itemViewHolder = (ItemViewHolder) holder;

            itemViewHolder.plantDescText.setText(plantsList.get(position - 1).getDescription());

            RequestOptions options = new RequestOptions()
                    .centerCrop()
                    .placeholder(R.drawable.background_small);
            String imageUrl = APP_URL.BASE_ROUTE_INTERN + plantsList.get(position - 1).getImages().get(0).getSrcAttr();

            Glide.with(context).load(imageUrl).apply(options).into(itemViewHolder.plantImg);

        }
    }

    @Override
    public int getItemViewType(int position) {
        if (position == 0) {
            return TYPE_HEADER;
        }
        return TYPE_ITEM;
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    // getItemCount increasing the position to 1. This will be the row of header
    @Override
    public int getItemCount() {
        return plantsList.size() + 1;
    }


    public interface OnItemClickListener {
        void OnItemClickListener(View view, int position);

        void RecycleViewExtraDetails(ChipGroup chipGroup);

        void nestedRecycleViewsSpecialOdd(RecyclerView nestedRecycleView);
    }

    private class HeaderViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

        private TextView searchNameTxt, searchFamilyTxt, plantGroupTxt, plantFamilySearchTxt, ecologyFilterTxt,
                frostSearchTxt;
        private ChipGroup chipGroup;
        private Button filterSearchBtn;
        private CardView ecologyCv;
        private CardView detailSearchCv;
        private RecyclerView recyclerViewExpandable;

        public HeaderViewHolder(View headerView, OnItemClickListener onItemClickListener) {
            super(headerView);
            searchNameTxt = headerView.findViewById(R.id.textView_plant_search_header_plant_search);
            searchFamilyTxt = headerView.findViewById(R.id.textView_plant_search_header_plant_search);
            ecologyCv = headerView.findViewById(R.id.cardView_plant_search_header_ecology);
            detailSearchCv = headerView.findViewById(R.id.cardView_plant_search_header_detail_search);
            plantGroupTxt = headerView.findViewById(R.id.textView_plant_search_header_plant_group);
            plantFamilySearchTxt = headerView.findViewById(R.id.textView_plant_search_header_plant_family);
            ecologyFilterTxt = headerView.findViewById(R.id.textView_plant_search_header_ecology_filter);
            frostSearchTxt = headerView.findViewById(R.id.textView_plant_search_header_frost_filter);
            chipGroup = headerView.findViewById(R.id.chip_group_plant_search_header);
            filterSearchBtn = headerView.findViewById(R.id.button_plant_search_header_filter_search);
            recyclerViewExpandable = headerView.findViewById(R.id.expandable_list_view_plant_search);

            searchNameTxt.setOnClickListener(this);
            searchFamilyTxt.setOnClickListener(this);
            ecologyCv.setOnClickListener(this);
            detailSearchCv.setOnClickListener(this);
            plantGroupTxt.setOnClickListener(this);
            plantFamilySearchTxt.setOnClickListener(this);
            ecologyFilterTxt.setOnClickListener(this);
            filterSearchBtn.setOnClickListener(this);
            frostSearchTxt.setOnClickListener(this);
        }

        @Override
        public void onClick(View view) {
            onItemClickListener.OnItemClickListener(view, getAdapterPosition());
            onItemClickListener.RecycleViewExtraDetails(chipGroup);

        }

    }

    public class ItemViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

        private Button readMoreBtn;
        private TextView plantDescText;
        private ImageView plantImg;

        public ItemViewHolder(View itemView, OnItemClickListener onItemClickListener) {
            super(itemView);

            plantDescText = itemView.findViewById(R.id.textView_plant_search_plants_item_description_text);
            readMoreBtn = itemView.findViewById(R.id.button_plant_search_plant_item_read_more);
            plantImg = itemView.findViewById(R.id.imageView_plant_search_plants_item_plant_image);

            readMoreBtn.setOnClickListener(this);

        }

        @Override
        public void onClick(View view) {
            onItemClickListener.OnItemClickListener(view, getAdapterPosition() - 1);
        }
    }


}

嵌套标题回收视图

public class SimpleDemoExpandableItemAdapter extends AbstractExpandableItemAdapter<SimpleDemoExpandableItemAdapter.MyGroupViewHolder,
        SimpleDemoExpandableItemAdapter.MyChildViewHolder> implements View.OnClickListener {
    RecyclerViewExpandableItemManager mExpandableItemManager;
    List<MyBaseItem> mItems;
    OnListItemClickMessageListener mOnItemClickListener;
    List<BadgesVM> badgesVMList;
    Context context;

    static class MyBaseItem {
        public final int id;
        public final String text;

        public MyBaseItem(int id, String text) {
            this.id = id;
            this.text = text;
        }
    }

    static abstract class MyBaseViewHolder extends AbstractExpandableItemViewHolder {
        TextView textView;
        Slider frostSlider;
        RecyclerView detailRecycleView;

        public MyBaseViewHolder(View itemView) {
            super(itemView);
            textView = itemView.findViewById(android.R.id.text1);
            frostSlider = itemView.findViewById(R.id.slider_plant_search_expandable);
            detailRecycleView = itemView.findViewById(R.id.recycle_view_plant_search_detail_search);
        }
    }

    static class MyGroupViewHolder extends MyBaseViewHolder {
        public MyGroupViewHolder(View itemView) {
            super(itemView);
        }
    }

    static class MyChildViewHolder extends MyBaseViewHolder {
        public MyChildViewHolder(View itemView) {
            super(itemView);
        }
    }

    public SimpleDemoExpandableItemAdapter(Context context, RecyclerViewExpandableItemManager expMgr, List<BadgesVM> badgesVMList, OnListItemClickMessageListener clickListener) {
        mExpandableItemManager = expMgr;
        mOnItemClickListener = clickListener;
        this.badgesVMList = badgesVMList;
        this.context = context;

        setHasStableIds(true); // this is required for expandable feature.

        mItems = new ArrayList<>();
        mItems.add(new MyBaseItem(0, "Filter nach ökologischen Kriterien"));
        mItems.add(new MyBaseItem(1, "Frosthärte"));
        mItems.add(new MyBaseItem(2, "Detailsuche"));

    }

    @Override
    public int getGroupCount() {
        return mItems.size();
    }

    @Override
    public int getChildCount(int groupPosition) {
        int childCount = 0;
        int groupId = mItems.get(groupPosition).id;
        if (groupId == 0) {
            childCount = badgesVMList.size();
        } else if (groupId == 1) {
            childCount = 1; //contains only one item
        } else if (groupId == 2) {
            childCount = 1; //contains only one item
        }
        return childCount;
    }

    @Override
    public long getGroupId(int groupPosition) {
        // This method need to return unique value within all group items.
        return mItems.get(groupPosition).id;
    }

    @Override
    public long getChildId(int groupPosition, int childPosition) {
        // This method need to return unique value within the group.
        int groupId = mItems.get(groupPosition).id;
        int childId = 0;

        if (groupId == 0) {
            badgesVMList.get(childPosition).getId();
        } else if (groupId == 1) {
            childId = 0;
        } else if (groupId == 2) {
            childId = 0;
        }
        return childId;
    }

    @Override
    @NonNull
    public MyGroupViewHolder onCreateGroupViewHolder(@NonNull ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_group_item_for_expandable_minimal, parent, false);
        MyGroupViewHolder vh = new MyGroupViewHolder(v);
        vh.itemView.setOnClickListener(this);
        return vh;
    }

    @Override
    @NonNull
    public MyChildViewHolder onCreateChildViewHolder(@NonNull ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_child_item_for_expandable_minimal, parent, false);
        MyChildViewHolder vh = new MyChildViewHolder(v);
        vh.itemView.setOnClickListener(this);
        return vh;
    }

    @Override
    public void onBindGroupViewHolder(@NonNull MyGroupViewHolder holder, int groupPosition, int viewType) {
        MyBaseItem group = mItems.get(groupPosition);
        holder.textView.setText(group.text);
    }

    @Override
    public void onBindChildViewHolder(@NonNull MyChildViewHolder holder, int groupPosition, int childPosition, int viewType) {

        int groupId = mItems.get(groupPosition).id;
        if (groupId == 0) {
            BadgesVM badgesVM = badgesVMList.get(childPosition);
            holder.textView.setVisibility(View.VISIBLE);
            holder.frostSlider.setVisibility(View.GONE);
            holder.detailRecycleView.setVisibility(View.GONE);
            holder.textView.setText(badgesVM.getName());
        } else if (groupId == 1) {
            holder.textView.setVisibility(View.GONE);
            holder.frostSlider.setVisibility(View.VISIBLE);
            holder.detailRecycleView.setVisibility(View.GONE);
        } else if (groupId == 2) {
            holder.textView.setVisibility(View.GONE);
            holder.frostSlider.setVisibility(View.GONE);
            holder.detailRecycleView.setVisibility(View.VISIBLE);

            // Setup expandable feature and RecyclerView
            RecyclerViewExpandableItemManager expMgr = new RecyclerViewExpandableItemManager(null);

            DetailSearchExpandableItemAdapter.OnListItemClickMessageListener clickListener = message -> {
                Toast.makeText(context, message, Toast.LENGTH_SHORT).show();

            };
            List<BadgesVM> badgesVMList = null;

            badgesVMList = new ArrayList() {{
                add(new BadgesVM("447", "Bienenfreundlich", "bienenfreundlich", false));
                add(new BadgesVM("320,322", "Vogelfreundlich", "vogelfreundlich", false));
                add(new BadgesVM("321", "Insektenfreundlich", "insektenfreundlich", false));
                add(new BadgesVM("445", "Ökologisch wertvoll", "oekologisch", false));
                add(new BadgesVM("531", "Schmetterlings freundlich", "schmetterlings", false));
                add(new BadgesVM("530", "Heimische Pflanze'", "heimische Pflanze'", false));
            }};

            // Create wrapped adapter:  MyItemAdapter -> expMgr.createWrappedAdapter -> MyHeaderFooterAdapter
            RecyclerView.Adapter adapter2;
            adapter2 = new DetailSearchExpandableItemAdapter(context, expMgr, badgesVMList, clickListener);
            adapter2 = expMgr.createWrappedAdapter(adapter2);
            //adapter = new DemoHeaderFooterAdapter(adapter, null);

            holder.detailRecycleView.setAdapter(adapter2);

            holder.detailRecycleView.setLayoutManager(new LinearLayoutManager(context));

            // NOTE: need to disable change animations to ripple effect work properly
            ((SimpleItemAnimator) holder.detailRecycleView.getItemAnimator()).setSupportsChangeAnimations(false);

            expMgr.attachRecyclerView(holder.detailRecycleView);

        }


    }

    @Override
    public boolean onCheckCanExpandOrCollapseGroup(@NonNull MyGroupViewHolder holder, int groupPosition, int x, int y, boolean expand) {
        // handles click event manually (to show Snackbar message)
        return false;
    }

    @Override
    public void onClick(View v) {
        RecyclerView rv = RecyclerViewAdapterUtils.getParentRecyclerView(v);
        RecyclerView.ViewHolder vh = rv.findContainingViewHolder(v);

        int rootPosition = vh.getAdapterPosition();
        if (rootPosition == RecyclerView.NO_POSITION) {
            return;
        }

        // need to determine adapter local flat position like this:
        RecyclerView.Adapter rootAdapter = rv.getAdapter();
        int localFlatPosition = WrapperAdapterUtils.unwrapPosition(rootAdapter, this, rootPosition);

        long expandablePosition = mExpandableItemManager.getExpandablePosition(localFlatPosition);
        int groupPosition = RecyclerViewExpandableItemManager.getPackedPositionGroup(expandablePosition);
        int childPosition = RecyclerViewExpandableItemManager.getPackedPositionChild(expandablePosition);

        String message;
        if (childPosition == RecyclerView.NO_POSITION) {
            // Clicked item is a group!

            // toggle expand/collapse
            if (mExpandableItemManager.isGroupExpanded(groupPosition)) {
                mExpandableItemManager.collapseGroup(groupPosition);
                message = "COLLAPSE: Group " + groupPosition;
            } else {
                mExpandableItemManager.expandGroup(groupPosition);
                message = "EXPAND: Group " + groupPosition;
            }
        } else {
            // Clicked item is a child!

            message = "CLICKED: Child " + groupPosition + "-" + childPosition;
        }

        mOnItemClickListener.onItemClicked(message);
    }

    public interface OnListItemClickMessageListener {
        void onItemClicked(String message);
    }

}

【问题讨论】:

  • 只是一个评论 - 那个图书馆真的很旧。必须有一种更新、更好的方法
  • 感谢您的建议,是的,很久没有更新了。我想知道是否有其他库提供此功能。

标签: java android android-recyclerview expandablelistview


【解决方案1】:

您搜索为您完成大部分工作的库是正确的,但我不喜欢您选择的库。它看起来不是很灵活和简洁。我建议看一下Groupie,它的 API 很干净。另请查看Reddit 以了解有关库的一些讨论。

如果你想自己写,我想你可以在没有嵌套Adapter的情况下解决它。只需创建一个“可扩展组”项目类型。然后在getItemCount() 中计算所有项目及其嵌套项目(展开时)。看看 Groupie 源代码。

对您的代码的一些额外反馈:

  1. 我会明确地将标题添加到您提供给适配器的项目列表中。因此,您宁愿提供List&lt;Item&gt; 并拥有HeaderItemPlantsItem,而不是List&lt;Plants&gt;。这样一来,您的域模型 (Plants) 和适配器中的视图模型(项目)之间就有了清晰的分离。
  2. 您的onBindViewHolder() 方法做得太多了。让你的 ViewHolder 子类来处理。使用抽象 bindTo(Item item) 方法创建抽象 ViewHolder。然后在您的 HeaderViewHolder 中对其进行子类化并执行实际工作(在实例检查之后)。
  3. 看看view binding,它可以让你的代码更简洁。 (Kotlin 也是如此。)

【讨论】:

  • 谢谢@Bram,我一定会调查的。这看起来像一个有趣的图书馆。乍一看,它似乎没有嵌套的可扩展列表,但我会试着弄清楚。
  • 所以经过多次尝试,我决定和 Groupie 一起去,只是想感谢 @Bram 向我推荐这个库。不得不说这个图书馆太棒了!
【解决方案2】:

您可以使用ConcatAdapter 来拥有多个带有 ViewHolders 的适配器,即使是包含 RecyclerViews 的那些也包含不同类型的布局,我在我的上一个项目中使用它并且它工作正常,您可以查看它here,仪表板模块使用多个适配器来拥有不同类型的布局。

您还可以使用他们在 Google iosched app 中使用的方法,以更好的方式拥有一个具有多种布局的适配器,将逻辑从适配器移动到 ViewHolders 及其包装类 ViewBinders。 ViewBinder负责 调用 onViewHolder、onCreateViewHolder 并将数据类型绑定到 ViewBinder 并将 ViewBinder 绑定到布局。有一篇关于如何在媒体中使用它的文章,如果我能找到它,我会发布链接。您还可以查看我为动画创建的这个示例,但使用ViewBinders 以简单的形式创建不同类型的布局。

以下是我希望在 GridLayout 中显示的数据类型和布局以及显示顺序

val data = mutableListOf<Any>().apply {

            // Add Vector Drawables
            add(HeaderModel("Animated Vector Drawable"))
            add(AVDModel(R.drawable.avd_likes))
            add(AVDModel(R.drawable.avd_settings))

            add(HeaderModel("Seekable Vector Drawable"))
            add(SeekableVDModel(R.drawable.avd_compass_rotation))
            add(SeekableVDModel(R.drawable.avd_views))
            add(SeekableVDModel(R.drawable.avd_hourglass))

            add(HeaderModel("Clocks"))
            add(AVDModel(R.drawable.avd_clock_alarm))
            add(AVDModel(R.drawable.avd_clock_clock))
            add(AVDModel(R.drawable.avd_clock_stopwatch))
}

这些是我想在我的 RecyclerView 中使用的相应数据类型,它是这些类中 ViewHolder 和布局的类型和绑定。

private fun createViewBinders(): HashMap<ItemClazz, MappableItemBinder> {

    val avdViewBinder = AVDViewBinder()
    val seekableVDViewBinder = SeekableVDViewBinder()
    val headViewBinder = HeaderViewBinder()

    return HashMap<ItemClazz, MappableItemBinder>()
        .apply {

            put(
                avdViewBinder.modelClazz,
                avdViewBinder as MappableItemBinder
            )

            put(
                seekableVDViewBinder.modelClazz,
                seekableVDViewBinder as MappableItemBinder
            )

            put(
                headViewBinder.modelClazz,
                headViewBinder as MappableItemBinder
            )
        }
}

并将数据List设置为adapter,让adapter调用与数据绑定的对应布局

   val dataList:List<Any> = getVectorDrawableItemList()

        val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)

        val adapter = MultipleViewBinderListAdapter(
            createViewBinders(),
            RecyclerView.Adapter.StateRestorationPolicy.ALLOW
        ).apply {
            submitList(dataList)
        }

对于可扩展列表,iosched app 是一种很好的做法,在 RecyclerVİew here 中有关于如何为可扩展项目设置动画的视频。您可以在 ViewHolder 中设置状态,甚至可以使用 MotionLayout 将动画从折叠状态变为可展开状态。所有这些都可以在没有任何第三方库的情况下完成,而且方式非常干净。

【讨论】:

  • 感谢您的详细解答。你的回答肯定有帮助,我需要一些关于如何解决这个问题的指导,我会尝试这个并报告。
  • iosched 和 plaid 应用程序是学习多种布局和其他主题的非常好的示例。祝你好运,如果你遇到困难或需要帮助,可以随时问我。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-10-29
  • 2013-06-24
  • 2015-12-15
  • 1970-01-01
  • 1970-01-01
  • 2019-09-06
相关资源
最近更新 更多