2023.02.17 - 안드로이드 앱개발자 과정

CHA·2023년 2월 18일
0

Android



ListView - ViewHolder


ListView 의 성능이슈

우리가 만들어보았던 ListView 에서는 사실상 체감하기 어려웠지만, 리스트뷰는 성능적인 부분에서 이슈가 있는 모델이었습니다. 우리처럼 아이템 하나 안에 뷰들을 적게잡고 만들면 상관없겠지만, 뷰의 개수가 많은 경우에는 속도가 많이 느려지게 됩니다.

많은 사람들은 그 이유를 뷰를 새로 생성해야 하기 때문이라고 생각했는데요, 사실은 그 부분에는 문제는 없었습니다. 우리는 Adapter 를 만들때 뷰를 재활용하거든요. 그래서 그 부분에서는 성능적인 이슈가 나올일이 없는거죠. 그래서 어떤 한 사람이 그 이유를 알아보니 findViewById() 메서드 때문이라는 사실을 알아내게 됩니다. 대량의 데이터를 가져와서 아이템에 넣어줄때, 아이템뷰에 있는 뷰들의 id 값을 가져와야 하는데, 이때 사용했던 findViewByid() 가 느려짐의 원인이었던것 이죠.

그래서 이 성능문제를 어떻게 해결해야하나 생각을 해보니 아이템뷰의 각각의 아이템의 id 를 저장해놓는 객체를 만들어 놓으면 findViewById() 를 사용하지 않고도 각각의 아이템들에게 정보를 전달할 수 있을것 같았습니다. 그렇게 만들어진 아이템들의 id 를 모아놓은 클래스가 ViewHolder 입니다.

그리고 ViewHolder 의 객체들을 각 아이템뷰의 태그에 넣어두자고 생각함. 즉, 각각의 아이템뷰들은 자신들의 자식뷰들의 주솟값이 담긴 ViewHolder 객체들을 Tag 에 담아놓는 형태였습니다. 실제로 이러한 방식은 ListView 의 성능 이슈를 깔끔하게 해결할 수 있었습니다.


ViewHolder 클래스

그러면 실제로 어떠한 방식으로 구현할 수 있는지 알아봅시다. 대량의 데이터나 아이템뷰의 시안 준비 등은 생략하고 ViewHolder 에 집중해서 코드를 살펴봅시다.

public class MyAdapter extends BaseAdapter {
    ArrayList<String> items;
    Context context;

    public MyAdapter(Context context, ArrayList<String> items){
        this.context = context;
        this.items = items;
    }
    @Override
    public int getCount() {
        return items.size();
    }
    @Override
    public Object getItem(int i) {
        return items.get(i);
    }
    @Override
    public long getItemId(int i) {
        return i;
    }

    @Override
    public View getView(int i, View view, ViewGroup viewGroup) {
        if( view == null ){
            LayoutInflater inflater = LayoutInflater.from(context);
            view = inflater.inflate(R.layout.listview_item, viewGroup,false);

            ViewHolder holder = new ViewHolder(view);
            view.setTag(holder);
        }
        String item = items.get(i);
        ViewHolder holder = (ViewHolder) view.getTag();
        holder.tv.setText(item);
        return view;
    }
    class ViewHolder{
        TextView tv;

        public ViewHolder(View itemView){
            tv = itemView.findViewById(R.id.tv);
        }
    }
}

우리가 새롭게 만든 Adapter 클래스 입니다. 나머지 부분은 앞선 예제와 동일하며, getView() 메서드 안쪽을 살펴봅시다. 먼저, ViewHolder 클래스를 설계해줍시다. 뷰홀더 클래스에서는 아이템뷰의 자식뷰들의 id 값을 가집니다. 생성자를 통해 현재번째 view 객체를 받아와서, 아이템뷰의 자식뷰 id 를 찾아옵니다.

