3. 아이템 상세 화면 ItemActivity
리사이클러뷰에서 제공하는 아이템뷰는 보통 사이즈를 작게 만들기에 주요정보만 제공하는 편입니다. 그렇기에 아이템뷰를 클릭했을 때 아이템의 상세정보를 볼 수 있는 별도의 상세화면을 사용합니다.
이번에는 아이템의 정보를 보다 상세하게 볼 수 있는 상세화면 액티비티를 만들어 보겠습니다. 액티비티의 이름은 ItemActivity 라고 명명하겠습니다.
이번에는 EdgeToEdge UI 를 그대로 두고 구현하겠습니다.
상세화면 구현 4단계
1) MainActivity 에서 아이템뷰 클릭했을 때 ItemActivity 로 전환하는 코드 작성 ( 아이템 정보 전달 및 확인 )
2) 상세화면 레이아웃 xml 파일 작성
3) ItemActivity 에서 아이템 정보를 보여주기
4) 액티비티 전환 효과 구현
1) MainActivity 에서 아이템뷰를 클릭했을 때 상세화면(ItemActivity)로 전환하기
상세화면으로 전환되는 이벤트는 아이템뷰를 클릭했을 때 입니다. 하여 아이템뷰에 클릭이벤트를 처리하는 리스너를 설정하도록 하겠습니다. 아이템뷰는 보이기에는 MainActivity 화면에 있지만 실제 만들어지는 위치는 MyAdapter 클래스 내부 입니다. 그래서 아이템뷰의 클릭이벤트 처리 역시 MyAdapter 클래스 안에서 설정해야 합니다.
아이템뷰의 클릭이벤트 처리는 ViewHolder에서 하거나 onBindViewHolder()에서 처리가 가능합니다. 보통 아이템 데이터를 참조하고 있는 onBindViewHolder() 메소드 안에서 처리하는 경우가 더 많습니다. 클릭이벤트는 람다표현보다 간결한 SAM 변환으로 처리를 해보겠습니다. 또한 ItemActivity 상세화면에서 Item 데이터를 보여줘야 하기에 Intent 객체에게 Extra 데이터로 전달하도록 하겠습니다.
//주 생성자 - 클래스명 옆에 constructor 키워드로 추가. 파라미터에 var or val 키워드를 추가하면 멤버변수 면서 매개변수가 됨.
//## RecyclerView.Adapter 상속 ##
class MyAdapter constructor(val context: Context, val items:MutableList<Item>) : RecyclerView.Adapter<MyAdapter.VH>(){
.........
//3] 아이템뷰에 Item 데이터 연결해주는 기능 : 현재 만들어야 할 position 번째 Item의 값을 뷰홀더의 멤버인 자식뷰들에 설정
override fun onBindViewHolder(holder: VH, position: Int) {
val item= items.get(position) //현재 아이템뷰에 보여줄 Item 데이터 얻어오기
holder.tvTitle.setText(item.title) // 현재 아이템뷰의 제목을 보여주는 텍스트뷰에 [제목] 설정
holder.tvMsg.text= item.msg // 현재 아이템뷰의 메세지를 보여주는 텍스트뷰에 [메세지] 설정
//holder.iv.setImageResource(item.img) // 현재 아이템뷰의 이미지를 보여주는 이미지뷰에 [도시 이미지] 설정
// 이미지 로딩 라이브러리 Glide 로 이미지 설정
Glide.with(context).load(item.img).into(holder.iv)
//---------------------------------------------------------
//SAM conversion 이용하여 이벤트처리
holder.itemView.setOnClickListener{ v ->
// ItemActivity 를 실행하기 위한 Intent 객체 생성
val intent= Intent(context, ItemActivity::class.java)
// 액티비티 전환할 때 Item 데이터 전달을 위해 Intent 에 Extra 데이터 추가
intent.putExtra("title", item.title)
intent.putExtra("msg", item.msg)
intent.putExtra("img", item.img)
// 위 인텐트 객체를 이용하여 ItemActivity 실행
context.startActivity(intent)
}
}//onBindViewHolder method....
}//MyAdapter class..
상세화면 ItemActivity 에 전달된 Item 데이터(제목, 메세지, 이미지)를 받아 변수에 저장해 놓겠습니다.
class ItemActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_item)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
//넘어온 Intent 객체가 이미 이 액티비티의 property로 존재함
val title= intent.getStringExtra("title")
val msg= intent.getStringExtra("msg")
val img= intent.getIntExtra("img", R.drawable.ic_launcher_foreground) //전달받은 이미지가 없다면 기본 앱아이콘 이미지 보여주기
}//onCreate method..
}//ItemActivity class....
받아온 Item 데이터를 보여주는 뷰들이 필요하겠네요. 글씨 2개(제목, 메세지)와 이미지 1개(도시 이미지)를 보여주는 레이아웃 파일을 만들어 보겠습니다.
2) 상세화면 레아이웃 xml 파일 만들기
Item 데이터(제목, 메세지, 도시 이미지)를 보여주는기 위해 뷰들을 배치하겠습니다. 툴바에는 제목, 이미지뷰에는 도시 이미지를, 메세지는 이미지뷰 아래 텍스트 뷰를 통해 보여주도록 하겠습니다.
<?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=".ItemActivity">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/design_default_color_primary"
app:title="아이템 제목"
app:titleTextColor="@color/white"
app:navigationIcon="@drawable/baseline_arrow_back_24"/>
<ImageView
android:id="@+id/iv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:contentDescription="city image"/>
<TextView
android:id="@+id/tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/black"
android:padding="8dp"
android:text="This is message."/>
</LinearLayout>
3) 상세화면(ItemActivity)에서 아이템 정보 보여주기
레이아웃 xml 파일에서 만든 뷰 3개(툴바, 이미지뷰, 텍스트뷰)에 대한 참조변수를 먼저 만들어줍니다. 코틀린은 프로퍼티(멤버변수)를 만들 때 반드시 초기화를 해야 하기에 by lazy 를 이용하여 늦은 초기화로 수행했습니다. 또한 상세화면에서 메인화면으로 다시 돌아가기 위해 제목줄(툴바)에 네비게이션 아이콘 버튼을 통해 뒤로 돌아가도록 하였습니다.
import android.os.Bundle
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.bumptech.glide.Glide
import com.google.android.material.appbar.MaterialToolbar
class ItemActivity : AppCompatActivity() {
// 뷰들의 참조변수 3개 준비 - 참조변수의 참조값을 변경할 상황이 거의 없기에 val 키워드로 선언.
// 초기값을 나중에 수행되도록 늦은 초기화 기법으로 by lazy 사용
val toolbar: MaterialToolbar by lazy { findViewById(R.id.toolbar) }
val iv: ImageView by lazy { findViewById(R.id.iv) }
//tv변수를 만들때 명시적으로 자료형이 주어지지 않으면 대입되는 자료형의 값에 따라 추론처리되는데 findViewById()는 어떤 뷰 객체인지 추론이 불가하기에 <>제네릭으로 타입을 지정하면서 대입해줘야 함
val tv by lazy { findViewById<TextView>(R.id.tv) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_item)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
//넘어온 Intent 객체가 이미 이 액티비티의 property로 존재함
val title= intent.getStringExtra("title")
val msg= intent.getStringExtra("msg")
val img= intent.getIntExtra("img", R.drawable.ic_launcher_foreground) //전달받은 이미지가 없다면 기본 앱아이콘 이미지 보여주기
// ---- Item 데이터(제목, 메세지, 도시 이미지)를 뷰들에 설정하기 -------------------
// 툴바에 제목 title 표시
toolbar.title= title
// 텍스트뷰에 메세지 msg 표시
tv.text= msg
// 이미지뷰에 도시 이미지 img 표시 - 이미지 로드 라이브러리 Glide 사용
Glide.with(this).load(img).into(iv)
// ----------------------------------------------------------------------
// 툴바(제목줄)에 뒤로가기 아이콘을 클릭했을 때 이전 화면(MainActivity)로 돌아가기. [ 뒤로가기 디스패처에게 뒤로가기 동작 실행 요청 ]
toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
}//onCreate method..
}//ItemActivity class....
여기까지만 제작해도 충분히 상세화면을 보여주는 목적은 달성한 코드입니다. 다만, 사용자에게 앱의 사용에 대한 몰입감을 주는 UX를 구현하기 위해 액티비티를 전환할 때 전환효과를 주도록 하겠습니다.
4) 액티비티 전환할 때 전환 효과 주기
전환효과는 메인화면의 작은 아이템뷰에 있던 도시 이미지가 확대되면서 상세화면의 큰 이미지뷰로 연결되는 듯한 효과를 줄 겁니다.
전환효과를 적용하려면 이전화면의 특정 뷰와 다음 화면의 특정 뷰에 같은 이름의 '별명'을 지정해주고 '장면전환 SceneTransition'객체를 만들어 액티비티를 전환할 때 옵션으로 지정하면 적용됩니다.
4.1 먼저, 아이템뷰가 클릭되었을 때 화면이 전환되므로 아이템뷰가 있는 MyAdapter 클래스 안에서 기존 액티비티 전환코드를 수정하겠습니다. 장면전환 효과 SceneTransitionAnimation 객체를 만들면서 아이템뷰에 있는 도시 사진을 보여주는 이미지뷰에 '별명'을 지정하고 액티비티 옵션객체로 생성하여 인텐트로 ItemActivity를 실행시키는 startActivity()에 두번째 파라미터로 전달하면 전환효과를 적용할 수 있습니다. 주석으로 ~~ 표시 다음에 있는 코드가 새로 추가한 전환효과 코드 입니다.
별명은 "img" 라는 이름으로 명명하였습니다.
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.app.ActivityOptionsCompat
import androidx.core.util.Pair
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
//주 생성자 - 클래스명 옆에 constructor 키워드로 추가. 파라미터에 var or val 키워드를 추가하면 멤버변수 면서 매개변수가 됨.
//## RecyclerView.Adapter 상속 ##
class MyAdapter constructor(val context: Context, val items:MutableList<Item>) : RecyclerView.Adapter<MyAdapter.VH>(){
.........
//3] 아이템뷰에 Item 데이터 연결해주는 기능 : 현재 만들어야 할 position 번째 Item의 값을 뷰홀더의 멤버인 자식뷰들에 설정
override fun onBindViewHolder(holder: VH, position: Int) {
val item= items.get(position) //현재 아이템뷰에 보여줄 Item 데이터 얻어오기
holder.tvTitle.setText(item.title) // 현재 아이템뷰의 제목을 보여주는 텍스트뷰에 [제목] 설정
holder.tvMsg.text= item.msg // 현재 아이템뷰의 메세지를 보여주는 텍스트뷰에 [메세지] 설정
//holder.iv.setImageResource(item.img) // 현재 아이템뷰의 이미지를 보여주는 이미지뷰에 [도시 이미지] 설정
// 이미지 로딩 라이브러리 Glide 로 이미지 설정
Glide.with(context).load(item.img).into(holder.iv)
//---------------------------------------------------------
//SAM conversion 이용하여 이벤트처리
holder.itemView.setOnClickListener{ v ->
// ItemActivity 를 실행하기 위한 Intent 객체 생성
val intent= Intent(context, ItemActivity::class.java)
// 액티비티 전환할 때 Item 데이터 전달을 위해 Intent 에 Extra 데이터 추가
intent.putExtra("title", item.title)
intent.putExtra("msg", item.msg)
intent.putExtra("img", item.img)
// 위 인텐트 객체를 이용하여 ItemActivity 실행
//context.startActivity(intent)
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//액티비티의 전환시에 뷰들에 연결 효과주기.. ActivityOptions - SceneTransition [ Pair()객체 - 전화효과를 줄 뷰에게 별칭을 연결.. 실행될 액티비티에서 연결된 뷰도 같은 별칭을 주면 자동으로 전환효과 보여짐 ] - ex. 당근마켓앱에 구현되어 있음.
val optionsCompat:ActivityOptionsCompat= ActivityOptionsCompat.makeSceneTransitionAnimation(context as MainActivity, Pair(holder.iv, "img")) //context를 Activity로 형변환 - 코틀린에서 클래스들의 형변환 연산자 as
context.startActivity(intent, optionsCompat.toBundle())
}
}//onBindViewHolder method....
}//MyAdapter class..
4.2 이제 새로 실행될 상세화면 ItemActivity 에서 전환효과로 연결될 아이템의 도시 사진을 보여주는 큰 이미지뷰에 이전에 지정했던 것과 같은 '별명'을 지정해주면 시스템이 같은 '별명 img' 를 가진 뷰와 이전 화면의 뷰 사이에 전화효과 애니메이션을 자동으로 구현시켜줍니다. 역시 주석 ~~ 표시 다음에 별명을 지정하는 한줄코드를 추가하겠습니다.
class ItemActivity : AppCompatActivity() {
// 뷰들의 참조변수 3개 준비 - 참조변수의 참조값을 변경할 상황이 거의 없기에 val 키워드로 선언.
// 초기값을 나중에 수행되도록 늦은 초기화 기법으로 by lazy 사용
val toolbar: MaterialToolbar by lazy { findViewById(R.id.toolbar) }
val iv: ImageView by lazy { findViewById(R.id.iv) }
//tv변수를 만들때 명시적으로 자료형이 주어지지 않으면 대입되는 자료형의 값에 따라 추론처리되는데 findViewById()는 어떤 뷰 객체인지 추론이 불가하기에 <>제네릭으로 타입을 지정하면서 대입해줘야 함
val tv by lazy { findViewById<TextView>(R.id.tv) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_item)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
//넘어온 Intent 객체가 이미 이 액티비티의 property로 존재함
val title= intent.getStringExtra("title")
val msg= intent.getStringExtra("msg")
val img= intent.getIntExtra("img", R.drawable.ic_launcher_foreground) //전달받은 이미지가 없다면 기본 앱아이콘 이미지 보여주기
// ---- Item 데이터(제목, 메세지, 도시 이미지)를 뷰들에 설정하기 -------------------
// 툴바에 제목 title 표시
toolbar.title= title
// 텍스트뷰에 메세지 msg 표시
tv.text= msg
// 이미지뷰에 도시 이미지 img 표시 - 이미지 로드 라이브러리 Glide 사용
Glide.with(this).load(img).into(iv)
// ----------------------------------------------------------------------
// 툴바(제목줄)에 뒤로가기 아이콘을 클릭했을 때 이전 화면(MainActivity)로 돌아가기. [ 뒤로가기 디스패처에게 뒤로가기 동작 실행 요청 ]
toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//iv 이미지뷰에 전환효과 주기위에 별칭 부여 [ 이전 액티비티에서 ActivityOptions 에서 Pair()로 설정한 별칭과 같은 명칭
iv.transitionName = "img"
}//onCreate method..
}//ItemActivity class....
네. iv.transitionName="img" 이 한줄만으로 자동으로 전환효과가 적용됩니다. 참 쉽네요.
[ 전환효과 적용 전과 적용 후 ]
화면이 전활될 때 이미지가 자연스럽게 연결되는 느낌을 주어 사용자의 몰입감을 유지시켜주는 사용자경험을 제공합니다.
여기까지 해서 앱을 구현할 때 가장 많이 사용하는 액티비티 전환과 리사이클러뷰를 코틀린으로 만들어 보았습니다.
앞으로의 앱 개발에 대한 소개는 코틀린언어를 통해 소개하도록 하겠습니다. 모든 코틀린 문법을 소개한 것은 아니기에 앞으로 앱개발에 대한 기술을 소개하면서 조금씩 코틀린언어에 대해 익숙해지면서 못다한 내용도 소개하도록 하겠습니다.