안드로이드/개발관련(Kotlin)

안드로이드 RecyclerView 아이템 줌하기(SanpHelper, ScrollListener)

닉네임못짓는사람 2023. 3. 31. 20:54
반응형

이번 글에서는 RecyclerVIew에서 현재 화면 가운데에 있는 아이템을 강조하는 법에 대해서 알아보도록 하자.

 

Adapter, ViewHolder, itemXml


일단 먼저 RecyclerView를 표시하기 위해서 Adapter와 ViewHolder, itemXml을 정의해주도록 하자.

 

class RecyclerAdapter(
    private val itemList: ArrayList<String>,
    private val onClickItem: (String) -> Unit
): Adapter<RecyclerViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewHolder {
        val binding = ItemRecyclerBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return RecyclerViewHolder(binding)
    }

    override fun onBindViewHolder(holder: RecyclerViewHolder, position: Int) {
        val item = itemList[position]

        holder.binding.run {
            cardView.apply {
                scaleX = 0.9f
                scaleY = 0.9f
            }

            cardView.setOnClickListener {
                onClickItem(item)
            }

            textView.text = item
        }
    }

    override fun getItemCount(): Int = itemList.count()
}

먼저 Adapter를 만들어주도록 하자.

paramer로는 데이터 리스트와 아이템을 클릭 했을 때 호출할 함수를 받아주도록 한다.

 

그 다음으로 뷰의 기본 크기를 0.9배로 맞춰줘서 줌되는 아이템을 좀 더 강조하는 효과를 주도록 하고

아이템 클릭시 onClickItem함수에 아이템을 argument로 넣어줘서 호출하도록 하자.

class RecyclerViewHolder(val binding: ItemRecyclerBinding): ViewHolder(binding.root) {
}
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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="wrap_content"
    android:layout_height="wrap_content">

    <androidx.cardview.widget.CardView
        android:id="@+id/card_view"
        android:layout_width="300dp"
        android:layout_height="400dp"
        android:elevation="10dp"
        app:cardCornerRadius="30dp">

        <TextView
            android:id="@+id/text_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            tools:text="animal"
            android:background="@color/teal_200"
            android:textColor="@color/black"
            android:textSize="50dp" />

    </androidx.cardview.widget.CardView>

</FrameLayout>

ViewHolder와 itemXml은 매우 간단하게 작성해서 따로 설명할 부분은 없다.

 

MainActivity, RecyclerView


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:listitem="@layout/item_recycler"/>

</androidx.constraintlayout.widget.ConstraintLayout>

이번엔 MainActivity를 만들어주는데, 일단 간단하게 RecyclerView만 있게 작성해주자.

 

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    private lateinit var recyclerAdapter: RecyclerAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        initView()
    }

    private fun initView() {
        binding.root.let {
            it.viewTreeObserver.addOnGlobalLayoutListener(object:
                ViewTreeObserver.OnGlobalLayoutListener {
                override fun onGlobalLayout() {
                    it.viewTreeObserver.removeOnGlobalLayoutListener(this)

                    initRecyclerView()
                }
            })
        }
    }

    private fun initRecyclerView() {
        binding.recyclerView.apply {
            recyclerAdapter = RecyclerAdapter(arrayListOf("호랑이", "사자", "곰", "고양이", "개", "기린")) {
                Toast.makeText(this@MainActivity, it, Toast.LENGTH_SHORT).show()
            }

            adapter = recyclerAdapter

            val snap = PagerSnapHelper()
            snap.attachToRecyclerView(this)

            val scrollListener = ScrollListener(layoutManager as LinearLayoutManager, binding.root.width)
            addOnScrollListener(scrollListener)

            addItemDecoration(Decoration(binding.root.width))
        }
    }
}

다음으로 RecyclerView를 설정해주는데, 부분별로 설명해보도록 하겠다.

 

setAdapter

recyclerAdapter = RecyclerAdapter(arrayListOf("호랑이", "사자", "곰", "고양이", "개", "기린")) {
    Toast.makeText(this@MainActivity, it, Toast.LENGTH_SHORT).show()
}

adapter = recyclerAdapter

위에서 작성한 Adapter클래스에 여러 동물 텍스트를 arrayList로 넣어준 뒤, clickEvent로는 Toast를 띄워주도록 한다.

그 뒤 adapter를 recycerView에 연결시켜주기만 하면 된다.

 

GlobalLayoutListener

binding.root.let {
    it.viewTreeObserver.addOnGlobalLayoutListener(object:
        ViewTreeObserver.OnGlobalLayoutListener {
        override fun onGlobalLayout() {
            it.viewTreeObserver.removeOnGlobalLayoutListener(this)

            initRecyclerView()
        }
    })
}