뷰홀더가 없을때에는 아이템뷰들을 재활용할때마다 find 작업을 해주어야 했다면 뷰홀더를 통해 아이템뷰를 처음에 생성할 때 id를 한번만 받아와서 계속 재활용이 되는 형태가 되었습니다. 뷰 뿐만이 아니라 id 또한 재활용이 되는셈이네요.

    ViewHolder holder = new ViewHolder(view);
    view.setTag(holder);

ViewHolder 클래스를 설계했다면 이제 객체를 생성하고 생성자로 현재번째 view 를 전달해줍니다. 그리고 그 현재번째의 view 에게 Tag 를 설정해서 태그값으로 holder 객체를 전달해줍니다. 이렇게 되면 현재번째 아이템뷰인 view 의 자식뷰들의 참조변수가 태그 안으로 들어가게 되는 셈입니다.

    String item = items.get(i);
    ViewHolder holder = (ViewHolder) view.getTag();
    holder.tv.setText(item);

그리고 현재 보여줄 데이터를 얻어오고, tag 로 저장되어 있던 ViewHolder 객체를 빼옵니다. 그리고 그 holder 객체를 이용하여 자식뷰의 참조변수를 가져와 데이터를 저장시켜줄 수 있습니다.

이렇게 해서 ViewHolder 를 이용한 어댑터 설정이 끝났습니다. 그리고 이러한 뷰홀더를 기반으로 구글에서는 새로운 기술인양 만든 기술이 있는데 그걸 한번 알아봅시다. 아, 그전에 하나만 알아보고 가겠습니다.

위 코드의 중간쯤에 현재번째 view 의 값을 설정하는 부분이 보일까요?
view = inflater.inflate(R.layout.listview_item, viewGroup,false);
이 코드 입니다.

이 코드에서 원래는 두번째 파라미터로 null 값을 전달해주었습니다. 예제를 위한 코드였기 때문이었는데요, inflate() 의 두번째 파라미터는 사실 inflate 를 통해 만들어진 뷰의 아이템을 어디에 붙일건지 정하는 파라미터 입니다. 앞선 리스트뷰 예제에서는 아이템을 리스트뷰에 붙여야합니다. 다만, 이 getView() 메소드는 우리가 호출하는것이 아닌 어댑터 뷰가 호출하는 메소드 입니다. 그래서 어댑터 뷰에서는 뷰를 전달받고 자동으로 알아서 리스트뷰에 붙여주게 됩니다. 그래서 우리가 어디에 붙일지 지정을 하면 안된다는 의미에서 null 값을 전달해주었던것 입니다. 그런데 이는 사실 별로 좋지 않은 코드구성 방식이며, 중간값으로는 getView() 의 파라미터로 전달받은 viewGroup 을 전달해주어야 합니다. 추가적으로 어댑터뷰에 붙이는건 어댑터 뷰가 알아서 해달라는 의미의 attachToRoot 값은 false 로 지정해주어야 합니다. 참고삼아 알아둡시다.


RecyclerView

ListView 를 만들었던 구글에서는 리스트뷰의 성능이슈를 고려하지 못했습니다. 한 개인이 만든 ViewHolder 기술이 대다수의 개발자들에게 전파되자, 구글은 이 ViewHolder 기술을 적용한 새로운 뷰를 만들어 냅니다. 그게 RecyclerView 입니다. 리사이클러뷰는 리스트뷰와 그리드뷰의 특징을 합친 뷰 입니다. 리사이클러뷰 역시 대량의 데이터를 가지고와 화면에 뿌려주는 목적은 리스트뷰와 동일합니다. 그러면 리사이클러뷰를 어떤 방식으로 사용하는지 한번 알아봅시다


1. 대량의 데이터 준비하기

이번 예제에서는 문자열 데이터 2개를 가지고 있는 아이템뷰 여러개를 화면에 띄워주는 예제를 통해 RecyclerView 의 사용법을 알아봅시다. 그러면 먼저 리사이클러뷰의 구성과 데이터를 준비해야겠죠? 가봅시다.

전체 화면 레이아웃 구성하기

