본문 바로가기

Android

Android RecyclerView에 대한 모든 것

안드로이드에서는 똑같은 class형태의 객체들을 나열하는 데에 RecyclerView라는 형식을 많이 사용한다.

 

ListView와 같은 기존 방식보다는 효율적이고 빠르다고 할 수 있다.

 

쉽게 스크롤할 수 있으며, 각 항목마다 접근과 이벤트를 쉽게 발생시킬 수 있다.

 

1. 생성 방법

RecyclerView의 생성방법에는 여러가지가 있겠지만, 내가 사용하는 방법 하나만을 포스트하겠음.

 

다만, 내용이 생각보다 양이 많고 처음하는 사람에게는 복잡하게 느껴질 수 있다.

 

하지만 몇 번 하게되면 나중에는 손 쉽게 할 수 있음.

 

연습용으로 쉽게 따라오려면 변수명이나 레이아웃 이름 등은 똑같이 하며 참고하는 것이 좋다.

 

1-1. 액티비티 레이아웃에 RecyclerView 추가

추가하려는 액티비티에 RecyclerView를 추가하는 과정이다.

 

해당 액티비티의 레이아웃 파일을 연다. (MainActivity라면 기본적으로 activity_main.xml이라는 파일명으로 존재하며, res/layout 폴더에 있다.)

 

우선, 나는 LinearLayout보다 ConstraintLayout을 선호하는 편이라, 거의 모든 레이아웃은 ConstraintLayout을 기준으로 설명하겠다.

 

레이아웃에서 형식을 바꾸고 싶다면 디자인 패널 하단의 Text 버튼을 눌러 코드의 상위 및 하위의 레이아웃 태그를 바꾸면 된다. (기본적으로는 LinearLayout으로 되어 있을 것이다.)

빨간 밑줄 부분이 레이아웃 형식임

레이아웃 형식에 대해선 나중에 자세히 포스팅하여 다루겠다.

 

다시 디자인 패널로 돌아와서, 좌측 상단 Palette의 Common 탭을 보면 바로 RecyclerView가 있다.

(많이 쓴다는 뜻)

 

다른 것과는 다르게 오른쪽에 다운로드 표시가 있는데, 미리 import되어 있지 않고 사용자가 직접 해주어야 한다.

 

다운로드 표시를 누르면 오류 표시가 뜨는데 그냥 OK를 눌러준다.

그러면 Android Studio가 알아서 implement해준다.

 

다운로드 프로세스가 끝나면 RecyclerView 오른쪽에 다운로드 표시가 없어진 것을 볼 수 있는데, 이제 다른 항목들처럼 끌어다 쓸 수 있다는 뜻임.

 

끌어다가 레이아웃에 뿌려주자.

 

RecyclerView가 넓게 퍼지면서 생성되는데, 상하좌우 dock을 맞춰준다.

 

그리고 해당 id를 설정한다.

 

나는 그냥 recycler_view_1로 하였다. (사실 id는 실제로 코드가 길어지고 레이아웃이 많아질 수록 서로 헷갈리는 것이 많이지므로 id만 보고 이게 뭔지 알 정도로 구분하는게 좋다. 서로 다른 view가 id 겹치고 하면 난장판이 된다.)

 

액티비티에 추가가 완료되었다!

 

1-2. 아이템 레이아웃 생성

우선 res/layout 폴더를 우클릭하여 New>Layout resource file을 선택한다.

 

파일 이름, 레이아웃 등등 선택 사항이 많은데, 잘 모르겠다면 파일 이름만 적고 OK하면 된다.

 

지금 만드는 레이아웃 파일은 RecyclerView 안에 들어가는 항목(item)의 레이아웃이니 ~~_item 이런 식으로 써도 좋다.

 

나는 send_message_item이라는 이름의 레이아웃으로 만들었다.

 

Root element는 기본적으로 LinearLayout으로 되어있는데 개인적으로 ConstraintLayout을 선호하는 편이라, 나는 ConstraintLayout으로 설정해주었다. (Cons만 써도 candidates 들을 보여줌)

 

