ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [RecyclerView.Adapter] RecyclerView에서 ChoiceMode처럼 쓰기
    개발/Android 2018. 4. 9. 20:17
    반응형

     RecyclerView는 기존의 ListView와 다르게 ChoiceMode를 지원하지 않아 간단하게 Select 상태를 가지는 RecyclerView를 사용하고자 할때는 별개로 구현이 필요합니다. 

     그래서 지난번에 구현한 GeneralAdapter를 상속받은 Selectable Adapter를 만들어 보겠습니다.


    [GeneralAdapter] RecyclerView.Adapter 편하게 쓰기


     먼저 구현 컨셉은 Select 상태정보를 Adapter에서 관리하도록 하여 사용자가 필요시에 해당정보를 제어할 수 있도록 합니다.  추가로 앞서 구현한 GeneralAdapter를 상속받아 GeneralAdapter가 가지는 사용 편의성을 이용할 수 있도록 합니다.

    
    public class GeneralSelectableAdapter extends GeneralAdapter {
        public enum Type {
            SINGLE, MULTIPLE
        }
    
        private SparseBooleanArray selectedArray = new SparseBooleanArray();
        private Type selectMode = Type.SINGLE;
    
        ...
    
        public GeneralSelectableAdapter(Context context,
                Class> classType, Type selectMode) {
            super(context, classType);
            this.selectMode = selectMode;
        }
    
        ...
    
        public void setSelectItem(int index, boolean isSelect) {
            int keyIndex = selectedArray.indexOfKey(index);
            if (keyIndex > -1 && !isSelect) {
                selectedArray.delete(index);
                notifyItemChanged(index);
                return;
            }
    
            if (selectMode == Type.SINGLE) {
                keyIndex = selectedArray.indexOfValue(true);
                if (keyIndex > -1) {
                    int lastSelectIndex = selectedArray.keyAt(keyIndex);
                    selectedArray.delete(lastSelectIndex);
                    notifyItemChanged(lastSelectIndex);
                }
            }
    
            selectedArray.put(index, isSelect);
            notifyItemChanged(index);
        }
    
        public void setSelectAll(boolean isSelect) {
            if (selectMode != Type.MULTIPLE) {
                return;
            }
    
            if (isSelect) {
                for (int i = 0, size = getItemCount(); i < size; i++) {
                    setSelectItem(i, true);
                }
            } else {
                selectedArray.clear();
                notifyItemRangeChanged(0, getItemCount());
            }
        }
    
        public int getSelectedItemCount() {
            return selectedArray.size();
        }
    
        public int[] getSelectedIndexArray() {
            int selectedItemCount = getSelectedItemCount();
            if (selectedItemCount == 0) {
                return null;
            }
    
            int[] indexArray = new int[selectedItemCount];
            for (int i = 0, size = selectedItemCount; i < size; i++) {
                indexArray[i] = selectedArray.keyAt(i);
            }
            Arrays.sort(indexArray);
            return indexArray;
        }
    
        public List getSelectedItems() {
            List list = new ArrayList<>();
            for (int i = 0, size = selectedArray.size(); i < size; i++) {
                list.add(getItem(selectedArray.keyAt(i)));
            }
            return list;
        }
    
        public void setSelectMode(Type type) {
            this.selectMode = type;
        }
    
        @Override
        public void onBindViewHolder(GeneralViewHolder holder, int position) {
            holder.setItemSelected(selectedArray.get(position));
            super.onBindViewHolder(holder, position);
        }
    
        ...
    
    }
    
    

     단일 선택 / 다중 선택 둘다 가능하도록 모드 선택기능을 제공하며, Select 상태정보는 SparseBooleanArray로 관리하고 true상태만 관리합니다. false는 delete를 해서 Select Count 정보를 좀 쉽게 가져올수 있게 하려고 했습니다.

     onBindViewHolder일때 Holder를 통해 Select 상태를 전달합니다. Adatper.notify...() 메소드들을 이용하면 data업데이트시 onBindViewHolder가 다시 호출되는데 이것을 이용합니다.

    
    public class GeneralViewHolder extends RecyclerView.ViewHolder {
        GeneralView itemView;
    
        ...
    
        public void setItemSelected(boolean isSelected) {
            itemView.setItemSelected(isSelected);
        }
    }
    
    
    
    public abstract class GeneralView extends FrameLayout {
        protected GeneralAdapter adapter;
        
        ...
    
        private boolean isItemSelected = false;
    
        public GeneralView(Context context) {
            ...
    
            onViewCreated();
        }
    
        ...
    
        protected void setAdapter(GeneralAdapter adapter) {
            this.adapter = adapter;
        }
    
        protected void setItemSelected(boolean isItemSelected) {
            this.isItemSelected = isItemSelected;
        }
    
        public boolean isItemSelected() {
            return isItemSelected;
        }
    
        public abstract void onBindData(int position, T data);
    
        public abstract View createView(ViewGroup parent);
    
        public void onViewCreated() {
        }
    
    

     GeneralViewHolder와 GeneralView에 Select관련 메소드를 추가 합니다. Select 특화 View로 분리를 위해선 ViewHolder로 같이 추가 구현이 필요해서 기존 클래스를 업데이트 하는 방향으로 했습니다.


     몇가지 메소드가 GeneralView에 추가되었습니다.

     첫째로 View가 생성완료 여부를 체크하기 위해 onViewCreated() 메소드를 추가했습니다. View가 생성되고 ButterKnife까지 동작 한 뒤에 추가 동작이 필요한 경우가 있어 구현하였습니다.

     둘째로 Adapter 인스턴스를 전달받기 위해 getter / setter를 추가 했습니다. 전달 받은 데이터만 가지고 동작을 하면 좋은데 클릭시 전체 데이터에 접근 하거나 하는 경우가 있어서 구현하였습니다.

    
    public abstract class GeneralSelectableView extends GeneralView {
        protected GeneralSelectableAdapter adapter;
    
        ...
    
        public GeneralSelectableView(Context context) {
            super(context);
        }
    
        ...
    
        @Override
        protected void setAdapter(GeneralAdapter adapter) {
            this.adapter = (GeneralSelectableAdapter) adapter;
        }
    
        protected void doItemSelect(int position, T data) {
            boolean select = !isItemSelected();
            setItemSelected(select);
            adapter.setSelectItem(position, select);
            
            ...
    
        }
    }
    
    

     GeneralView를 상속받은 GeneralSelectableView를 생성합니다. doItemSelect()를 통해 Select를 변경합니다. 메소드 동작 자체는 toggle 동작을 하기 때문에 메소드명이 맞지는 않지만 추후에 리팩토링을 예약하며 패스합니다.


     동작에 필요한 클래스는 수정 및 생성이 다 끝났기 때문에 이제 간단하게 동작하는 코드를 작성해 봅시다.

    
    public class ItemView extends GeneralSelectableView {
        @BindView(R.id.selected_imageview)
        protected View selectedView;
        @BindView(R.id.text)
        protected TextView textView;
    
        private String data;
        private int position;
    
        public ItemView(Context context) {
            super(context);
        }
    
        @Override
        public void onBindData(int position, String data) {
            this.position = position;
            this.data = data;
    
            textView.setText(data);
    
            if (isItemSelected()) {
                selectedView.setVisibility(View.VISIBLE);
            } else {
                selectedView.setVisibility(View.GONE);
            }
        }
    
        @Override
        public View createView(ViewGroup parent) {
            return LayoutInflater.from(parent.getContext()).inflate(R.layout.view_recyclerview_item, parent, false);
        }
    
        @OnClick(R.id.list_item)
        protected void onClick(View v) {        
            doItemSelect(position, data);
        }
    }
    
    

     GeneralSelectableView를 상속받은 ItemView 클래스를 생성합니다. Select 상태 변경시 확인을 위해 onBindData() 메소드에서 isItemSelected()를 이용하여 Select 상태정보를 체크합니다.

    
    RecyclerView recyclerView = findViewById(R.id.recyclerview);
    recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
    recyclerView.setAdapter(new GeneralSelectableAdapter<>(getActivity(), ItemView.class, getStringList());
    
    

     마지막으로 RecyclerView에 Adapter를 설정합니다.


     GeneralAdapter와 사용방법은 동일하게 가져가면서 내부적으로 Select 동작을 할 수 있도록 구현하였습니다. 

    코드 사용법을 최대한 동일하게 가져가려고 GeneralAdapter를 상속받아 구현을 하였는데 그로인해서 기존 구현된 클래스에 코드가 새로 추가되는 경우가 생겼습니다. 이부분은 계속 고민이 되는 부분입니다.


     마찬가지로 Click Event나 Select Event는 개발 정책에 따라 GeneralSelectableView 내부에서 처리하거나 외부로 Listener를 연결하면 됩니다.

    반응형
Designed by Tistory.