Viewpager 와 RecyclerView 에 띄워줄 리스트는 Retrofit2 로 가상 API 서버에서 가져왔다. 방법은 아래 링크를 참고하면된다.
바로가기
먼저 홈 화면에서 미리보기로 호텔 정보를 쉽게 볼 수 있는 ViewPager 를 만들어 보자
ViewPager 에 보여줄 리스트 item 레이아웃
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@color/white"
app:cardCornerRadius="16dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
tools:layout_height="100dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/thumbnailImageView"
android:layout_width="100dp"
android:layout_height="100dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView
android:id="@+id/titleTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="@color/black"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
tools:text="강남역 최저가!!"
android:maxLines="2"
app:layout_constraintStart_toEndOf="@id/thumbnailImageView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/priceTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
tools:text="23,000원"
android:maxLines="1"
android:textColor="@color/black"
android:textStyle="bold"
app:layout_constraintStart_toEndOf="@id/thumbnailImageView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/titleTextView"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
tools:context=".MainActivity">
<com.naver.maps.map.MapView
android:id="@+id/mapView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="80dp"/>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/houseViewPager"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_gravity="bottom"
android:layout_marginBottom="120dp"
android:orientation="horizontal"/>
<com.naver.maps.map.widget.LocationButtonView
android:id="@+id/currentLocationButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|start"
android:layout_margin="12dp"/>
<include layout="@layout/bottom_sheet"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
ViewPager2 를 추가해준다.
LocationButtonView 는 현재 위치를 찾는 버튼이 bottom_sheet 에 가려져서 위치를 좌상단으로 변경하기 위함이다.
Glide 는 안드로이드에서 이미지를 빠르고 효율적으로 불러올 수 있게 도와주는 라이브러리 이다. 깃허브 주소
implementation 'com.github.bumptech.glide:glide:4.12.0'
package com.dldmswo1209.airbnb
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
class HouseViewPagerAdapter: ListAdapter<HouseModel, HouseViewPagerAdapter.ItemViewHolder>(differ) {
inner class ItemViewHolder(val view: View): RecyclerView.ViewHolder(view){
fun bind(houseModel: HouseModel){
val titleTextView = view.findViewById<TextView>(R.id.titleTextView)
val priceTextView = view.findViewById<TextView>(R.id.priceTextView)
val thumbnailImageView = view.findViewById<ImageView>(R.id.thumbnailImageView)
titleTextView.text = houseModel.title
priceTextView.text = houseModel.price
Glide
.with(thumbnailImageView.context)
.load(houseModel.imgUrl)
.into(thumbnailImageView)
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): HouseViewPagerAdapter.ItemViewHolder {
val inflater = LayoutInflater.from(parent.context)
return ItemViewHolder(inflater.inflate(R.layout.item_house_detail_for_viewpager, parent, false))
}
override fun onBindViewHolder(holder: HouseViewPagerAdapter.ItemViewHolder, position: Int) {
holder.bind(currentList[position])
}
companion object{
val differ = object: DiffUtil.ItemCallback<HouseModel>(){
override fun areItemsTheSame(oldItem: HouseModel, newItem: HouseModel): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: HouseModel, newItem: HouseModel): Boolean {
return oldItem == newItem
}
}
}
}
다음은 bottom_sheet 에 RecyclerView 를 적용시키기 위한 준비작업을 해보자
RecyclerView 의 리스트 item 레이아웃
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
xmlns:tools="http://schemas.android.com/tools">
<ImageView
android:id="@+id/thumbnailImageView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_margin="24dp"
app:layout_constraintDimensionRatio="3:2"/>
<TextView
android:id="@+id/titleTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:textColor="@color/black"
android:textSize="20sp"
tools:text="강남역 최저가"
app:layout_constraintTop_toBottomOf="@id/thumbnailImageView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/priceTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/titleTextView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="24dp"
android:layout_marginTop="12dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
tools:text="24,000원"
android:textSize="20sp"
android:textColor="@color/black"
android:textStyle="bold" />
</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
app:behavior_peekHeight="100dp"
android:background="@drawable/top_radius_white_background"
xmlns:app="http://schemas.android.com/apk/res-auto">
<View
android:layout_width="30dp"
android:layout_height="3dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:background="#cccccc"
android:layout_marginTop="12dp"/>
<TextView
android:id="@+id/bottomSheetTitleTextView"
android:layout_width="0dp"
android:layout_height="100dp"
android:text="여러개의 숙소"
android:textSize="15dp"
android:textStyle="bold"
android:textColor="@color/black"
android:gravity="center"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<View
android:id="@+id/lineView"
android:layout_width="0dp"
android:layout_height="1dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/bottomSheetTitleTextView"
android:background="#cccccc"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/lineView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
위의 ViewPager 어답터와 거의 동일 하다.
Glide 의 transform 에 속성값을 줘서 이미지를 좀 더 예쁘게(?) 만들어줬다.
CenterCrop() : 가운데를 기준으로 이미지를 확대
RoundedCorners() : 이미지 모서리를 둥글게 만듬
RoundedCorners(12) 이렇게하면 dp가 아닌 px로 인식하기 때문에 스마트폰의 해상도에 따라 다르게 보일 수 있다.
그렇기 때문에 dp 값을 px 로 변환해주는 함수가 필요하다.
필자는 dpToPx() 라는 함수를 만들어 사용했다.
package com.dldmswo1209.airbnb
import android.content.Context
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
class HouseListAdapter: ListAdapter<HouseModel, HouseListAdapter.ItemViewHolder>(differ) {
inner class ItemViewHolder(val view: View): RecyclerView.ViewHolder(view){
fun bind(houseModel: HouseModel){
val titleTextView = view.findViewById<TextView>(R.id.titleTextView)
val priceTextView = view.findViewById<TextView>(R.id.priceTextView)
val thumbnailImageView = view.findViewById<ImageView>(R.id.thumbnailImageView)
titleTextView.text = houseModel.title
priceTextView.text = houseModel.price
Glide
.with(thumbnailImageView.context)
.load(houseModel.imgUrl)
.transform(CenterCrop(), RoundedCorners(dpToPx(thumbnailImageView.context, 12)))
.into(thumbnailImageView)
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): HouseListAdapter.ItemViewHolder {
val inflater = LayoutInflater.from(parent.context)
return ItemViewHolder(inflater.inflate(R.layout.item_house, parent, false))
}
override fun onBindViewHolder(holder: HouseListAdapter.ItemViewHolder, position: Int) {
holder.bind(currentList[position])
}
private fun dpToPx(context: Context, dp: Int) : Int{
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), context.resources.displayMetrics).toInt()
}
companion object{
val differ = object: DiffUtil.ItemCallback<HouseModel>(){
override fun areItemsTheSame(oldItem: HouseModel, newItem: HouseModel): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: HouseModel, newItem: HouseModel): Boolean {
return oldItem == newItem
}
}
}
}
어답터를 생성해서 연결 해주고 submitList 로 서버에서 가져온 dto.items 를 전달해주면 끝이다.
package com.dldmswo1209.airbnb
import android.graphics.Color
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.dldmswo1209.airbnb.databinding.ActivityMainBinding
import com.naver.maps.geometry.LatLng
import com.naver.maps.map.*
import com.naver.maps.map.overlay.Marker
import com.naver.maps.map.util.FusedLocationSource
import com.naver.maps.map.util.MarkerIcons
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class MainActivity : AppCompatActivity(), OnMapReadyCallback {
private lateinit var naverMap: NaverMap
private lateinit var binding: ActivityMainBinding
private lateinit var locationSource: FusedLocationSource
private val recyclerView: RecyclerView by lazy {
findViewById(R.id.recyclerView)
}
private val recyclerAdapter = HouseListAdapter()
private val viewPagerAdapter = HouseViewPagerAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.mapView.onCreate(savedInstanceState)
binding.mapView.getMapAsync(this)
binding.houseViewPager.adapter = viewPagerAdapter
recyclerView.adapter = recyclerAdapter
recyclerView.layoutManager = LinearLayoutManager(this)
}
override fun onMapReady(map: NaverMap) {
naverMap = map
// 확대/축소 범위 설정
naverMap.maxZoom = 18.0
naverMap.minZoom = 10.0
// 지도 초기 위치
val cameraUpdate = CameraUpdate.scrollTo(LatLng(37.497885,127.02751))
naverMap.moveCamera(cameraUpdate)
val uiSetting = naverMap.uiSettings
uiSetting.isLocationButtonEnabled = false
// 버튼의 위치를 임의로 설정하기 위함
binding.currentLocationButton.map = naverMap
// 위치를 반환하는 FusedLocationSource 생성
locationSource = FusedLocationSource(this@MainActivity, LOCATION_PERMISSION_REQUEST_CODE)
// 위치소스 지정
naverMap.locationSource = locationSource
getHouseListFromAPI()
}
private fun getHouseListFromAPI(){
val retrofit = Retrofit.Builder()
.baseUrl("https://run.mocky.io")
.addConverterFactory(GsonConverterFactory.create())
.build()
retrofit.create(HouseService::class.java).also {
it.getHouseList()
.enqueue(object: Callback<HouseDto>{
override fun onResponse(call: Call<HouseDto>, response: Response<HouseDto>) {
if(!response.isSuccessful){
// 실패 처리
return
}
response.body()?.let { dto ->
updateMarker(dto.items)
viewPagerAdapter.submitList(dto.items)
recyclerAdapter.submitList(dto.items)
}
}
override fun onFailure(call: Call<HouseDto>, t: Throwable) {
// 실패 처리
}
})
}
}
private fun updateMarker(houses: List<HouseModel>){
houses.forEach { house ->
val marker = Marker()
marker.position = LatLng(house.lat, house.lng)
// todo 마커 클릭 리스너
marker.map = naverMap
marker.tag = house.id
marker.icon = MarkerIcons.BLACK
marker.iconTintColor = Color.RED
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// requestCode 확인
if(requestCode != LOCATION_PERMISSION_REQUEST_CODE)
return
// 권한 팝업을 쉽게 구현하기 위해서 google 에서 제공하는 라이브러리를 사용
if(locationSource.onRequestPermissionsResult(requestCode,permissions,grantResults)){
if(!locationSource.isActivated){
naverMap.locationTrackingMode = LocationTrackingMode.None
}
return
}
}
override fun onStart() {
super.onStart()
binding.mapView.onStart()
}
override fun onResume() {
super.onResume()
binding.mapView.onResume()
}
override fun onPause() {
super.onPause()
binding.mapView.onPause()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
binding.mapView.onSaveInstanceState(outState)
}
override fun onStop() {
super.onStop()
binding.mapView.onStop()
}
override fun onDestroy() {
super.onDestroy()
binding.mapView.onDestroy()
}
override fun onLowMemory() {
super.onLowMemory()
binding.mapView.onLowMemory()
}
companion object{
private const val LOCATION_PERMISSION_REQUEST_CODE = 10000
}
}