밍맹의 생각날 때 적는 블로그

[Android/Kotlin] Shared elemet transition - RecyclerView에 적용 본문

안드로이드

[Android/Kotlin] Shared elemet transition - RecyclerView에 적용

mingmaeng 2020. 6. 25. 19:57

 

이 포스팅은 이전 화면전환 애니메이션 포스팅의 연장선이다.

이전 포스팅을 보지 못했다면 한 번 보고 오는 것을 추천한다.

 

[Android/Kotlin] 화면 전환 시 생동감 있게 애니메이션 적용하기

 

[Android/Kotlin] 화면 전환 시 생동감 있게 애니메이션 적용하기

최근 안드로이드 개발을 공부하면서 관심을 가지고 있는 분야가 디자인이다. 앱이 최적화가 잘 돼있고, 효율이 좋은 것도 중요하지만 정작 앱이 멋이 없다면 사용자의 만족도를 끌어올리기 힘��

kangmin1012.tistory.com

 

이번 포스팅에서는 RecyclerView에서 아이템을 클릭했을 때, 화면 전환 애니메이션을 적용하는 법을 알아보겠다.

이번 실습을 하면서 이전 포스팅에서 의문을 느꼈던 점 일부를 해결했으니 그 의문에 대해서도 얘기해보려고 한다.

 


레이아웃은 본인이 원하는 레이아웃을 사용해도 무방

 지난 번 포스팅에서는 RelativeLayout을 사용했다. 이는 화면 전환 애니메이션이 RelativeLayout을 기반으로

작동하는 줄 알았기 때문이었다. 그러나 이번 실습을 통해서 다른 레이아웃을 써도 정상적으로 작동한다는 것을 알게 되었다.

 

먼저 RecylerView에 사용할 xml 이미지를 만들어 준다.

 

layout_rcv_item.xml

<?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:background="?attr/selectableItemBackground"
    android:layout_gravity="center"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <androidx.cardview.widget.CardView
        android:background="@color/white"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="15dp"
        android:elevation="8dp"
        app:cardCornerRadius="5dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            >

            <ImageView
                android:id="@+id/product_img"
                android:layout_width="100dp"
                android:layout_height="0dp"
                android:layout_marginHorizontal="10dp"
                android:scaleType="fitXY"
                android:transitionName="imageTransition"
                app:layout_constraintDimensionRatio="1"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                tools:srcCompat="@tools:sample/avatars" />

            <TextView
                android:id="@+id/product_txt"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginVertical="15dp"
                android:layout_marginHorizontal="15dp"
                android:textColor="@color/black"
                android:textSize="20sp"
                android:singleLine="true"
                android:transitionName="titleTransition"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/product_img"
                tools:text="Title" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </androidx.cardview.widget.CardView>

</androidx.constraintlayout.widget.ConstraintLayout>

 

나름 디자인 적인 멋(?)을 살려서 CardView를 사용하였다. 레이아웃을 ConstraintLayout을 사용한 것을 볼 수 있다.

 

ItemClickListener를 외부에서 만들기

 RecyclerView를 만들기 위해서는 Adapter와 ViewHolder가 필요하다.

기존 아이템클릭이벤트를 구현하기 위해서는 ViewHolder에서 간단하게 구현할 수 있었다.

그러나 화면 전환 애니메이션을 적용하기 위한 ActivityOptions.makeSceneTransitionAnimation()에는 액티비티가 들어간다.

ViewHolder에서는 액티비티를 불러올 수 없으므로 ( 불러오는 방법을 알고 계시다면 알려주세요.... ) 아이템 클릭 리스너를 액티비티에서 작성해 준다.

 

 기존 어댑터에서 추가적으로 다음과 같은 코드를 추가해 준다.

 

ProductAdapter.kt

class ProductAdapter (val context : Context) : RecyclerView.Adapter<ProductVH>(){

    ...

    interface OnItemClickListener{
        fun onItemClick(v : View, data : ProductData)
    }

    private var listener : OnItemClickListener? = null

