마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

프래그먼트에 리사이클러뷰

0 추천
import android.content.ClipData;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import java.util.ArrayList;
import java.util.List;

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

    public static final int HEADER = 0;
    public static final int CHILD = 1;

    private List<ClipData.Item> data;
    public ExpandableListAdapter(List<ClipData.Item> data){
        this.data = data;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
        View view = null;
        Context context = parent.getContext();
        float dp = context.getResources().getDisplayMetrics().density;
        int subItemPaddingLeft = (int) (18 * dp);
        int subItemPaddingTopAndBottom = (int) (5 * dp);
        switch (type) {
            case HEADER:
                LayoutInflater inflater = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                view = inflater.inflate(R.layout.list_header, parent, false);
                ListHeaderViewHolder header = new ListHeaderViewHolder(view);
                return header;
            case CHILD:
                TextView itemTextView = new TextView(context);
                itemTextView.setPadding(subItemPaddingLeft, subItemPaddingTopAndBottom, 0, subItemPaddingTopAndBottom);
                itemTextView.setTextColor(0x88000000);
                itemTextView.setLayoutParams(
                        new ViewGroup.LayoutParams(
                                ViewGroup.LayoutParams.MATCH_PARENT,
                                ViewGroup.LayoutParams.WRAP_CONTENT));
               return new RecyclerView.ViewHolder(itemTextView) {
                   @Override
                   public String toString() {
                       return super.toString();
                   }
               };
        }
        return null;
    }

    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        final ClipData.Item item = data.get(position);
        switch (item.type) {
            case HEADER:
                final ListHeaderViewHolder itemController = (ListHeaderViewHolder) holder;
                itemController.refferalItem = item;
                itemController.header_title.setText(item.text);
                if (item.invisibleChildren == null) {
                    itemController.btn_expand_toggle.setImageResource(R.drawable.circle_minus);
                } else {
                    itemController.btn_expand_toggle.setImageResource(R.drawable.circle_plus);
                }
                itemController.btn_expand_toggle.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (item.invisibleChildren == null) {
                            item.invisibleChildren = new ArrayList<ClipData.Item>();
                            int count = 0;
                            int pos = data.indexOf(itemController.refferalItem);
                            while (data.size() > pos + 1 && data.get(pos + 1).type == CHILD) {
                                item.invisibleChildren.add(data.remove(pos + 1));
                                count++;
                            }
                            notifyItemRangeRemoved(pos + 1, count);
                            itemController.btn_expand_toggle.setImageResource(R.drawable.circle_plus);
                        } else {
                            int pos = data.indexOf(itemController.refferalItem);
                            int index = pos + 1;
                            for (ClipData.Item i : item.invisibleChildren) {
                                data.add(index, i);
                                index++;
                            }
                            notifyItemRangeInserted(pos + 1, index - pos - 1);
                            itemController.btn_expand_toggle.setImageResource(R.drawable.circle_minus);
                            item.invisibleChildren = null;
                        }
                    }
                });
                break;
            case CHILD:
                TextView itemTextView = (TextView) holder.itemView;
                itemTextView.setText(data.get(position).text);
                break;
        }
    }


    @Override
    public int getItemCount() {
        return data.size();
    }

    public static class ListHeaderViewHolder extends RecyclerView.ViewHolder{

        public TextView header_title;
        public ImageView btn_expand_toggle;
        public ClipData.Item refferalItem;

        public ListHeaderViewHolder(@NonNull View itemView) {
            super(itemView);
            header_title = (TextView) itemView.findViewById(R.id.header_title);
            btn_expand_toggle = (ImageView) itemView.findViewById(R.id.btn_expand_toggle);

        }
    }
    public  class ClipDataItem {

        public int type;
        public String text;
        public List<ClipData.Item> invisibleChildren;

        private boolean expanded = false;

        public boolean isExpanded(){
            return this.expanded;
        }

        public ClipDataItem(int type, String text){
            this.type = type;
            this.text = text;
        }

        public int getType(){
            return type;
        }

        public String getText(){
            return text;
        }
    }

}

 

onBindViewHolder에서 계속 에러가 진행됩니다

type, text, invisibleChildren 부분이 에러진행중입니다

