250x250
Notice
Recent Posts
Recent Comments
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
Tags
- 안드로이드 sunflower
- 막내의막무가내 플러터
- 2022년 6월 일상
- 주택가 잠실새내
- 막내의막무가내 SQL
- 프래그먼트
- 막내의막무가내 코볼 COBOL
- 막내의막무가내 일상
- 주엽역 생활맥주
- 막내의 막무가내
- 막내의막무가내 프로그래밍
- 막내의막무가내 알고리즘
- 막내의막무가내
- 막내의막무가내 안드로이드 코틀린
- 안드로이드 Sunflower 스터디
- 막내의 막무가내 알고리즘
- 프로그래머스 알고리즘
- 막내의막무가내 안드로이드 에러 해결
- flutter network call
- 막내의막무가내 안드로이드
- 막내의막무가내 플러터 flutter
- 막내의막무가내 rxjava
- 안드로이드
- 막무가내
- 부스트코스
- Fragment
- 막내의막무가내 코틀린
- 부스트코스에이스
- 막내의막무가내 코틀린 안드로이드
- 막내의막무가내 목표 및 회고
Archives
- Today
- Total
막내의 막무가내 프로그래밍 & 일상
[안드로이드] 코틀린 리사이클러뷰 무한 스크롤 구현 본문
728x90
[2021-04-13 업데이트]
영화 불러오는데 무한 스크롤이 필요하여 해당 기능을 구현해봤습니다.
1. 무한스크롤 리스너 클래스 추가
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
abstract class EndlessRecyclerViewScrollListener : RecyclerView.OnScrollListener {
// The minimum amount of items to have below your current scroll position
// before loading more.
private var visibleThreshold = 5
// The current offset index of data you have loaded
private var currentPage = 0
// The total number of items in the dataset after the last load
private var previousTotalItemCount = 0
// True if we are still waiting for the last set of data to load.
private var loading = true
// Sets the starting page index
private val startingPageIndex = 0
var mLayoutManager: RecyclerView.LayoutManager
constructor(layoutManager: LinearLayoutManager) {
mLayoutManager = layoutManager
}
constructor(layoutManager: GridLayoutManager) {
mLayoutManager = layoutManager
visibleThreshold *= layoutManager.spanCount
}
constructor(layoutManager: StaggeredGridLayoutManager) {
mLayoutManager = layoutManager
visibleThreshold *= layoutManager.spanCount
}
private fun getLastVisibleItem(lastVisibleItemPositions: IntArray): Int {
var maxSize = 0
for (i in lastVisibleItemPositions.indices) {
if (i == 0) {
maxSize = lastVisibleItemPositions[i]
} else if (lastVisibleItemPositions[i] > maxSize) {
maxSize = lastVisibleItemPositions[i]
}
}
return maxSize
}
// This happens many times a second during a scroll, so be wary of the code you place here.
// We are given a few useful parameters to help us work out if we need to load some more data,
// but first we check if we are waiting for the previous load to finish.
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
var lastVisibleItemPosition = 0
val totalItemCount = mLayoutManager.itemCount
if (mLayoutManager is StaggeredGridLayoutManager) {
val lastVisibleItemPositions =
(mLayoutManager as StaggeredGridLayoutManager).findLastVisibleItemPositions(null)
// get maximum element within the list
lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions)
} else if (mLayoutManager is GridLayoutManager) {
lastVisibleItemPosition =
(mLayoutManager as GridLayoutManager).findLastVisibleItemPosition()
} else if (mLayoutManager is LinearLayoutManager) {
lastVisibleItemPosition =
(mLayoutManager as LinearLayoutManager).findLastVisibleItemPosition()
}
// If the total item count is zero and the previous isn't, assume the
// list is invalidated and should be reset back to initial state
if (totalItemCount < previousTotalItemCount) {
currentPage = startingPageIndex
previousTotalItemCount = totalItemCount
if (totalItemCount == 0) {
loading = true
}
}
// If it’s still loading, we check to see if the dataset count has
// changed, if so we conclude it has finished loading and update the current page
// number and total item count.
if (loading && totalItemCount > previousTotalItemCount) {
loading = false
previousTotalItemCount = totalItemCount
}
// If it isn’t currently loading, we check to see if we have breached
// the visibleThreshold and need to reload more data.
// If we do need to reload some more data, we execute onLoadMore to fetch the data.
// threshold should reflect how many total columns there are too
if (!loading && lastVisibleItemPosition + visibleThreshold > totalItemCount) {
currentPage++
onLoadMore(currentPage, totalItemCount, view)
loading = true
}
}
// Call this method whenever performing new searches
fun resetState() {
currentPage = startingPageIndex
previousTotalItemCount = 0
loading = true
}
// Defines the process for actually loading more data based on page
abstract fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?)
}
2. 어댑터(어댑터에는 따로 해줄건 없습니다.)
package com.mtjin.androidarchitecturestudy.ui
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.RatingBar
import android.widget.TextView
import androidx.core.text.HtmlCompat
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.mtjin.androidarchitecturestudy.R
import com.mtjin.androidarchitecturestudy.data.Movie
class MovieAdapter :
RecyclerView.Adapter<MovieAdapter.ViewHolder>() {
private lateinit var clickCallBack: (Movie) -> Unit
private val items: ArrayList<Movie> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view: View = LayoutInflater.from(parent.context)
.inflate(R.layout.item_movie, parent, false)
val viewHolder = ViewHolder(view)
view.setOnClickListener {
clickCallBack(items[viewHolder.adapterPosition])
}
return viewHolder
}
override fun getItemCount(): Int = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
items[position].let {
holder.bind(it)
}
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val ivPoster = itemView.findViewById<ImageView>(R.id.iv_poster)
private val rvRating = itemView.findViewById<RatingBar>(R.id.rb_rating)
private val tvTitle = itemView.findViewById<TextView>(R.id.tv_title)
private val tvReleaseDate = itemView.findViewById<TextView>(R.id.tv_release_date)
private val tvActor = itemView.findViewById<TextView>(R.id.tv_actor)
private val tvDirector = itemView.findViewById<TextView>(R.id.tv_director)
fun bind(movie: Movie) {
with(movie) {
Glide.with(itemView).load(image)
.placeholder(R.drawable.ic_default)
.into(ivPoster!!)
rvRating.rating = (userRating.toFloatOrNull() ?: 0f) / 2
tvTitle.text = HtmlCompat.fromHtml(title, HtmlCompat.FROM_HTML_MODE_COMPACT)
tvReleaseDate.text = pubDate
tvActor.text = actor
tvDirector.text = director
}
}
}
fun setItems(items: List<Movie>) {
this.items.addAll(items)
notifyDataSetChanged()
}
fun setItemClickListener(clickCallBack: (Movie) -> Unit) {
this.clickCallBack = clickCallBack
}
fun clear() {
this.items.clear()
notifyDataSetChanged()
}
}
3. 액티비티
저는 서버에 요청할 때 page가 아닌 아이템의 개수가 필요해서 totalItemsCount를 사용했습니다. 스크롤 조건이 된 경우 requestPagingMovie를 호출해줍니다.
private fun initAdapter() {
movieAdapter = MovieAdapter()
val linearLayoutManager = LinearLayoutManager(this)
rvMovies.layoutManager = linearLayoutManager
scrollListener = object : EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
requestPagingMovie(query, totalItemsCount + 1)
}
}
rvMovies.addOnScrollListener(scrollListener)
rvMovies.adapter = movieAdapter
}
만약 새로 영화 검색을 해야하는 등 무한스크롤을 초기화 시켜야하는 경우는
scrooListener.resetState()를 해주면 됩니다.
private fun requestMovie(query: String) {
showLoading()
scrollListener.resetState()
myApplication.movieRepository.getSearchMovies(query,
success = {
if (it.isEmpty()) {
onToastMessage("해당 영화는 존재하지 않습니다.")
} else {
movieAdapter.clear()
movieAdapter.setItems(it)
onToastMessage("영화를 불러왔습니다.")
}
hideLoading()
},
fail = {
Log.d(TAG, it.toString())
when (it) {
is HttpException -> onToastMessage("네트워크에 문제가 있습니다.")
else -> onToastMessage(it.message.toString())
}
hideLoading()
})
}
영화를 더 불러오는 함수입니다.
fun requestPagingMovie(query: String, offset: Int) {
showLoading()
myApplication.movieRepository.getPagingMovies(query, offset,
success = {
if (it.isEmpty()) {
onToastMessage("마지막 페이지")
} else {
movieAdapter.setItems(it)
onToastMessage("영화를 불러왔습니다.")
}
hideLoading()
},
fail = {
Log.d(TAG, it.toString())
when (it) {
is HttpException -> onToastMessage("네트워크에 문제가 있습니다.")
else -> onToastMessage(it.message.toString())
}
hideLoading()
})
}
전체 액티비티 코드입니다.
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.ProgressBar
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.mtjin.androidarchitecturestudy.R
import com.mtjin.androidarchitecturestudy.utils.MyApplication
import retrofit2.HttpException
class MovieSearchActivity : AppCompatActivity() {
private lateinit var etInput: EditText
private lateinit var btnSearch: Button
private lateinit var rvMovies: RecyclerView
private lateinit var pbLoading: ProgressBar
private lateinit var movieAdapter: MovieAdapter
private lateinit var myApplication: MyApplication
private lateinit var query: String
private lateinit var scrollListener: EndlessRecyclerViewScrollListener
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_movie_search)
initView()
initAdapter()
initListener()
}
private fun initView() {
myApplication = application as MyApplication
etInput = findViewById(R.id.et_input)
btnSearch = findViewById(R.id.btn_search)
rvMovies = findViewById(R.id.rv_movies)
pbLoading = findViewById(R.id.pb_loading)
}
private fun initAdapter() {
movieAdapter = MovieAdapter()
val linearLayoutManager = LinearLayoutManager(this)
rvMovies.layoutManager = linearLayoutManager
scrollListener = object : EndlessRecyclerViewScrollListener(linearLayoutManager) {
override fun onLoadMore(page: Int, totalItemsCount: Int, view: RecyclerView?) {
requestPagingMovie(query, totalItemsCount + 1)
}
}
rvMovies.addOnScrollListener(scrollListener)
rvMovies.adapter = movieAdapter
}
private fun initListener() {
//어댑터 아이템 클릭리스너
movieAdapter.setItemClickListener { movie ->
Intent(Intent.ACTION_VIEW, Uri.parse(movie.link)).takeIf {
it.resolveActivity(packageManager) != null
}?.run(this::startActivity)
}
//검색버튼
btnSearch.setOnClickListener {
query = etInput.text.toString().trim()
if (query.isEmpty()) {
onToastMessage("검색어를 입력해주세요.")
} else {
onToastMessage("잠시만 기다려주세요.")
requestMovie(query)
}
}
}
private fun requestMovie(query: String) {
showLoading()
scrollListener.resetState()
myApplication.movieRepository.getSearchMovies(query,
success = {
if (it.isEmpty()) {
onToastMessage("해당 영화는 존재하지 않습니다.")
} else {
movieAdapter.clear()
movieAdapter.setItems(it)
onToastMessage("영화를 불러왔습니다.")
}
hideLoading()
},
fail = {
Log.d(TAG, it.toString())
when (it) {
is HttpException -> onToastMessage("네트워크에 문제가 있습니다.")
else -> onToastMessage(it.message.toString())
}
hideLoading()
})
}
fun requestPagingMovie(query: String, offset: Int) {
showLoading()
myApplication.movieRepository.getPagingMovies(query, offset,
success = {
if (it.isEmpty()) {
onToastMessage("마지막 페이지")
} else {
movieAdapter.setItems(it)
onToastMessage("영화를 불러왔습니다.")
}
hideLoading()
},
fail = {
Log.d(TAG, it.toString())
when (it) {
is HttpException -> onToastMessage("네트워크에 문제가 있습니다.")
else -> onToastMessage(it.message.toString())
}
hideLoading()
})
}
private fun onToastMessage(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}
private fun showLoading() {
pbLoading.visibility = View.VISIBLE
}
private fun hideLoading() {
pbLoading.visibility = View.GONE
}
companion object {
const val TAG = "MovieSearchActivity"
}
}
참고 및 출처 :
https://github.com/codepath/android_guides/wiki/Endless-Scrolling-with-AdapterViews-and-RecyclerView
댓글과 공감은 큰 힘이 됩니다. 감사합니다. !!
728x90
'안드로이드 > 코틀린 & 아키텍처 & Recent' 카테고리의 다른 글
Comments