----------- activity_main.xml
<RelativeLayout ... 중략>
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        android:orientation="vertical"/>
</RelativeLayout>

리사이클러뷰를 먼저 준비했습니다. 리사이클러뷰 객체를 만들때는 주의할 점이 하나 있습니다. 바로 layoutManager 속성입니다. 리사이클러뷰는 리스트뷰와 그리드뷰를 합친 뷰 입니다. 그렇기 때문에 그리드뷰로 리사이클러뷰를 구성할지, 리스트뷰로 구성할지를 정해줘야 하는것입니다. 이 속성을 설정하지 않는다고 해서 오류는 나지 않습니다. 다만 화면에 표시가 안됩니다. 그러니 꼭 잊지말고 이 속성을 설정해줍시다. 만일 리스트뷰로 설정한다면, orientation 속성도 사용할 수 있습니다. 화면구성을 가로로 할지, 세로로 할지도 정해줄 수 있습니다.

그리고 우리가 흔히 보는 화면중에 격자형도 아니고 리스트뷰의 형태도 아닌 약간 불규칙적으로 아이템들이 구성되어 있는 화면도 보신적이 있을겁니다. 사실 그리드뷰와 리스트뷰 속성 이외에도 StaggeredGridLayoutManager 속성값이 있는데 이 속성값은 나중에 한번 다뤄보겠습니다. 일단 이번 예제에서는 리스트뷰로 설정합시다.

대량의 데이터 준비하기

그러면 이제 데이터를 준비해야겠네요. 우리는 문자열을 두개 보여주는 아이템이 필요하므로 String 멤버변수가 두 개인 Item 클래스를 하나 설계합니다.

public class Item {

    String name;    // 이름
    String message; // 메시지

    public Item(String name, String message) {
        this.name = name;
        this.message = message;
    }
}

그리고 MainActivity.java 에서 ArrayList 를 하나 준비하고 거기에 add 를 이용하여 대량의 데이터를 준비합시다.

public class MainActivity extends AppCompatActivity {
    ArrayList<Item> items = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        items.add(new Item("sam","hello"));
        items.add(new Item("robin","me"));
        items.add(new Item("kim","love"));
        items.add(new Item("hong","why"));
        items.add(new Item("cho","peace"));
        items.add(new Item("lee","like"));
        items.add(new Item("Mee","hug"));
    }
}

자 그러면 이제 대량의 데이터는 준비가 되었으니, 리사이클러뷰의 각각의 아이템들이 어떤 모양을 하고 있는지를 알아야겠죠? 그 시안을 준비해봅시다. 사실 이렇게만 봐도 리스트뷰, 스피너, 그리드뷰에서 대량의 데이터를 화면에 구성하는 방법이 크게 다르지 않습니다. 원리를 잘 이해해 봅시다.


2. 리사이클러뷰의 아이템 시안 준비하기

<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:contentPadding="16dp"
    app:cardElevation="10dp"
    android:layout_margin="4dp"
    app:cardCornerRadius="8dp">
  
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/recycler_item_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20sp"
            android:text="name"
            android:textStyle="bold"
            android:textColor="@color/black"/>

        <TextView
            android:id="@+id/recycler_item_message"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text ="This is message"
            android:background="@color/teal_700"
            android:textColor="@color/white"
            android:layout_below="@id/recycler_item_name"/>
    </RelativeLayout>
</androidx.cardview.widget.CardView>

요번에는 CardView 의 특징도 알아볼겸, 카드뷰를 이용하여 화면을 구성해봅시다. 카드뷰는 레이아웃 자체가 카드처럼 생겼습니다. 그리고 실제로 실행을 시켜보면 뷰 밑쪽에 약간 그림자가 깔려있어 뷰 자체가 약간 떠있는 듯한 느낌을 줍니다. 물론 이 그림자 또한 cardElevation 속성값으로 조절이 가능합니다. 또 하나 카드뷰의 특징은 일반적인 padding 속성을 사용할 수 없다는 점입니다. 그 대신 contentPadding 속성을 사용하여 패딩을 줄 수 있습니다.