맨밑에 Item class 부분에서 연결이 안되는 건지..구글에서 찾아서 응용을 한다고 했는데도 에러가 계속됩니다

 

뽕짝 (350 포인트) 님이 9월 12일 질문
뽕짝님이 9월 12일 수정
무슨 에러가 나는 지에 대한 설명이 없네요. 그리고 코드를 보니까 Expanded RecyclerView를 구현하시는 것 같은데, 특정 아이템의 expand상태는 부모 클래스의 속성이 되는 게 맞을 것 같아요. 그리고 님과 같은 parent 클래스에 child item을 가지는 데이터를 이용하실 거면 ViewHolder에 CHILD 뷰타입을 사용하는 것은 좀 이상해 보여요. 이렇게 하면 리사이클러뷰를 접고 펼치는 기능이 구현이 안될 것 같아요.

public  class ClipDataItem {
 
       ...
       private boolean expanded = false;
      
       public boolean isExpanded() {
            return this.expanded;
       }
}
CHILD  부분을 ViewHolder 부분 말고 다른부분에 구현해야 되는건가요??

_질문에 코드부분 수정해서 다시 올려 놨습니다 한번만 맞는지 봐주시면 감사할거 같습니다. 에러 부분이 맨밑에 ClipDataItem 부분에서 잘못된건가요??
에러 증상에 대해 말씀을 안하셨네요.

onBindViewHolder에서 계속 에러가 진행됩니다
type, text, invisibleChildren 부분이 에러진행중입니다.

위의 설명으로는 뭘 하시려고 하는데, 어떻게 안된다는 걸 이해할 수가 없습니다.

1개의 답변

0 추천
 
채택된 답변

소스코드만으로 판단할 때 Expandable RecyclerView를 구현하고 싶으신듯 합니다.  먼저 간단히 데이터 클래스를 ParentItem, ChildIrem으로 하겠습니다. Adapter에서 사용하기 쉽게 둘다 같은 인터페이스를 구현하거나 부모 클래스를 상속받도록 합니다.

public interface ListItem {
    int getViewType();
}

public class ParentItem implements ListItem {
    public static final int VIEW_TYPE = R.layout.item_parent;

    private int type;
    private String text;
    // 라사이클러뷰에서 자식 아이템들이 접혔는지 펴졌는지 상태를 보관하는 변수
    private boolean expanded;
    private List<ChildItem> children = new ArrayList<>();

    public ParentItem(int type, String text, boolean expanded) {
        this.type = type;
        this.text = text;
        this.expanded = expanded;
    }

    public ParentItem(ParentItem another) {
        this.type = another.type;
        this.text = another.text;
        this.expanded = another.expanded;
        this.children = another.children;
    }

    public int getType() {
        return type;
    }

    public String getText() {
        return text;
    }

    public boolean isExpanded() {
        return expanded;
    }

    public void setExpanded(boolean expanded) {
        this.expanded = expanded;
    }

    public List<ChildItem> getChildren() {
        return children;
    }

    @DrawableRes
    public int getDrawableRes() {
        return expanded ? R.drawable.ic_expand_less : R.drawable.ic_expand_more;
    }

    @Override
    public int getViewType() {
        return VIEW_TYPE;
    }
}

 

다름으로 Adapter 입니다.

public class ExpandableAdapter extends RecyclerView.Adapter<BaseViewHolder> {

    private List<ListItem> items = new ArrayList<>();
    private ParentViewHolder.Listener listener;

    public ExpandableAdapter(ParentViewHolder.Listener listener) {
        this.listener = listener;
    }

    public void submitList(List<ListItem> items) {
        this.items = items;
        // 무조건 notifyDataSetChange를 호출하는 것은 효율적이지 않으므로 권장되지 않음.
        // 변경된 아이템만 갱신하기 위하여 notify*Change를 대신 사용하거나 , AsyncDiffUtil또는
        // ListAdapter를 사용하는 것이 좋음.
        notifyDataSetChanged();
    }

    public ListItem getItem(int position) {
        return this.items.get(position);
    }

    @Override
    public int getItemViewType(int position) {
        return getItem(position).getViewType();
    }