일단 이 부분은 rootView의 width를 얻기위한 부분인데, 자세한 설명은 다음에 기회가 되면 글로 작성해보도록 하겠다.

 

SnapHelper

val snap = PagerSnapHelper()
snap.attachToRecyclerView(this)

다음으로 RecyclerView에 snapHelper를 연결해주는데,

이걸 사용해서 RecyclerView에서 하나의 아이템만 선택할 수 있도록 한다.

 

ScrollListener

val scrollListener = ScrollListener(layoutManager as LinearLayoutManager, binding.root.width)
addOnScrollListener(scrollListener)

이 부분이 이 글에서 핵심인 아이템을 ZoomIn, ZoomOut하는 부분이다.

RecyclerView에 ScrollListener를 만들어서 연결해주는데, 아래에서 어떻게 만들었는지 보도록 하자.

 

class ScrollListener(
    private val layoutManager: LinearLayoutManager,
    private val screenWidth: Int,
): OnScrollListener() {
    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        super.onScrolled(recyclerView, dx, dy)
        setFocus(layoutManager)
    }

    private fun setFocus(manager: LinearLayoutManager) {
        manager.let {
            val firstPos = it.findFirstVisibleItemPosition()
            val lastPos = it.findLastVisibleItemPosition()
            val completePos = it.findFirstCompletelyVisibleItemPosition()

            val firstView = it.findViewByPosition(firstPos)
            val lastView = it.findViewByPosition(lastPos)

            if (completePos != RecyclerView.NO_POSITION) {
                val completeView = it.findViewByPosition(completePos)

                if (lastPos == completePos) firstView?.let { view -> zoomOutView(view) }
                else if (firstPos == completePos) lastView?.let { view -> zoomOutView(view)}
                zoomInView(completeView)
            } else {
                if (firstView != null && lastView != null) {
                    if (lastView.x <= screenWidth / 2) {
                        zoomOutView(firstView)
                        zoomInView(lastView)
                    } else {
                        zoomOutView(lastView)
                        zoomInView(firstView)
                    }
                }
            }
        }
    }

    private fun zoomInView(inView: View?) {
        inView?.run {
            if (!isSelected) {
                val anim = AnimationUtils.loadAnimation(context, R.anim.anim_focus_in)
                startAnimation(anim)
                isSelected = true
            }
        }
    }

    private fun zoomOutView(outView: View?) {
        outView?.run {
            if (isSelected){
                val anim = AnimationUtils.loadAnimation(context, R.anim.anim_focus_out)
                startAnimation(anim)
                isSelected = false
            }
        }
    }

우리가 Listener에서 사용할 함수는 onScrolled이다.

onScrolled함수가 실행되면 setFocus함수를 실행하는데, 어떻게 동작하는지 설명해보도록 하겠다.

 

  1. 현재 화면에서 좌, 우, 중앙에 있는 View들을 각각 구한다. 이 때 사용하는 함수로는
    1) findFirstVisibleItemPosition - 현재 화면에 보이는 View중 맨 처음(좌 or 상)에 있는 아이템의 위치를 구한다.
    2) findLastVisibleItemPosition - 현재 화면에 보이는 View중 맨 마지막(우 or 하)에 있는 아이템의 위치를 구한다.
    3) findFirstCompletelyVisibleItemPosition - 현재 화면에 완전히 보이는 VIew중 맨 처음에 있는 아이템의 위치를 구한다.
  2. 다음으로 조건이 나뉘는데, CompleteView가 있을 경우
    firstView와 lastVIew중 CompleteView가 아닌 View를 ZoomOut하고, CompleteView를 ZoomIn한다.
  3. CompleteView가 없을 경우 firstView와 lastVIew중
    현재 화면에서 더 많은 공간을 차지하고 있는 View를 ZoomIn하고, 그렇지 않은 View를 ZoomOut한다.
    이 부분을 통해 아래와 같은 동작이 가능하다.
  4. ZoomIn, ZoomOut함수에서 animation을 사용해 View를 조작해준다.

 

Decoration


마지막으로 RecyclerView에 Decoration까지 추가해보도록 하자.

class Decoration(
    private val screenWidth: Int
): ItemDecoration() {
    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        val pos = parent.getChildAdapterPosition(view)
        val count = state.itemCount

        view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
        val offset = (screenWidth / 2) - (view.measuredWidth / 2)
        if (pos == 0) {
            outRect.left = offset
        } else if (pos == count - 1) {
            outRect.right = offset
        }
    }
}

Decoration에선 첫 번째 아이템과 마지막 아이템을 중앙에 배치하기 위해서

화면 사이즈의 1/2 - 아이템 사이즈의 1/2값을 offset으로 주도록 한다.

 

 

 

반응형