빈 레이아웃이 생성된 것을 확인했으면 다시 액티비티 클래스로 돌아간다.

 

1-3. 액티비티 클래스에 코드 추가

RecyclerView는 기본적으로 RecyclerView, Adapter, LayoutManager 3개로 구성된다.

 

View 자체와 데이터와 view를 binding 해주는 adapter, 배치 형태를 잡아주는 LayoutManager가 모두 필요하다.

 

추가로, List 형태의 class 배열이 필요하다. (결과적으로 recyclerView에 추가하는 아이템들을 담는 것)

 

나는 MainActivity 위에 바로 RecyclerView를 추가하는 예시를 들겠다.

public class MainActivity extends AppCompatActivity {

    RecyclerView recyclerView;
    RecyclerAdapter adapter;
    RecyclerView.LayoutManager layoutManager;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

위와 같이 3개를 추가한다.

 

RecyclerAdapter는 아마도 빨간 줄로 뜨거나 Unresolved Symbol로 뜰텐데, 나중에 추가하며 오류는 사라질 것이다.

 

어답터는 각자 이름을 알아서 정하길 바란다. 나는 그냥 RecyclerAdapter로 하였다.

 

카카오톡 같은 메신저의 메시지를 class로 예를 들겠다.

 

이 메시지 하나하나가 모두 recyclerView에 해당하는 아이템이다.

(실제로도 카카오톡에서 RecyclerView를 사용하여 메시지 표시를 구현했는지는 모름 ㅎ;;)

 

이 메시지 클래스의 이름을 TextMessage라 하겠다.

 

클래스 내용은 대충 이렇게 되어있다고 가정하자. (isReceived = 내가 보낸 문자면 false, 받는 문자면 true)

 

public class TextMessage {
    private String senderName;
    private long timestamp;
    private String messageContent;

    private boolean isReceived;

    public TextMessage(String senderName, long timestamp, String messageContent, boolean isReceived) {
        this.senderName = senderName;
        this.timestamp = timestamp;
        this.messageContent = messageContent;
        this.isReceived = isReceived;
    }

    public String getSenderName() {
        return senderName;
    }

    public void setSenderName(String senderName) {
        this.senderName = senderName;
    }

    public long getTimestamp() {
        return timestamp;
    }

    public void setTimestamp(long timestamp) {
        this.timestamp = timestamp;
    }

    public String getMessageContent() {
        return messageContent;
    }

    public void setMessageContent(String messageContent) {
        this.messageContent = messageContent;
    }

    public boolean isReceived() {
        return isReceived;
    }

    public void setReceived(boolean received) {
        isReceived = received;
    }
}

이제 item의 클래스도 완성되었으니 다시 MainActivity로 돌아가서 리스트로 하나 만들어준다.

public class MainActivity extends AppCompatActivity {

    RecyclerView recyclerView;
    RecyclerAdapter adapter;
    RecyclerView.LayoutManager layoutManager;

    ArrayList<TextMessage> messageBundle = new ArrayList<>();

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

        recyclerView = (RecyclerView) findViewById(R.id.recycler_view_1);
        recyclerView.setHasFixedSize(true);
        
        layoutManager = new LinearLayoutManager(this);
        ((LinearLayoutManager) layoutManager).setReverseLayout(true);
        ((LinearLayoutManager) layoutManager).setStackFromEnd(true);
        
        recyclerView.setLayoutManager(layoutManager);
        adapter = new RecyclerAdapter();
        recyclerView.setAdapter(adapter);
    }
}

recyclerView를 id를 통해 MainActivity의 view와 연결시켜준다.

 

setHasFixedSize() 함수는 RecyclerView 전체 크기의 변경이 없을 경우 사용 시 성능이 개선된다.

 

RecyclerView의 크기가 변경될 여지가 없다면 true를 넣어주자.

 

layoutManager의 setReverseLayout(true)와 setStackFromEnd(true)는 선택사항이다.

 