우리가 뷰의 모서리를 둥글게 하기 위해서 드로어블 파일을 따로 만들어 배경으로 지정해주고 radius 속성값을 이용하여 둥글게 만들었는데요, 카드뷰는 아예 이 속성이 있습니다. cardCornerRadius 속성을 이용하면 모서리를 둥글게 만들 수 있습니다.

그리고 아이템의 모양을 잡기 위해 상대레이아웃 하나에 텍스트 뷰 두개를 깔았습니다. 이렇게 해서 아이템 뷰들의 모양의 시안을 준비했습니다.


3. 어댑터 만들기 & 붙이기

먼저 ListView 에서는 어댑터의 기본적인 능력을 가져와서 기능 개선을 통해 새로운 어댑터를 만들었습니다. 리사이클러뷰에서도 마찬가지입니다만, 우리가 가져왔던 BaseAdapter 대신 RecyclerView.Adapter 를 가져와 사용해주어야 한다는 점을 기억해줍시다.

그리고 BaseAdapter 의 추상메소드는 4가지가 있었죠. 근데 사실 우리가 목적에 맞게 사용하는 메소드는 정해져있었습니다. getCount() 와 getView() 정도였죠. 그리고 getView() 에서는 아이템뷰를 생성하였고, 그 뷰의 자식뷰들에게 값을 넣어주는 작업이 있었습니다. 그래서 리사이클러뷰에서는 잘 사용하지 않는 메서드는 빼고, 아이템뷰를 생성하는 메소드 따로, 값을 넣는 메소드를 따로 만들어주었습니다.

자 그럼 이제 데이터도 준비가 됐고, 시안도 준비가 되었으니 어댑터 하나를 만들어줍시다. 그 정보들을 어댑터에 전달하고 어댑터가 어댑터 뷰에게 전달하면 모든 작업은 끝납니다.

뷰홀더 클래스 만들기

class VH extends RecyclerView.ViewHolder {

        TextView recycler_item_name;
        TextView recycler_item_message;
        
        public VH(@NonNull View itemView) {
            super(itemView); 

            recycler_item_name = itemView.findViewById(R.id.recycler_item_name);
            recycler_item_message = itemView.findViewById(R.id.recycler_item_message);
        }
    }

리사이클러뷰의 아이템뷰, 그 안의 자식뷰들의 참조변수를 담고 있어야 하는 뷰홀더 클래스를 설계했습니다. 뷰홀더 클래스는 자식뷰의 참조변수의 값들을 담고 있어야 하므로, 멤버변수로 텍스트뷰 2개의 참조변수를 선언해주었습니다. 그리고 RecyclerView.ViewHolder 를 상속받게 해주어야 합니다. 또한 super(itemView); 를 해주어야 뷰홀더 시스템이 가동이 되니 잊지맙시다.

어댑터 클래스 만들기

public class MyAdapter extends RecyclerView.Adapter {
    Context context;
    ArrayList<Item> items;

    public MyAdapter(Context context, ArrayList<Item> items) {
        this.context = context;
        this.items = items;
    }
    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        VH viewHolder = new VH(LayoutInflater.from(context).inflate(R.layout.recyclerview_item,parent,false));
        return viewHolder; 
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        VH vh = (VH)holder; 
        vh.recycler_item_name.setText(items.get(position).name);
        vh.recycler_item_message.setText(items.get(position).message);
    }

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

MyAdapter 클래스를 만들고, RecyclerView.Adapter 클래스를 상속시켜줬습니다. 그랬더니 세 개의 메소드를 오버라이드 해야합니다. 그 세개의 메소드는 onCreateViewHolder() , onBindViewHolder() , getItemCount() 입니다.

우선 리사이클러뷰의 어댑터의 메소드에 관한 내용을 이야기 하기 전에 Tag 에 관한 이야기 한번 해봅시다. 우리는 앞선 예제에서 Tag 를 사용해서 성능이슈를 말끔하게 해결했습니다. 왜 Tag 를 이용했을까요?

