선택 위젯(Selection Widget) : | 여러 개의 아이템 중에 하나를 선택할 수 있는 리스트 모양의 위젯. 어댑터(Adapter) 패턴을 사용한다. |
위젯은 각 아이템을 보여주기만 할 뿐이다. 각 아이템을 위한 뷰는 어댑터가 만든다.
어댑터에서 반환되는 객체는 컨테이너 객체이다.
리싸이클러뷰(RecyclerView) : | 기본적으로는 상하 스크롤이며 좌우 스크롤도 가능하다. 캐시(Cache) 메커니즘이 구현되어있음(메모리를 효율적으로 사용할 수 있도록) |
리싸이클러뷰 = 껍데기 / 어댑터 = 숙주(?) 같은 느낌으로 이해하면 된다.
예제 2개를 진행할 것이다.
- <리싸이클러뷰 - 리스트 모양 만들기>
- <리싸이클러뷰 - 격자 모양 만들기>
- <리싸이클러뷰(심화) - 클릭 이벤트 만들기>
<리싸이클러뷰 - 리스트 모양 만들기>
⭐1. activity_main.xml ⭐
코드
<?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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity" >
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/RecyclerView"
/>
</LinearLayout>
리싸이클러뷰의 id를 설정해준다.
⭐2. 새로운 클래스 만들기 (Person.java)⭐
먼저 변수(name, mobile)를 2개 선언한 다음 생성자를 만든다. (Generate -> Constructor)
생성자를 추가한 뒤에 get, set 메서드를 추가한다. (Generate -> Getter and Setter)
package com.example.chapter7_4;
public class Person {
// 이름과 전화번호를 저장해두기 위한 변수
String name;
String mobile;
public Person(String mobile, String name) {
this.mobile = mobile;
this.name = name;
}
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
public String getMobile(){
return this.mobile;
}
public void setMobile(String mobile){
this.mobile = mobile;
}
}
⭐3. 새로운 어댑터 만들기 (PersonAdapter.java)⭐
코드
package com.example.chapter7_4;
public class PersonAdapter extends RecyclerView.Adapter<PersonAdapter.ViewHolder> {
ArrayList<Person> items = new ArrayList<Person>();
// 각각의 아이템을 위한 뷰는 뷰홀더에 담아두게 된다.
static class ViewHolder extends RecyclerView.ViewHolder {
TextView textView_name;
TextView textView_mobile;
public ViewHolder(@NonNull View itemView) {
super(itemView); // 부모 클래스(ViewHolder)로 전달되는 뷰 객체를 참조함.
textView_name = itemView.findViewById(R.id.textView);
textView_mobile = itemView.findViewById(R.id.textView2);
}
public void setItem(Person item){ // 뷰홀더에 들어있는 뷰 객체의 데이터 세팅
textView_name.setText(item.getName());
textView_mobile.setText(item.getMobile());
}
}
@NonNull
@Override
// 자동호출 (뷰홀더 객체가 만들어질 때)
public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
// 각 아이템을 위해 정의한 XML 레이아웃을 이용해 뷰 객체를 만들어준다.
LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
View itemView = inflater.inflate(R.layout.person_item, viewGroup, false);
return new ViewHolder(itemView); // 뷰 객체를 뷰홀더 객체에 담아 반환한다.
}
@Override
// 자동호출 (뷰홀더 객체가 재사용될 때)
public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
// 기존의 뷰 객체를 사용하고 데이터만 바꿔준다.
Person item = items.get(position);
viewHolder.setItem(item);
}
@Override
public int getItemCount() { // 아이템의 개수를 반환
return items.size();
}
public void addItem(Person item){
items.add(item);
}
public void setItems(ArrayList<Person> items){
this.items = items;
}
public Person getItem(int position){
return items.get(position);
}
public void setItem(int position, Person item){
items.set(position, item);
}
}
PersonAdapter 클래스가 RecyclerView.Adapter 클래스를 상속하도록 한다.
public class PersonAdapter extends RecyclerView.Adapter<PersonAdapter.ViewHolder>
빨간 줄이 생길 것이다. PersonAdapter에 커서를 가져다대면 나오는 전구를 클릭해서 Implement Methods를 클릭한 뒤 3개의 메서드를 생성한다. (onCreateViewHolder, onBindViewHolder, getItemCount)
리싸이클러뷰에 보이는 여러 개의 아이템들은 내부에서 캐시되기 때문에 아이템 개수만큼 객체로 만들어지지는 않음. 메모리가 효율적으로 사용되기 위해 뷰홀더가 재사용된다.
onCreateViewHolder() : 뷰홀더가 새로 만들어지는 시점에 호출되는 메서드
onBindViewHolder() : 뷰홀더가 재사용될 때 호출되는 메서드
onCreateViewHolder() 메서드에는 뷰 타입을 위한 정수값이 파라미터로 전달되는데, 보통은 뷰 타입을 한 가지로 해서 많이 사용하지는 않는 파라미터라고 한다. (ex. 이미지를 보여주기 or 이미지+텍스트 같이 보여주기 등 선택할 수 있는 기능)
위 코드 작성 시 R.layout.person_item, R.id.textView, R.id.textView2이 빨간색으로 변할 것이다. 이제 레이아웃을 만들어주자.
⭐4. 새 레이아웃 XML 파일을 생성한다. (person_item.xml)⭐
이런 느낌으로 만들어준다.
여기서 했던 방식대로 카드뷰를 만들면 된다.
(잘못 만든 거)
문제 : 처음에 했을 때 1,2번 사진같이 카드뷰끼리의 간격이 매우 떨어져있었다.
해결 : 가장 바깥쪽의 리니어 레이아웃의 폭과 높이를 wrap_content로 설정해줬더니 해결.
또한 수정 후에는 3번 사진처럼 간격이 조금 넓었는데, cardElevation을 5dp로 줄이니까 해결되었다.
코드
<?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="wrap_content"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:layout_width="409dp"
android:layout_height="wrap_content"
app:cardCornerRadius="10dp"
app:cardElevation="5dp"
app:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/imageView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
app:srcCompat="@mipmap/ic_launcher" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="5dp"
android:orientation="vertical">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="이름"
android:textSize="30sp" />
<TextView
android:id="@+id/textView2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="전화번호"
android:textSize="20sp" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
⭐5. MainActivity.java⭐
package com.example.chapter7_4;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView recyclerView = findViewById(R.id.RecyclerView);
LinearLayoutManager layoutManager =
new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(layoutManager);
PersonAdapter adapter = new PersonAdapter();
adapter.addItem(new Person("강예지", "010-5555-5555"));
adapter.addItem(new Person("강동원", "010-3737-3737"));
adapter.addItem(new Person("강하늘", "010-1000-1000"));
recyclerView.setAdapter(adapter);
}
}
리싸이클러뷰에는 레이아웃 매니저를 설정할 수 있다.
레이아웃 매니저 : 리싸이클러뷰가 보일 기본적인 형태를 설정 (ex. 세로 방향, 가로 방향, 격자 모양)
- 세로 방향 설정 : LinearLayoutManager 객체 사용, 방향 VERTICAL로 설정
- 가로 방향 설정 : LinearLayoutManager 객체 사용, 방향 HORIZONTAL로 설정
- 격자 모양 설정 : GridLayoutManager 객체 사용, 칼럼 수 지정
⭐6. 실행결과⭐
만들고 나서도 이상한 걸 눈치 못채다가 아래 예제를 하다가 이상한 것을 눈치챘다.
전화번호랑 이름의 위치가 바뀌었다. Person 클래스에서 파라미터 두개의 위치를 바꿔주면 해결된다.
public Person(String mobile, String name) - > public Person(String name, String mobile)
<리싸이클러뷰 - 격자 모양 만들기>
⭐1. 위에서 만든 프로젝트를 복사한다.⭐
C > 사용자 > amye > AndroidStudioProject 폴더에 들어가면 프로젝트들이 있다. 위에서 만든 프로젝트 폴더를 복붙한다. (Chapter7_4를 복붙한 프로젝트 파일의 이름을 Chapter7_4_2로 설정했다.)
안드로이드 스튜디오에 들어가서 복붙한 프로젝트를 연다.
해야할 것 : 패키지명 바꾸기, 그래들 파일 수정하기, 빌드 파일 제거하기
- 패키지명 바꾸기
복붙하면 프로젝트명만 바뀌기 때문에, 안의 패키지명도 바꿔줘야한다.
2024.12.16 - [TIL/안드로이드 스튜디오] - 패키지명 변경하기
- 그래들 파일 수정하기
패키지명 바꿔준대로 그대로 똑같이 변경해주기.
- 빌드 파일 제거하기
복붙한 프로젝트 폴더 내의 app 폴더에서 build 폴더를 제거한다.
build 폴더 : 앱이 빌드되면서 만들어지는 폴더. (크기가 매우 크다.)
[ build 폴더를 삭제하는 것을 권장하는 경우 ]
- 새로운 프로젝트로 복사하거나
- 소스 코드의 변경이 일어나거나(간단한 변경일 경우 Sync Project로 해결)
- 프로젝트를 다른 PC에 옮기는 경우
⭐2. MainActivity 수정 : 격자 형태로 바꾸기⭐
LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
↓
GridLayoutManager layoutManager = new GridLayoutManager(this, 2);
두번째 파라미터는 칼럼의 개수를 의미한다.
<리싸이클러뷰(심화) - 클릭 이벤트 만들기>
위 격자모양 프로젝트에 이어서 만들 것이다.
각 아이템을 클릭할 시, 해당 아이템의 name 정보를 띄우는 토스트 메시지를 구현할 것이다.
클릭 이벤트는 리싸이클러뷰가 아닌 각 아이템에 발생하게 된다.
그러므로 뷰홀더 안에서 클릭 이벤트를 처리할 수 있도록 만드는 것이 좋다.
어댑터 객체 밖에서 리스너를 설정하고 설정된 리스너 쪽으로 이벤트를 전달받도록 한다. (리스너 안에서 토스트 메시지를 띄우게 되면 클릭했을 때의 기능이 변경될 때마다 어댑터를 수정해야하는 문제가 생기기 때문.)
(솔직히 뭐라는 건지 잘 모르겠다.)
⭐1. 인터페이스 생성⭐
New -> Java Class -> Interface 선택 (여기서 모르고 Class로 선택하는 바람에 한참 해맸다.)
package com.example.chapter7_4_2;
import android.view.View;
public interface OnPersonItemClickListener {
public void onItemClick(PersonAdapter.ViewHolder holder, View view, int position);
}
파라미터로 뷰홀더 객체, 뷰 객체, 뷰의 position 정보(아이템을 구분하는 인덱스 값)가 전달된다.
⭐2. PersonAdapter.java 수정⭐
전체코드
package com.example.chapter7_4_2;
public class PersonAdapter extends RecyclerView.Adapter<PersonAdapter.ViewHolder>
implements OnPersonItemClickListener {
ArrayList<Person> items = new ArrayList<Person>();
OnPersonItemClickListener listener;
// 뷰홀더 클래스
static class ViewHolder extends RecyclerView.ViewHolder {
TextView textView_name;
TextView textView_mobile;
public ViewHolder(@NonNull View itemView, final OnPersonItemClickListener listener) {
super(itemView);
textView_name = itemView.findViewById(R.id.textView);
textView_mobile = itemView.findViewById(R.id.textView2);
// 아이템뷰에 OnClickListener 설정하기
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int position = getAdapterPosition(); // 아이템이 어댑터에서 몇 번째인지 인덱스 정보를 반환
if(listener!=null) // 아이템뷰 클릭 시 미리 정의한 다른 리스너의 메서드 호출하기
listener.onItemClick(ViewHolder.this, view, position);
}
});
}
public void setItem(Person item){
textView_name.setText(item.getName());
textView_mobile.setText(item.getMobile());
}
}
@NonNull
@Override
// 자동호출 (뷰홀더 객체가 만들어질 때)
public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
// 각 아이템을 위해 정의한 XML 레이아웃을 이용해 뷰 객체를 만들어준다.
LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
View itemView = inflater.inflate(R.layout.person_item, viewGroup, false);
return new ViewHolder(itemView, this); // 뷰 객체를 뷰홀더 객체에 담아 반환한다.
}
@Override
// 자동호출 (뷰홀더 객체가 재사용될 때)
public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
// 기존의 뷰 객체를 사용하고 데이터만 바꿔준다.
Person item = items.get(position);
viewHolder.setItem(item);
}
@Override
public int getItemCount() { // 아이템의 개수를 반환
return items.size();
}
public void addItem(Person item){
items.add(item);
}
public void setItems(ArrayList<Person> items){
this.items = items;
}
public Person getItem(int position){
return items.get(position);
}
public void setItem(int position, Person item){
items.set(position, item);
}
public void setOnItemClickListener(OnPersonItemClickListener listener){
this.listener = listener;
}
@Override
public void onItemClick(ViewHolder holder, View view, int position) {
if(listener!=null){
listener.onItemClick(holder, view, position);
}
}
}
[ 나눠서 분석 ]
- onPersonItemClickListener🌠
PersonAdapter 클래스가 새로 정의한 OnPersonItemClickListener 인터페이스를 구현하도록 한다.
public class PersonAdapter extends RecyclerView.Adapter<PersonAdapter.ViewHolder>
implements OnPersonItemClickListener {
listener라는 이름의 변수를 선언한다.
OnPersonItemClickListener listener;
public void setOnItemClickListener(OnPersonItemClickListener listener){
this.listener = listener;
}
setOnItemClickListener 메서드를 추가하여, 이 메서드가 호출될 시 리스너 객체를 위의 listener 변수에 할당한다.
(이렇게 하면 onItemClick 메서드가 호출되었을 때 다시 외부에서 설정된 메서드가 호출되도록 만들 수 있다.)
- setOnClickListener🌠
1. ViewHolder의 파라미터 값을 하나 더 추가해준다. (final OnPersonItemClickListener listener)
public ViewHolder(@NonNull View itemView) {
↓
public ViewHolder(@NonNull View itemView, final OnPersonItemClickListener listener) {
뷰홀더 내부에 클릭 이벤트를 설정하기 위해 리스너 객체를 파라미터로 전달했다.
public ViewHolder(@NonNull View itemView, final OnPersonItemClickListener listener) {
super(itemView);
textView_name = itemView.findViewById(R.id.textView);
textView_mobile = itemView.findViewById(R.id.textView2);
// 아이템뷰에 OnClickListener 설정하기
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int position = getAdapterPosition(); // 아이템의 인덱스 정보를 반환
if(listener!=null) // 아이템뷰 클릭 시 미리 정의한 다른 리스너의 메서드 호출하기
listener.onItemClick(ViewHolder.this, view, position);
}
});
}
2. 뷰가 클릭되면 listener 객체의 onItemClick 이벤트가 호출된다.
@Override
public void onItemClick(ViewHolder holder, View view, int position) {
if(listener!=null){
listener.onItemClick(holder, view, position);
}
}
외부에 설정된 메서드가 호출됨. (이 부분이 좀 이해가 안감)
3. ViewHolder의 파라미터가 2개로 바뀌었으므로
onCreateViewHolder 메서드 내부의 return 부분 코드를 변경한다.
return new ViewHolder(itemView); // 뷰 객체를 뷰홀더 객체에 담아 반환한다.
↓
return new ViewHolder(itemView, this); // 뷰 객체를 뷰홀더 객체에 담아 반환한다.
⭐3. MainActivity.java 수정⭐
onCreate 내부에 아래 코드를 추가한다.
adapter.setOnItemClickListener(new OnPersonItemClickListener(){
// 어댑터에 리스너 설정하기
@Override
public void onItemClick(PersonAdapter.ViewHolder holder, View view, int position) {
// 아이템 클릭 시 어댑터에서 해당 아이템의 Person 객체 가져오기
Person item = adapter.getItem(position);
Toast.makeText(getApplicationContext(), "아이템 선택됨 : "+item.getName(),
Toast.LENGTH_LONG).show();
}
});
⭐4. person_item.xml 수정⭐
아이템들이 내부 뷰에 맞춰서 wrap_content로 나오게 해야 안 짤리고 잘 나온다.
딱히 어려운 건 아니라서 이리저리 하다보면 잘 만들어짐. (그래도 혹시 모르니 코드도 같이 올린다.)
코드
<?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="wrap_content"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="10dp"
app:cardElevation="5dp"
app:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/imageView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
app:srcCompat="@mipmap/ic_launcher" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="5dp"
android:layout_marginRight="10dp"
android:orientation="vertical">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="이름"
android:textSize="34sp" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="전화번호"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
⭐5. 실행 결과⭐
선택하는 아이템의 이름이 토스트 메시지로 나타나는 것을 확인할 수 있다.
어렵다.. 한달 정도 전에 책을 전부 읽진 말고 필요한 부분만 보자는 마인드로, 그때 리싸이클러뷰를 공부하려고 책을 펼쳤는데 너무 어려워서 결국 처음부터 다시 시작했던 기억이 난다..
근데 여전히 어렵다. 걍 여기가 어려운 파트인 것 같다..
'TIL > 안드로이드 스튜디오' 카테고리의 다른 글
도전!13 - 리싸이클러뷰에 고객 정보 추가하기 (0) | 2025.01.15 |
---|---|
7장 - 스피너🔻 (0) | 2025.01.14 |
7장 - 나인패치, 카드뷰, 새로운 뷰 만들기 (0) | 2025.01.11 |
도전!12 - 서비스에서 수신자로 메시지 보내기 (0) | 2025.01.10 |
도전!11 - 서비스 실행하고 화면에 보여주기 (0) | 2025.01.10 |