둘 다 써넣으면 순서가 역순이 되고 아이템도 끝(역순이니 처음이 됨)에 출력한다.

 

예를 들어, 원래대로라면 다음과 같이 아이템이 추가된다.

 

하지만 두 함수를 모두 써놓으면,

 

위와 같은 항목 추가가 이루어진다. (하늘색 부분이 실제로 보게되는 부분)

더보기

사실 실제로 아래와 같이 아이템이 추가된다고 해서 7번 항목으로 자동 스크롤이 되진 않는다.

item이 add될 때 실행되는 코드에 아래와 같은 코드를 넣어줘야 스크롤이 제대로 갱신된다.

layoutManager.scrollToPosition(messageBundle.size()-1);

전반적인 준비는 끝났다.

 

이제 Adapter Class를 추가할 차례다.

 

RecyclerView Adapter에 커스텀 adapter를 적용할 수 있는 것이다.

 

1-3. Custom RecyclerView Adapter 생성

어답터 클래스를 생성한다.

 

나는 RecyclerAdapter로 이름을 지었으니, RecyclerAdapter.class로 만들었다.

이 클래스를 이제부터 RecyclerView에 적용할 수 있는 adapter로 꾸며볼 것이다.

 

우선, 클래스 내에 이름이 ViewHolder라는 또 다른 클래스를 생성한다.

 

위와 같이 클래스를 생성하고 RecyclerView.ViewHolder라는 클래스를 extend 해준다.

 

그 후 빨간 줄 위에 커서를 올리고 alt+Enter를 누르면 위 사진과 같이 Create constructor...가 뜨는데, 엔터 쳐주면 됨.

 

위와 같이 생성자가 만들어진다.

 

View 인자 이름을 itemView에서 view로 바꿔준다. (짧을 수록 좋음, v를 써도 좋다.)

 

다시 상위 클래스인 RecyclerAdapter로 돌아가 오른쪽에 다음과 같이 코드를 넣는다.

 

파란 부분이 추가한 부분이다. ViewHolder의 상위 클래스에는 어답터 클래스의 이름(빨간 줄)과 일치하게 한다.

 

위에서 constructor를 추가했던 것과 같은 방식으로 unimplemented method를 추가해준다.

 

후보가 getItemCount(), onBind뭐시기(), onCreate블라블라() 3개 나오는데 다 추가해준다. 그냥 OK 누르면 됨.

 

전체적인 모습이다.

 

OnCreateViewHolder()는 해당 adapter와 연결하는 recyclerView에 추가할 item의 레이아웃과 item Data를 bind함.

onBindViewHolder()는 recyclerView 자체와 item 데이터셋을 서로 연결시켜주는 과정이다.

getItemCount()는 데이터셋의 데이터 개수이다.

 

일단 @NonNull 위 부분에 액티비티에서 추가한 데이터셋을 하나 생성한다.

ArrayList<TextMessage> messageBundle = new ArrayList<>();

그리고 생성자를 통해 액티비티의 데이터셋과 해당 데이터셋을 얕은 복사를 통해 binding 해준다.

 

얕은 복사의 의미는 옆의 포스트를 참고할 것 :  2019/10/23 - [JAVA] - JAVA 객체 복사 방식 (깊은 복사 vs 얕은 복사)