앞서 발생했던 문제는 리스트뷰의 성능 문제였습니다. 리스트뷰를 재활용하는것까지는 좋은데, 재활용을 할 때마다 getView() 내부에서 자식뷰의 id 값을 참조하기 위해 findViewById() 를 사용하니 속도가 너무 느려진겁니다. 그래서 이 findViewById() 를 매번 재활용할때마다 사용하지 않고 처음 뷰가 생성될 때 한번만 사용할 수 없을까 고민합니다. 그래서 뷰의 재활용 여부를 판단하는 if 문 내부에서 뷰를 생성해주고, 뷰홀더 객체를 생성한 후에 뷰홀더 객체의 생성자 파라미터로 뷰를 전달하여 자식뷰의 참조변수에 자식뷰 id 값을 담아주고 그 데이터가 담긴 뷰홀더 객체를 아이템뷰의 주머니인 Tag 에 담아 보관하기 시작한겁니다. 이렇게 하니 뷰를 재활용을 할 때에도 find 를 하지 않고 Tag 에서 꺼내서 사용할 수 있게 된거죠.

다만 이러한 과정이 깔끔한 느낌이 아니었는지, 리사이클러뷰에서는 Tag 에 의한 작업은 없습니다. 대신에 뷰를 생성하는 메소드의 리턴값으로 뷰홀더 객체를 전달하고, 값을 지정하는 메소드에서 이 뷰홀더 객체를 받아 사용합니다. 사실상 Tag 를 이용한 느낌인거죠.

그러면 이제 메소드를 하나씩 살펴봅시다.

  • int getItemCount()
    이름 그대로 아이템의 개수를 반환하는 메소드 입니다. 리턴값으로 우리가 받아온 데이터의 개수를 반환해주면 됩니다.

  • RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
    뷰홀더를 만들어야 할것 같은 메소드네요. 이 메소드는 재활용을 할 뷰가 없다면, 뷰를 만들기 위해 자동으로 호출되는 메소드 입니다. 그렇기 때문에 우리는 뷰를 만들어줘야 합니다. 또한 리턴타입을 보면 RecyclerView.ViewHolder 으로 되어있습니다. 즉, 뷰홀더 객체를 하나 만들고, 객체의생성자 파라미터로 만든 뷰를 전달해준 뒤, 뷰홀더 객체 내부에서 자식뷰들의 id 값을 저장하고 저장된 뷰홀더 객체를 반환하는 메소드 입니다. 그리고 이렇게 반환된 뷰홀더 객체는 다음에 소개할 onBindViewHolder() 메소드의 파라미터로 전달됩니다.

  • void onBindViewHolder(RecyclerView.ViewHolder holder, int position)
    이 메소드에서는 아이템 뷰에 들어있는 자식뷰들에게 값을 넣어주는 작업을 하는 메소드 입니다. 우리가 onCreateViewHolder() 메소드에서 리턴해주었던 뷰홀더 객체를 첫번째 파라미터로 받습니다. ( 이때 업캐스팅이 일어납니다 ) 그렇게 받아온 뷰홀더 객체를 VH vh = (VH)holder 로 다운캐스팅을 해줍니다. 그러면 이 뷰홀더 객체 안에는 아이템뷰의 자식뷰들의 id 값이 들어있겠죠. 그러면 이제 데이터를 넣어주기만 하면 됩니다.

어댑터 붙이기

        recyclerView = findViewById(R.id.recyclerview);
        adapter = new MyAdapter(this,items);
        recyclerView.setAdapter(adapter);

이제까지 했던 방식과 동일하게 어댑터를 붙여주면 됩니다. 이렇게 하면 리사이클러뷰의 작업이 끝이 납니다.


RecyclerView 클릭 이벤트