    fun setOnItemClickListener (listener : OnItemClickListener){
        this.listener = listener
    }
    
    ...
    
    override fun onBindViewHolder(holder: ProductVH, position: Int) {
        holder.onBind(data[position],listener)
    }
    
}

 

아이템클릭리스너를 정의하는 코드를 작성한다. 그리고 onBindViewHolder()에서 뷰홀더에 있는 onBind()를 호출 할 때

아이템 데이터 뿐 아니라 listner까지 전달해 준다.

 

ProductVH.kt

class ProductVH (view : View) : RecyclerView.ViewHolder(view){

    val productImg : ImageView = view.findViewById(R.id.product_img)
    val productTitle : TextView = view.findViewById(R.id.product_txt)
    private var mLastClickTime : Long = 0

    fun onBind(
        data: ProductData,
        listener: ProductAdapter.OnItemClickListener?
    ){

        Glide.with(itemView).load(data.productImg)
            .error(R.drawable.ic_baseline_close_24)
            .into(productImg)
        productTitle.text = data.productTitle


        itemView.setOnClickListener {
            if(SystemClock.elapsedRealtime() - mLastClickTime > 1000){
                val pos = adapterPosition
                if ( pos != RecyclerView.NO_POSITION){
                    listener?.onItemClick(itemView, data)
                }
            }
            mLastClickTime = SystemClock.elapsedRealtime()
        }

    }
}

 

onBind() 함수 안에 itemView.setOnClickListener 부분을 집중적으로 보면 된다.

아이템을 클릭한 위치를 받아와서 해당 위치가 item 위치일 경우 listener에 존재하는 onItemClick()함수를 호출한다.

SystemClock.elapsedRealtime()을 이용해서 더블클릭을 방지해 준다.

더블클릭을 허용할 경우 연속적인 아이템 클릭 시 어플이 죽어버리기 때문에 꼭 설정해 주도록 한다.

 

LayoutAniActivity.kt

...
adapter.setOnItemClickListener(object : ProductAdapter.OnItemClickListener{
            override fun onItemClick(v: View, data : ProductData) {


                val intent = Intent(this@LayoutaniActivity, SecondLayoutActivity::class.java)
                intent.putExtra("data",data)

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){

                    val options : ActivityOptions = ActivityOptions.makeSceneTransitionAnimation(
                        this@LayoutaniActivity,v.product_img,"imageTransition"
                    )
                    startActivity(intent,options.toBundle())
                }
                else{
                    startActivity(intent)
                }


            }
        })

 

이제 액티비티 코드에서 아이템클릭 리스너를 정의하여 사용하면 된다.

API21 이상부터 애니메이션이 호환되기 때문에, API를 체크해주는 부분까지 구현해 주었다.

애니메이션이 지나치게 많으면 화면이 매우 조잡해지기 때문에 자연스러운 애니메이션을 보여주기 위해서

이미지만 애니메이션을 설정해 준다. 단일 애니메이션 적용 시에는 Pair()를 사용할 필요가 없다.

 


애니메이션을 공부하면서 ViewHolder가 아닌 외부에서 ItemClick을 구현하는 것도 배우고, 어떻게 해야 자연스러운

연출이 가능한가를 진지하게 고민할 수 있었던 것 같다. 앞으로 다양한 상황에서 애니메이션을 구현하게 될 텐데

그럴 때마다 새롭게 코드를 짜는 방법을 배우게 되면 자연스럽게 코드의 흐름들을 잡을 수 있을 것 같다.

실습한 내용은 Git에 있는 LayoutAniActivity와 rcv 패키지, SecondLayoutActivity를 확인하면 된다.

(다른 프로젝트에다가 구현해 놓은 것이라 다른게 껴 있다...)

 

https://github.com/kangmin1012/NewAndroidExercise/tree/AnimationSample

 

kangmin1012/NewAndroidExercise

Contribute to kangmin1012/NewAndroidExercise development by creating an account on GitHub.

github.com

 

Comments