ViewPager2 와 RecyclerView 사용하기

LeeEunJae·2022년 7월 29일
2

Study Kotlin

목록 보기
5/20
post-custom-banner

📌 실행 결과

Viewpager 와 RecyclerView 에 띄워줄 리스트는 Retrofit2 로 가상 API 서버에서 가져왔다. 방법은 아래 링크를 참고하면된다.
바로가기

먼저 홈 화면에서 미리보기로 호텔 정보를 쉽게 볼 수 있는 ViewPager 를 만들어 보자

📌item_house_detail_for_viewpager.xml

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>

📌 activity_main.xml

<?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)

Glide 는 안드로이드에서 이미지를 빠르고 효율적으로 불러올 수 있게 도와주는 라이브러리 이다. 깃허브 주소

implementation 'com.github.bumptech.glide:glide:4.12.0'

📌 HouseViewPagerAdapter.kt

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 를 적용시키기 위한 준비작업을 해보자

📌 item_house.xml

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>

📌 bottom_sheet.xml

<?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>

📌 HoseListAdpater.kt

위의 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
            }
        }
    }
}

📌 MainActivity.kt

어답터를 생성해서 연결 해주고 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
    }

}
profile
매일 조금씩이라도 성장하자
post-custom-banner

0개의 댓글