Estoy diseñando una interfaz de usuario relativamente compleja, he buscado stackoverflow y no he encontrado un diseño similar. Puede haber muchos enfoques para esto, pero me gustaría pedir opiniones de expertos sobre cómo lograrlo y me gustaría compartir mi enfoque y asegurarme de que lo estoy haciendo de la manera correcta. Mi enfoque es que he creado una vista de reciclaje con encabezado y una vista de reciclaje de encabezado interior Estoy usando una biblioteca de vista de reciclaje expandible desarrollada por h6ah4i (tomado de github). Por favor, avíseme si hay un mejor enfoque para esto.

La siguiente vista previa de la imagen es una maqueta en vivo del resultado final que me gustaría obtener. No es la pantalla real. Mi pregunta es cuál es la mejor manera de lograr esto, ¿debo usar la vista de reciclaje expandible o la vista de lista expandible en el encabezado de vista de reciclaje? Aprecio cualquier respuesta como enfoques o bibliotecas, no tiene que ser similar a mi código. Cualquier sugerencia es bienvenida. Espero que esta publicación también ayude a otras personas como yo en la búsqueda de una solución similar.

Adaptador 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);
        }
    }


}

vista de reciclaje de encabezado anidado

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);
    }

}

enter image description here

2
Amir Dora. 5 oct. 2020 a las 20:02

2 respuestas

La mejor respuesta

Tenías razón al buscar en una biblioteca que hace la mayor parte del trabajo por ti, pero no me gusta la biblioteca que elegiste. No parece muy flexible y conciso. Sugiero echar un vistazo a Groupie, su API es bastante limpia. Consulte también Reddit para obtener más información sobre las bibliotecas.

Si quieres escribirlo tú mismo, creo que puedes resolverlo sin Adapter anidados. Simplemente cree un tipo de elemento de 'grupo expandible'. Luego, en getItemCount() cuentas todos los elementos y sus elementos anidados (cuando se expanden). Eche un vistazo al código fuente de Groupie.

Algunos comentarios adicionales sobre su código:

  1. Agregaría explícitamente el encabezado a la lista de elementos que le da a su adaptador. Entonces, en lugar de un List<Plants>, prefiere proporcionar un List<Item> y tener un HeaderItem y PlantsItem. De esta manera, tiene una clara separación entre sus modelos de dominio (Plants) y los modelos de vista (los elementos) en su adaptador.
  2. Tu método onBindViewHolder() hace demasiado. Deje que sus subclases ViewHolder se encarguen de eso. Cree un ViewHolder abstracto con un método bindTo(Item item) abstracto. Luego, en su HeaderViewHolder subclase y haga el trabajo real (después de una instancia de verificación).
  3. Eche un vistazo a enlace de vista, puede hacer que su código sea más conciso. (También Kotlin).
2
Bram Stoker 6 oct. 2020 a las 12:44

Puede utilizar ConcatAdapter para tener varios adaptadores con ViewHolders que contengan diferentes tipos de diseños incluso con los que contienen RecyclerViews, utilicé en mi último proyecto y funciona bien, puedes consultarlo aquí, el módulo del panel utiliza varios adaptadores para tener diferentes tipos de diseños.

También puede utilizar el enfoque que utilizaron en la aplicación iosched de Google para tener un adaptador con varios diseños de una mejor manera donde mueve la lógica del adaptador a ViewHolders y su clase de contenedor ViewBinders. ViewBinder es responsable de llamar a onViewHolder, onCreateViewHolder y vincular el tipo de datos a un ViewBinder y ViewBinder a un diseño. Hay un artículo sobre cómo usarlo en medio, publicaré el enlace si puedo encontrarlo. También puede consultar esta muestra que creé para animaciones pero usé ViewBinders de forma sencilla para crear diferentes tipos de diseños.

A continuación se muestra el tipo de datos y diseño que deseo mostrar en GridLayout y en qué orden

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))
}

Estos son los tipos de datos correspondientes que quiero usar en mi RecyclerView, son los tipos y el enlace a ViewHolder y el diseño en estas clases.

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
            )
        }
}

Y configure la lista de datos en el adaptador y deje que el adaptador llame al diseño correspondiente que está vinculado a los datos

   val dataList:List<Any> = getVectorDrawableItemList()

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

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

Para la lista expandible, la aplicación iosched es una buena manera de hacerlo, hay un video sobre cómo animar elementos expandibles en RecyclerVİew aquí. Puede establecer el estado en ViewHolder e incluso usar MotionLayout para animar desde el estado contraído hasta el expandible. Todo se puede hacer sin ninguna biblioteca de terceros y de forma muy limpia.

1
Thracian 14 oct. 2020 a las 17:55