자 그럼 리사이클러뷰도 만들어봤겠다, 아이템뷰를 클릭 해봐야겠죠? 클릭 이벤트를 만들어봅시다. 그런데 이제까지 했던대로 MainActivity.java 에서 클릭이벤트를 할 수 없게 되었습니다. 리사이클러뷰의 경우 클릭 이벤트를 뷰홀더 클래스 내부에서 정의해주어야 합니다.

class VH extends RecyclerView.ViewHolder {

    TextView recycler_item_name;
    TextView recycler_item_message;
    public VH(@NonNull View itemView) {
        super(itemView); 
        recycler_item_name = itemView.findViewById(R.id.recycler_item_name);
        recycler_item_message = itemView.findViewById(R.id.recycler_item_message);
       
       itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int position = getLayoutPosition();
                items.get(position);
                Toast.makeText(context, "clicked : " + position, Toast.LENGTH_SHORT).show();
            }
        });
    }
}

클릭 리스너를 생성하는 위치만 다른 뿐, 이벤트를 처리하는 방식은 동일합니다.


RecyclerView 예제 - OnePiece

RecyclerView 에 대해 배워봤으니 이를 이용하는 예제 하나를 만들어보면서 추가적으로 이야기할 부분들을 이야기 해봅시다.


0. 화면 구성하기

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    android:background="#DADADA"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:gravity="center">

        <Button
            android:id="@+id/btn_add"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:text="ADD"/>

        <Button
            android:id="@+id/btn_delete"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="5dp"
            android:text="DELETE"/>

        <Button
            android:id="@+id/btn_linear"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="linear"
            android:layout_margin="5dp"/>

        <Button
            android:id="@+id/btn_grid"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="grid"
            android:layout_margin="5dp"/>
    </LinearLayout>

    <androidx.recyclerview.widget.RecyclerView
        android:padding="16dp"
        android:id="@+id/recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        android:orientation="vertical"/>
</LinearLayout>

이번에는 리사이클러뷰의 데이터를 추가, 삭제를 해볼 예정이며 리스트뷰의 형태와 그리드 뷰의 형태도 알아보겠습니다. 그래서 리니어 버튼을 누르면 리스트뷰의 형태로 나오고, 그리드 버튼을 누르면 그리드뷰의 형태가 나오도록 해봅시다.


1. 대량의 데이터 준비하기

이번 예제에서는 선원의 이름과 역할, 프로필 사진과 이미지 사진을 화면에 띄워봅시다. 그러기 위해 일단 Item 클래스 하나를 만들어 대량의 데이터를 만들 준비를 합시다.

public class Item {
    String name;    
    String role;    
    int profile;    
    int imgId;    

    public Item(String name, String role, int profile, int imgId) {
        this.name = name;
        this.role = role;
        this.profile = profile;
        this.imgId = imgId;
    }
}

그리고 대량의 데이터를 준비합니다.

public class MainActivity extends AppCompatActivity {

    ArrayList<Item> items = new ArrayList<>();
    Button addBtn,deleteBtn,linearBtn,gridBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        items.add(new Item("루피","해적단의 선장",R.drawable.crew_luffy,R.drawable.bg_one01));
        items.add(new Item("조로","해적단의 부선장",R.drawable.crew_zoro,R.drawable.bg_one02));
        items.add(new Item("나미","해적단의 항해사",R.drawable.crew_nami,R.drawable.bg_one03));
        items.add(new Item("우솝","해적단의 저격수",R.drawable.crew_usopp,R.drawable.bg_one04));
        items.add(new Item("상디","해적단의 요리사",R.drawable.crew_sanji,R.drawable.bg_one05));
        items.add(new Item("초파","해적단의 의사",R.drawable.crew_chopper,R.drawable.bg_one06));
        items.add(new Item("니코로빈","해적단의 역사가",R.drawable.crew_nicorobin,R.drawable.bg_one07));


        addBtn = findViewById(R.id.btn_add);
        deleteBtn = findViewById(R.id.btn_delete);
        linearBtn = findViewById(R.id.btn_linear);
        gridBtn = findViewById(R.id.btn_grid);
    }
}

2. 아이템 시안 준비하기

