안녕하세요. godsangin입니다.
혹시 페이스북, 인스타그램에서 게시물을 내리면 계속해서 새로운 게시물을 로딩하는 구성을 보신적이 있으신가요 ??
오늘은 그와 같이 특정 아이템으로 구성된 AdapterView를 endless하게 구현하는 방법에 대하여 알아보도록 하겠습니다.
서버에서 데이터 불러오기 예제를 참고하시면 도움이 될 것 같습니다.
https://in-idea.tistory.com/22
이번 예제에서는 위 게시물의 Skin이라는 객체가 아닌 Product라는 객체라는 점 유의하시길 바랍니다.
우선 RecyclerView에 대한 Adapter가 필요하겠죠 ??
package com.example.hwahae
import android.content.Context
import android.content.Intent
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.os.Handler
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
class ProductAdapter():RecyclerView.Adapter<RecyclerView.ViewHolder>(){
constructor(context:Context, products:List<Product>, adapterClickListener: AdapterClickListener, onLoadMoreListener: OnLoadMoreListener, gridLayoutManager: GridLayoutManager):this(){
this.context = context
this.products = ArrayList(products)
this.adapterClickListener = adapterClickListener
this.onLoadMoreListener = onLoadMoreListener
this.gridLayoutManager = gridLayoutManager
}
lateinit var context:Context
lateinit var products:ArrayList<Product?>
lateinit var adapterClickListener:AdapterClickListener
lateinit var onLoadMoreListener:OnLoadMoreListener
lateinit var gridLayoutManager: GridLayoutManager
var isModeLoading = false
var visibleThreshold = 1
var firstVisibleItem = 0
var visibleItemCount = 0
var totalItemCount = 0
var lastVisibleItem = 0
public interface OnLoadMoreListener{
fun onLoadMore()
}
public fun setRecyclerView(view:RecyclerView){
view.addOnScrollListener(object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
visibleItemCount = recyclerView.childCount
totalItemCount = gridLayoutManager.itemCount
firstVisibleItem = gridLayoutManager.findFirstVisibleItemPosition()
lastVisibleItem = gridLayoutManager.findLastVisibleItemPosition()
if(!isModeLoading && (totalItemCount - visibleItemCount) <= (firstVisibleItem + visibleThreshold)){
if(onLoadMoreListener != null){
onLoadMoreListener.onLoadMore()
}
isModeLoading = true
}
}
})
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
var view = LayoutInflater.from(context).inflate(R.layout.product_item, parent, false)
return Holder(view)
}
fun addItemMore(newOne:List<Product>){
products.addAll(newOne)
notifyItemRangeChanged(0, products.size)
}
fun setMoreLoading(isModeLoading:Boolean){
this.isModeLoading = isModeLoading
}
override fun getItemCount(): Int {
return products.size
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder as Holder).bind(products[position]!!, context)
}
inner class Holder(itemView: View?) : RecyclerView.ViewHolder(itemView!!) {
//coding your own view
val productTitle = itemView?.findViewById<TextView>(R.id.product_title)
val productPrice = itemView?.findViewById<TextView>(R.id.product_price)
val productImage = itemView?.findViewById<ImageView>(R.id.product_image)
fun bind(product: Product, context: Context) {
productTitle?.text = product.title
productPrice?.text = product.price + "원"
Glide.with(context).load(product.thumbnailImage)
.override(172, 172).centerCrop().into(productImage!!)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
val drawable = context.getDrawable(R.drawable.round_background_imageview) as GradientDrawable
productImage.background = drawable
productImage.clipToOutline = true
}
else {
//TODO("VERSION.SDK_INT < LOLLIPOP")
}
itemView.setOnClickListener{
//nextPage
adapterClickListener.setOnClickListener(product.id)
}
}
}
}
코드가 생각보다 기네요..! 뒤의 Holder부분은 본인이 필요한 방식데로 꾸며주시면 되고 Adapter에서 중요한 점은 setRecyclerView에서 recyclerview에 대한 scroll listener를 작성하는 부분과 interface인 onloadmore입니다.
recyclerview에서 보이는 아이템과 해당 아이템의 인덱스를 이용하여 리스트가 스크롤 될 때 마지막 리스트까지 불러왔는지 여부를 확인하고, Activity에서 실행될 listener의 onReadMore함수를 호출합니다.(현재 화면에서 리스트의 인덱스를 알기 위해서는 LayoutManager를 Adapter가 알고 있어야합니다. -> adapter생성 시 layoutManager를 매개변수로 두는 이유)
여기서 Activity에서 작성될 onLoadMore함수를 통하여 새로운 아이템들을 리스트에 추가하고 adapter에 notify해주면 무한스크롤 이벤트를 추가할 수 있습니다.(참 쉽죠....?)
다음으로 RecyclerView와 Adapter를 연결할 Activity입니다.
package com.example.hwahae
import android.content.Intent
import android.os.AsyncTask
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.os.Message
import android.util.JsonReader
import android.util.Log
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.InputStream
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
class MainActivity : AppCompatActivity(){
lateinit var myHeader:ConstraintLayout
lateinit var myRecyclerView:RecyclerView
lateinit var recyclerAdapter:ProductAdapter
var pageNum = 1
var products:List<Product>? = null
val REQUESTCODE_PRODUCT_DETAIL = 1000
val onLoadMoreListener = object:ProductAdapter.OnLoadMoreListener{
override fun onLoadMore() {
pageNum++
getProductsTask()
}
}
val adapterClickListener = object:AdapterClickListener{
override fun setOnClickListener(data: Int) {
val intent = Intent(applicationContext, ProductDetailActivity::class.java)
intent.putExtra("data", data)
startActivityForResult(intent, REQUESTCODE_PRODUCT_DETAIL)
overridePendingTransition(R.anim.anim_translate_up, R.anim.anim_translate_down)
}
}
val connectionListener = object: ConnectionListener{
override fun responseEnd() {//데이터를 전부 받아왔을 경우 실행됨
object: Thread(){
override fun run() {
super.run()
val msg = adapterFrontTask.obtainMessage()
adapterFrontTask.sendMessage(msg)
}
}.run()
}
}
val adapterFrontTask: Handler = object: Handler(){
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
if(pageNum == 1){
val gm = GridLayoutManager(applicationContext, 2)
recyclerAdapter =
ProductAdapter(applicationContext, products!!, adapterClickListener, onLoadMoreListener, gm)
myRecyclerView.layoutManager = gm
myRecyclerView.adapter = recyclerAdapter
recyclerAdapter.setRecyclerView(myRecyclerView)
recyclerAdapter.notifyDataSetChanged()
}
else{
recyclerAdapter.addItemMore(products!!)
recyclerAdapter.setMoreLoading(false)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
myHeader = findViewById(R.id.header)
myRecyclerView = findViewById(R.id.item_recyclerview)
setLayoutComponents()
}
fun setLayoutComponents(){
getProductsTask()
}
fun getProductsTask(){
AsyncTask.execute(object: Runnable {
override fun run() {
var inputStream: InputStream? = null
try{
val myEndpoint = URL("insert your url here/products?page=" + pageNum)
val myConnection = myEndpoint.openConnection() as HttpURLConnection
myConnection.setRequestProperty("User-Agent", "my-rest-app-v0.1")
myConnection.setRequestProperty("Content-Type", "application/json; charset=UTF-8")
myConnection.requestMethod = "GET"
myConnection.readTimeout = 20000
myConnection.doInput = true
myConnection.connect()
val response = myConnection.responseCode
Log.d("response==", response.toString())
if(response == 200){
inputStream = myConnection.inputStream
products = readJsonStream(inputStream)
if(products != null){
connectionListener.responseEnd()
}
}
}catch(e: Exception){
e.printStackTrace()
Log.e("response==", "Error:" + e.message)
}finally {
if(inputStream != null){
inputStream.close()
}
}
}
})
}
fun readJsonStream(inputStream: InputStream):List<Product>{
val reader = JsonReader(InputStreamReader(inputStream, "UTF-8"))
var result:List<Product> = ArrayList()
try{
reader.beginObject()
while(reader.hasNext()){
var name = reader.nextName()
if(name.equals("statusCode")){
reader.nextInt()
}
else if(name.equals("body")){
result = readMessagesArray(reader)
}else if(name.equals("scanned_count")){
reader.nextInt()
//마지막 처리
}else{
reader.skipValue()
}
}
reader.endObject()
}finally {
reader.close()
}
return result
}
fun readMessagesArray(reader: JsonReader):List<Product>{
val messages = ArrayList<Product>()
reader.beginArray()
while(reader.hasNext()){
messages.add(Product(reader))
}
reader.endArray()
return messages
}
}
oncreate에서 최초에 getProductsTask가 한번 실행되어 Adapter와 RecyclerView를 초기화하고 그 뒤로 Adapter의 설정을 바꿔 줄 onLoadMoreListener를 등록합니다. 2번째 이후로 실행되는 getProductsTask는 pageNum에 따라 새로운 데이터를 요청하고 adapterFontTaskListener에 정의된 대로 새로 Adapter를 구상하지 않고 앞서 설정했던 addItemMore함수를 통하여 불러온 새로운 아이템을 추가합니다.
이로써 RecyclerView의 무한 로딩을 구현할 수 있습니다. 저와 같이 GridLayoutManager를 사용하지 않고 LinearLayoutManager를 사용하였다면 로딩아이템도 추가해 볼 수 있습니다. (Grid에서 추가하려면 복잡한 구조가 될 것 같아 잠시 미루기로 했습니다..)
아래는 제가 참고한 블로그의 주소입니다.(로딩 아이템이 추가된 LinearLayoutManager형태입니다.)
https://isntyet.tistory.com/114
다음 시간에는 그 동안 서버에서 불러오는 방식을 Retrofit을 통하여 구현하는 예제로 찾아뵙겠습니다. 아직 공부를 조금 더 해봐야 겠지만 서버 요청을 하고 Json으로 파싱하는 과정을 간소화 할 수 있을 것 같습니다.
부족한 글 읽어주셔서 감사합니다.
그럼 안녕~~
'IT 프로그래밍-Android' 카테고리의 다른 글
[안드로이드] Viewpager와 indicator를 한번에 !! (0) | 2020.03.17 |
---|---|
Retrofit을 적용해서 데이터 불러오기 (0) | 2020.01.18 |
[Android] Glide 라이브러리 gif파일 로드 (0) | 2020.01.10 |
[Android] 서버에서 데이터 불러오기 (0) | 2019.12.23 |
[Android] 키보드 앱 만들기 후기 (12) | 2019.11.20 |