public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.ViewHolder>{
    final static int SEND = 1000;
    final static int RECEIVE = 1001;
    ArrayList<TextMessage> messageBundle = new ArrayList<>();
    Context mContext;
    
    public RecyclerAdapter(ArrayList<TextMessage> bundle){
        this.messageBundle = bundle;
    }
    
    @NonNull
    ...

위와 같이 생성자를 만들어 bind. 나중에 필요하니 Context도 미리 선언해두자.

 

위의 final static int 값들은 나중에 메시지의 타입을 분리해주기 위해 추가했다.

 

위와 같이 설정해 두면 액티비티에서 추가한 데이터가 이쪽으로도 연동되어 추가된다. (삭제도 마찬가지)

 

onCreateViewHolder()는 다음과 같이 써준다.

@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    Context mContext  = parent.getContext();
    LayoutInflater inflater = (LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    View view = inflater.inflate(R.layout.class_item, parent, false);
    ViewHolder holder = new ViewHolder(view);
    
    return holder;
}

R.layout 뒤에는 자신이 만든 데이터셋 아이템 레이아웃 이름을 넣으면 된다.

 

onBindViewHolder()는 잠시 스킵...

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

getItemCount()는 데이터셋의 사이즈를 넣는다.

 

이제 다시 ViewHolder 클래스로 가기 전에, item 레이아웃을 꾸며야 한다.

 

 

1-4. Item Layout 완성

send_message_item.xml로 돌아가서, 빈 View에 오브젝트를 추가하여 꾸며보자.

 

메시지의 내용을 보여줄 TextView를 Palette에서 끌어다가 넣는다.

 

ConstraintLayout은 상하좌우 dock을 커서로 잡아당겨 붙일 수 있다.

이 이후로 기본적인 것들은 넘어가도록 하겠다. 내용이 너무 길어져서... 나중에 포스트에서 자세히 다루겠다.

 

메시지 내용 TextView

id = content_view

maxWidth = 260dp (Text에서 직접 속성 추가해야함 : android:maxWidth="260dp")

padding = 5dp (TextView와 내용 사이의 padding 추가)

elevation = 3dp (TextView 밑에 깔리는 그림자)

 

메시지 보낸 시간 TextView

id = time_view

textSize = 12sp

text = 오후 2:35 (예시 테스트)

 

메시지 보낸 사람 TextView

id = name_view

textSize = 12sp

 

그 후 Component Tree에서 최상위 ConstraintLayout을 선택하여 Attributes에서 layout_height을 wrap_content로 설정

(이렇게 설정하지 않으면 RecyclerView에 아이템이 하나밖에 안 보여버렷...)

 

전체적인 모습은 위와 같다.

 

이제 카카오톡과 조금 비슷하게 보이게 하기 위해 xml 파일을 custom하게 만들어 볼 것이다.

 

귀찮다면 그냥 content_view의 background Color를 임의로 주어 대충 꾸며도 된다.

 

1-4-1. Custom Resource 만들기

 

res/drawable 탭을 오른쪽 클릭하여 New>Drawable Resource File를 선택한다.

 

이름은 send_message_skin으로 하겠다.

 

생성하면 아래와 같은 코드가 기본적으로 써져있다.

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

</selector>

<selector>태그를 지워버리고, 코드를 써넣어 아래와 같이 만든다.

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle" >

    <!-- View background color -->
    <solid
        android:color="#FFF75E" >
    </solid>

    <!-- The radius makes the corners rounded -->
    <corners
        android:radius="8dp">
    </corners>
</shape>

이 상태로 저장 후, 위의 sent_content_view의 background 오른쪽 버튼을 클릭하여 방금 만든 skin을 입힌다.

만든 drawable은 Drawable 탭의 Project 내에서 찾을 수 있다.

 

OK를 눌러 적용해주면,

 

그럴싸하게 바뀌었다!

 

더보기

덧붙여서, 만든 shape(skin)에 border 선이 없어 마음에 들지 않는다면

<stroke android:width="1dp" android:color="#DDDD44"/>

위 코드를 drawable 파일 corners 태그 밑에 달아주면 된다.

 

같은 방식으로 receive_message_item.xml 도 짜준다.

단, id는 send_message_item.xml 의 것들과 같아야 한다. (복사했다면 문제 X)

 

 

1-5. Custom RecyclerView Adapter 마저 구현

다시 RecyclerAdapter.java로 돌아온다.

 

ViewHolder 내에 아까 item 레이아웃에 추가했던 view들을 추가하고, 생성자에서 id로 연결해준다.

 

다음과 같이 정리한다.

public class ViewHolder extends RecyclerView.ViewHolder{
    TextView nameView;
    TextView timeView;
    TextView contentView;

    public ViewHolder(@NonNull View view) {
        super(view);
        nameView = view.findViewById(R.id.name_view);
        timeView = view.findViewById(R.id.time_view);
        contentView = view.findViewById(R.id.content_view);
    }
}

이제 ViewHolder가 완성되었으니 onBindViewHolder() 구현을 하러 가보자.

 

해당 함수는 위치(index)별로 데이터셋에서 가져온 데이터를 View에 넣는 함수다.

@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
    TextMessage curMsg = messageBundle.get(position);

    SimpleDateFormat sdf = new SimpleDateFormat("a h시 m분", Locale.KOREA);
    
    holder.nameView.setText(curMsg.getSenderName());
    holder.contentView.setText(curMsg.getMessageContent());
    holder.timeView.setText(sdf.format(new Date(curMsg.getTimestamp())));
}