대량의 데이터는 준비했으니, 이제 리사이클러뷰의 아이템의 시안을 준비합시다.

<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:cardCornerRadius="16dp"
    android:layout_marginTop="8dp"
    android:layout_marginBottom="8dp"
    android:layout_marginLeft="2dp"
    android:layout_marginRight="2dp">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <de.hdodenhof.circleimageview.CircleImageView
            android:id="@+id/civ_profile"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:layout_margin="8dp"
            android:src="@drawable/crew_luffy"/>

        <TextView
            android:id="@+id/name_tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20sp"
            android:textColor="@color/black"
            android:textStyle="bold"
            android:layout_toRightOf="@id/civ_profile"
            android:layout_alignTop="@id/civ_profile"
            android:text="루피"/>
        <TextView
            android:id="@+id/role_tv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@id/name_tv"
            android:layout_alignLeft="@id/name_tv"
            android:text="해적단의 선장"/>

        <ImageView
            android:id="@+id/img_iv"
            android:layout_width="match_parent"
            android:layout_height="240dp"
            android:layout_below="@id/civ_profile"
            android:layout_marginTop="4dp"
            android:src="@drawable/bg_one01"
            android:scaleType="fitXY"/>
    </RelativeLayout>
</androidx.cardview.widget.CardView>

아이템뷰의 전체 레이아웃은 카드뷰로, 안쪽 자식뷰들은 텍스트 뷰 2개 이미지 뷰 2개로 설정했습니다.


3. 뷰홀더 클래스

class VH extends RecyclerView.ViewHolder{

    CircleImageView civ_profile;
    TextView name_tv, role_tv;
    ImageView img_iv;
    public VH(@NonNull View itemView) {
        super(itemView);

        civ_profile = itemView.findViewById(R.id.civ_profile);
        name_tv = itemView.findViewById(R.id.name_tv);
        role_tv = itemView.findViewById(R.id.role_tv);
        img_iv = itemView.findViewById(R.id.img_iv);

        itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int position = getLayoutPosition();
                Item item = items.get(position);
                Toast.makeText(context, item.name + "\n" + item.role, Toast.LENGTH_SHORT).show();
            }
        });
    }
}

뷰홀더 클래스를 만들어주었으며, 자식뷰 4개의 id 값을 받아오기 위한 클래스 입니다. 또한 아이템뷰의 클릭 리스너를 달아 주었습니다.


4. 어댑터 만들기 & 붙이기

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.VH> {

    Context context;
    ArrayList<Item> items;

    public MyAdapter(Context context, ArrayList<Item> items) {
        this.context = context;
        this.items = items;
    }

    @NonNull
    @Override
    public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(context).inflate(R.layout.recyclerview_item,parent,false);
        VH holder = new VH(itemView);
        return holder; 
    }

    @Override
    public void onBindViewHolder(@NonNull VH holder, int position) {
        Item item = items.get(position);

        holder.name_tv.setText(item.name);
        holder.role_tv.setText(item.role);
        holder.civ_profile.setImageResource(item.profile);
        holder.img_iv.setImageResource(item.imgId);
    }

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

이 부분은 이야기할게 조금 있습니다. 어댑터를 만들기 위해서는 기본적인 어댑터의 기능을 가지고 있는 클래스를 상속받아야 합니다. 저번 예제에서는 RecyclerView.Adapter 를 상속받았었죠. 하지만 위 코드를 보면 RecyclerView.Adapter<MyAdapter.VH> 를 상속받았습니다. 즉, 제네릭 타입이 있는 클래스를 상속받은 것 입니다.