    @NonNull
    @Override
    public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
        switch (viewType) {
            case ParentItem.VIEW_TYPE:
                return new ParentViewHolder(itemView, listener);
            case ChildItem.VIEW_TYPE:
                return new ChildViewHolder(itemView);
            default:
                throw new IllegalArgumentException("Cannot find view for " + viewType);
        }
    }

    @Override
    public void onBindViewHolder(@NonNull BaseViewHolder holder, int position) {
        ListItem item = getItem(position);
        holder.bind(item);
    }

    @Override
    public int getItemCount() {
        return items.size();
    }
}

 

다음은 ViewHolder 들입니다.  ViewHolder도. 공통클래스를 이용합니다.

public abstract class BaseViewHolder extends RecyclerView.ViewHolder {

    public BaseViewHolder(@NonNull View itemView) {
        super(itemView);
    }

    abstract void bind(ListItem item);
}

public class ParentViewHolder extends BaseViewHolder {
    interface Listener {
        void onAddChildClicked(int parentPosition);

        void onToggleClicked(ParentItem item);
    }

    private final ItemParentBinding binding;
    private final Listener listener;

    public ParentViewHolder(@NonNull View itemView, Listener listener) {
        super(itemView);
        binding = ItemParentBinding.bind(itemView);

        this.listener = listener;

        binding.addBtn.setOnClickListener(v -> {
            listener.onAddChildClicked(getAdapterPosition());
        });
    }

    @Override
    public void bind(ListItem item) {
        ParentItem parentItem = (ParentItem) item;
        binding.toggleImg.setOnClickListener(v -> {
            listener.onToggleClicked(parentItem);
        });
        binding.toggleImg.setImageResource(parentItem.getDrawableRes());

        binding.titleTxt.setOnClickListener(v -> {
            listener.onToggleClicked(parentItem);
        });
        binding.titleTxt.setText(parentItem.getText());
    }
}

public class ChildViewHolder extends BaseViewHolder {
    private final ItemChildBinding binding;

    public ChildViewHolder(@NonNull View itemView) {
        super(itemView);
        binding = ItemChildBinding.bind(itemView);
    }

    @Override
    public void bind(ListItem item) {
        ChildItem childItem = (ChildItem) item;
        binding.titleTxt.setText(childItem.getText());
    }
}

 

이렇게 하면, 남은 것은 리사이클뷰의 아이템을 토글할 때 , 해당 ParentItem의 expanded의 값을 변경할 다음 ExpandableAdapter에 변경된 아이템을 submitList해주면 됩니다.

public ExpandableActivity extends AppCompatActivity() implements ParentViewHolder.Listener {
    ...

   @Override
    public void onAddChildClicked(int parentPosition) {
        //해당 포지션에 해당하는 ParentItem을 찾아 자식 아이템 추가.
    }

    @Override
    public void onToggleClicked(ParentItem item) {
         // ParemtItem 의 expanded 를 업데이트하고  adapter 를 갱신.
    } 
}

 


onAddChildClicked과 onToggleClicked는 모두 파라미터로 AdapterPosition 을 사용해도 되고 ParentItem을 사용해도 됩니다. 
제 샘플에서는 둘다 구현가능하다는 걸 보여주기 위해 섞어서 사용했고, 실제 적용하실 때는 position 이나  ParentItem 하나만 사용하시기 바랍니다. 이제 나머지는 ExpandableActivity에서 ParentViewHolder.Listener를 구현해 주시기만 하면 됩니다.

그리고, expanded 를 갱신할 때 adapter 내부에서 SparseArray등을 통하여 바로 처리할 수도 있으나, 어댑터의 데이터를 가지고 있는 클래스가 존재할 경우(이게 좋은 방식입니다만), 데이터 불일치가 생길 수 잇으므로, 어댑터에 사용되는 데이터는 한곳에이 있는 데이터만을 사용할 수 있는 것이 바람직하므로 개인적으로는 꺼려하는 방식입니다. 실제 아이템의 토글상태를 처리하는 부분은 단순한 자바 코드 문제이므로 쉽게 구현하실 수 있으리라 생각합니다.

spark (79,410 포인트) 님이 9월 12일 답변
뽕짝님이 9월 12일 채택됨
...