holder의 하위 객체로 아까 만들었던 ViewHolder 클래스의 component에 접근할 수 있다.

 

아! 생각해보니 받는 문자와 보내는 문자가 서로 View의 형태가 다르기 때문에, onCreateViewHolder()를 수정해주어야 한다.

@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    Context mContext  = parent.getContext();
    LayoutInflater inflater = (LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    View view = null;
    if(viewType == SEND){
        view = inflater.inflate(R.layout.send_message_item, parent, false);
    }else{
        view = inflater.inflate(R.layout.receive_message_item, parent, false);
    }
    ViewHolder holder = new ViewHolder(view);
    return holder;
}

onCreateViewHolder의 인자 중 viewType이 view를 구별할 수 있도록 또 다른 함수를 추가해줘야 한다.

@Override
public int getItemViewType(int position) {
    TextMessage curMsg = messageBundle.get(position);
    return curMsg.isReceived()?RECEIVE:SEND;
}

이렇게 구분해주는 인자를 넣어주면 viewType을 통해 분리가 가능함.

 

1-6. 액티비티 클래스에 마저 완성

다시 MainActivity로 돌아오면 오류가 하나만 남아있게 된다.

 

adatper 초기화 부분을 다음과 같이

adapter = new RecyclerAdapter(messageBundle);

생성자를 통해 데이터셋을 넘겨주면 끝이다.

 

1-7. 테스트

AVD (가상 에뮬레이터) 나 자신의 핸드폰으로 테스트를 해보자.

 

그 전에, 테스트를 위해 몇 가지를 수정했다.

 

액티비티의 레이아웃에 위와 같이 테스트할 수 있는 뷰를 몇 가지 추가하였다.

 

액티비티 본문의 onCreate()에도 다음과 같은 코드를 추가했다.

final Button oppSend = (Button)findViewById(R.id.opp_send_button);
final Button meSend = (Button)findViewById(R.id.me_send_button);
final EditText contentText = (EditText)findViewById(R.id.send_content);
oppSend.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if(contentText.getText().toString().length() == 0)return;
        messageBundle.add(new TextMessage("상대방", System.currentTimeMillis(), contentText.getText().toString(), true));
        adapter.notifyDataSetChanged();
        layoutManager.scrollToPosition(messageBundle.size()-1);
        contentText.setText("");
    }
});
meSend.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if(contentText.getText().toString().length() == 0)return;
        messageBundle.add(new TextMessage("나", System.currentTimeMillis(), contentText.getText().toString(), false));
        adapter.notifyDataSetChanged();
        layoutManager.scrollToPosition(messageBundle.size()-1);
        contentText.setText("");
    }
}); 

각각의 버튼과 EditText가 무엇인지 레이아웃을 보고 짐작이 갈 것이다.

 

기존에 추가한 setReversedLayout() 함수와 setStack~~() 함수는 주석처리하였다. (거꾸로 보는 뷰가 아니기 때문)

 

그대로 AVD에 넣고 돌려보았다.

 

매우 잘 되는 것을 볼 수 있다.

 

위의 코드와 아예 똑같진 않고.. 사실 색깔을 다크테마로 바꾸긴 했다.

 

다른 내용은 천천히 추가할 것이다.