제네릭 타입이 없다면 onCreateViewHolder() 메소드는 기본으로 RecyclerView.ViewHolder 를 리턴타입으로 가집니다. 단, 제네릭을 사용하여 제네릭 타입으로 <MyAdapter.VH> 를 사용한다면 내가 만든 뷰홀더 클래스를 사용하는게 됩니다. 그러면 onCreateViewHolder()메소드의 리턴타입도 VH 가 되게 됩니다. 이게 어떤 의미가 있을까요? 앞선 예제에서 뷰홀더 객체를 생성하고, onBindViewHolder() 메소드에서 파라미터로 받아올 때, 업캐스팅으로 타입 형변환을 했었습니다. 그리고 다시 뷰홀더 객체를 사용하기 위하여 다운캐스팅을 통해 자식뷰의 참조변수에 접근했었죠.

제네릭 타입을 사용하면 그러한 과정이 없어집니다. onCreateViewHolder() 메소드의 리턴타입 자체가 VH 이기 때문에 onBindViewHolder() 메서드의 파라미터로 받을 때도 VH 그대로 받아 형변환이 일어나지 않기 때문입니다. VH 로 받기 때문에 당연히 다운캐스팅도 필요가 없어지죠. 상당히 편리하죠?

자, 이렇게 해서 어댑터 클래스도 다 만들어보았습니다. 이제 붙여주는 일만 남았네요. 이것 또한 3줄짜리 코드이며 간단하기 때문에 따로 작성은 하지 않겠습니다. 그 대신 우리에게는 할 일이 남아있죠. 추가,삭제와 리사이클러뷰 타입의 변경 기능을 만들어봅시다.


추가, 삭제, 타입 변경

데이터 추가와 데이터 삭제하기

... 중략
addBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        items.add(0,new Item("new1","해적단의 신입",R.drawable.bg_one08,R.drawable.bg_one09));

        adapter.notifyDataSetChanged();
        adapter.notifyItemInserted(0);
        recyclerView.scrollToPosition(0);
    }
});

deleteBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
    	items.remove(0);
    }
});

ArrayList 의 add 기능을 이용하여 데이터를 추가시켜주어야 합니다. 주의할 점은 우리가 데이터를 추가하는 대상은 리사이클러뷰에게 하는것이 아닙니다. 우리가 만든 대량의 데이터에 데이터를 추가하고 어댑터에게 추가되었다고 공지를 해주어야 합니다.

어댑터에게 공지하는 코드가 adapter.notifyDataSetChanged(); 였습니다. 단, 이 메서드는 어댑터에게 데이터의 변경이 이뤄지긴 이뤄졌는데 데이터 전체가 다 이루어졌다고 공지하는 메서드입니다. 즉, 데이터 전체를 처음부터 다시 다 만들어줍니다. 그러니 리소스의 낭비가 심할수 밖에 없을것 같습니다.

위 처럼 데이터가 소량으로 추가되는 경우라면 adapter.notifyItemInserted(); 메서드를 사용하는것이 좋습니다. 파라미터로는 데이터가 추가된 인덱스번호를 알려주면 됩니다. 이렇게 하고 실행해서 데이터를 추가해보면 스크롤은 따라서 안올라가는걸 확인할 수 있습니다. 그럴때는 recyclerView.scrollToPosition(0); 를 이용하면 스크롤을 맨 위로 올릴 수 있습니다.

데이터의 제거는 추가의 로직과 동일합니다.

리사이클러뷰 모양 바꾸기

모양을 바꾸는 것은 그리 어렵지 않습니다. GridView 로 바꾸고 싶다면 GridLayoutManager 객체를 만들고, 리사이클러뷰의 메서드인 setLayoutManager() 의 파라미터로 GridLayoutManager 의 객체를 전달해주면 됩니다. 리스트뷰도 같은 방식으로 하면됩니다. 단, GridLayoutManager 의 객체를 만들때에는 두번째 파라미터로 몇 열을 만들것인지 알려주어야 합니다.

linearBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(MainActivity.this,LinearLayoutManager.VERTICAL,false);
        recyclerView.setLayoutManager(linearLayoutManager);
    }
});

gridBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        GridLayoutManager gridLayoutManager = new GridLayoutManager(MainActivity.this,2);
        recyclerView.setLayoutManager(gridLayoutManager);
    }
});
profile
Developer

0개의 댓글