MotionLayout에 대한 내용은 이전에 게시물을 올린적이 있지만, 다시해봐도 새로운 내용이기 때문에... ㅋㅋ 복습하는 차원에서 정리해봤습니다.
motionLayout에 motion 효과를 주기 위해서 app:layoutDescription 속성에 어떤 모션효과를 줄 것인지 정의 되어있는 xml 파일을 지정해줘야합니다.
우선 밑의 코드를 activity_main 에 복붙하고 효과를 주는 부분은 밑에서 알아보도록 하겠습니다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout 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"
app:layoutDescription="@xml/button_shown_scene"
android:id="@+id/buttonShownMotionLayout">
<ScrollView
android:id="@+id/scrollView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="600dp"
android:background="@color/black"/>
<androidx.constraintlayout.motion.widget.MotionLayout
android:id="@+id/gatheringDigitalThingsLayout"
android:layout_width="480dp"
android:layout_height="300dp"
android:layout_gravity="center_horizontal"
app:layoutDescription="@xml/gathering_digital_things_scene">
<ImageView
android:id="@+id/tvImageView"
android:layout_width="400dp"
android:layout_height="250dp"
android:scaleType="centerCrop"
android:src="@drawable/tv"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/tabletImageView"
android:layout_width="200dp"
android:layout_height="100dp"
android:src="@drawable/tablet"
android:scaleType="centerCrop"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<ImageView
android:id="@+id/laptopImageView"
android:layout_width="200dp"
android:layout_height="150dp"
android:src="@drawable/laptop"
android:scaleType="centerCrop"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<ImageView
android:id="@+id/phoneImageView"
android:layout_width="100dp"
android:layout_height="130dp"
android:src="@drawable/phone"
android:scaleType="centerCrop"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.motion.widget.MotionLayout>
</androidx.appcompat.widget.LinearLayoutCompat>
</ScrollView>
<Button
android:id="@+id/button"
android:layout_width="0dp"
android:layout_height="64dp"
android:text="2주 무료 이용"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"/>
</androidx.constraintlayout.motion.widget.MotionLayout>
우선 버튼을 제외한 4개의 imageView 에 대한 모션을 정의한 xml 파일입니다.
res/xml 경로에 xml 파일을 하나 생성하고 밑의 코드를 작성합니다.
자세한 코드 설명은 밑에서 하겠습니다.
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<Transition
app:constraintSetEnd="@+id/end"
app:constraintSetStart="@+id/start"
app:duration="500">
<KeyFrameSet>
<KeyAttribute
android:scaleX="0.9"
android:scaleY="0.9"
app:framePosition="0"
app:motionTarget="@+id/tvImageView"
app:transitionEasing="decelerate"
android:alpha="0"/>
<KeyAttribute
android:scaleX="1"
android:scaleY="1"
app:framePosition="100"
app:motionTarget="@+id/tvImageView"
app:transitionEasing="decelerate"
android:alpha="1"/>
</KeyFrameSet>
<KeyFrameSet>
<KeyAttribute
android:scaleX="0.8"
android:scaleY="0.8"
app:framePosition="0"
app:motionTarget="@+id/tabletImageView"
app:transitionEasing="decelerate"
android:alpha="0"/>
<KeyAttribute
android:scaleX="1"
android:scaleY="1"
app:framePosition="100"
app:motionTarget="@+id/tabletImageView"
app:transitionEasing="decelerate"
android:alpha="1"/>
</KeyFrameSet>
<KeyFrameSet>
<KeyAttribute
android:scaleX="0.8"
android:scaleY="0.8"
app:framePosition="0"
app:motionTarget="@+id/laptopImageView"
app:transitionEasing="decelerate"
android:alpha="0"/>
<KeyAttribute
android:scaleX="1"
android:scaleY="1"
app:framePosition="100"
app:motionTarget="@+id/laptopImageView"
app:transitionEasing="decelerate"
android:alpha="1"/>
</KeyFrameSet>
<KeyFrameSet>
<KeyAttribute
android:scaleX="0.8"
android:scaleY="0.8"
app:framePosition="0"
app:motionTarget="@+id/phoneImageView"
app:transitionEasing="decelerate"
android:alpha="0"/>
<KeyAttribute
android:scaleX="1"
android:scaleY="1"
app:framePosition="100"
app:motionTarget="@+id/phoneImageView"
app:transitionEasing="decelerate"
android:alpha="1"/>
</KeyFrameSet>
</Transition>
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/tvImageView"
android:layout_width="400dp"
android:layout_height="250dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.9"/>
<Constraint
android:id="@+id/tabletImageView"
android:layout_width="200dp"
android:layout_height="100dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintVertical_bias="0.8"/>
<Constraint
android:id="@+id/laptopImageView"
android:layout_width="250dp"
android:layout_height="150dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintVertical_bias="0.8"/>
<Constraint
android:id="@+id/phoneImageView"
android:layout_width="100dp"
android:layout_height="130dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintVertical_bias="0.8"/>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/tvImageView"
android:layout_width="400dp"
android:layout_height="250dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<Constraint
android:id="@+id/tabletImageView"
android:layout_width="200dp"
android:layout_height="100dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.1"
app:layout_constraintVertical_bias="0.75"/>
<Constraint
android:id="@+id/laptopImageView"
android:layout_width="250dp"
android:layout_height="150dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.8"
app:layout_constraintVertical_bias="0.75"/>
<Constraint
android:id="@+id/phoneImageView"
android:layout_width="100dp"
android:layout_height="130dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.9"
app:layout_constraintVertical_bias="0.7"/>
</ConstraintSet>
</MotionScene>
편의를 위해 tvImageView 의 모션만 가지고 설명하겠습니다.
<Transition
app:constraintSetEnd="@+id/end"
app:constraintSetStart="@+id/start"
app:duration="500">
<KeyFrameSet>
<KeyAttribute
android:scaleX="0.9"
android:scaleY="0.9"
app:framePosition="0"
app:motionTarget="@+id/tvImageView"
app:transitionEasing="decelerate"
android:alpha="0"/>
<KeyAttribute
android:scaleX="1"
android:scaleY="1"
app:framePosition="100"
app:motionTarget="@+id/tvImageView"
app:transitionEasing="decelerate"
android:alpha="1"/>
</KeyFrameSet>
</Transition>
Transition 으로 모션의 시작 및 종료상태, 원하는 중간상태를 지정하고, KeyFrameSet 안에 있는 KeyAttribute 로 모션이 동작하는 과정에서 뷰 속성의 변화를 지정합니다.
위의 코드에서는 모션 타겟을 tvImageView로 두고, 속성값을 지정해줬습니다.
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/tabletImageView"
android:layout_width="200dp"
android:layout_height="100dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintVertical_bias="0.8"/>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/tabletImageView"
android:layout_width="200dp"
android:layout_height="100dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.1"
app:layout_constraintVertical_bias="0.75"/>
</ConstraintSet>
Constraint 는 모션 요소의 위치를 지정해줍니다.
start 상태일 때, app:layout_constraintHorizontal_bias="0" 였던 것이
end 상태일 때, app:layout_constraintHorizontal_bias="0.1" 변화되어 start -> end 로 모션이 변화하면서 왼쪽에서 살짝 튀어나오는 듯한 효과가 발생합니다.
app:layout_constraintVertical_bias 속성도 같은 맥락입니다.
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<Transition
app:constraintSetEnd="@id/end"
app:constraintSetStart="@+id/start"
app:duration="500">
<KeyFrameSet>
<KeyAttribute
app:framePosition="0"
app:motionTarget="@+id/button"
app:transitionEasing="decelerate"
android:alpha="0"/>
<KeyAttribute
app:framePosition="100"
app:motionTarget="@+id/button"
app:transitionEasing="decelerate"
android:alpha="1"/>
</KeyFrameSet>
</Transition>
<ConstraintSet android:id="@+id/start">
<Constraint android:id="@+id/button"
android:layout_width="0dp"
android:layout_height="64dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="1.4"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"/>
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint android:id="@+id/button"
android:layout_width="0dp"
android:layout_height="64dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.97"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"/>
</ConstraintSet>
</MotionScene>
start 상태일 때, app:layout_constraintVertical_bias="1.4" (화면 밑 바깥쪽)
end 상태일 때, app:layout_constraintVertical_bias="0.97"(화면 밑에서 살짝 위)
화면 밑에서 튀어나오는 듯한 효과가 발생합니다.
위에서 모션 레이아웃에 어떤 모션효과를 줄 것인지 설정을 했고, 코드를 통해 모션효과를 실제로 적용하는 과정이 필요합니다.
그리고, 어느 시점에 start -> end 모션을 주고, end -> start 모션을 주는지 알려줘야합니다.
import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.TypedValue
import androidx.constraintlayout.motion.widget.MotionLayout
import com.dldmswo1209.ott.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
val binding by lazy{ ActivityMainBinding.inflate(layoutInflater) }
private var isGatheringMotionAnimating = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
// 현재 scrollView 의 y축 값을 가져오기 위해서 viewTreeObserver 를 사용해야 한다.
// viewTreeObserver 에 리스너를 붙여줘서 스크롤 변화를 감지한다.
binding.scrollView.viewTreeObserver.addOnScrollChangedListener {
if(binding.scrollView.scrollY > 150f.dpToPx(this).toInt()){
// 150px 만큼 스크롤 되면
if(!isGatheringMotionAnimating){
binding.gatheringDigitalThingsLayout.transitionToEnd() // 애니메이션 동작 start->end
binding.buttonShownMotionLayout.transitionToEnd()
}
}else{ // 150px 만큼 스크롤 되지 않은 경우
if(!isGatheringMotionAnimating){
binding.gatheringDigitalThingsLayout.transitionToStart() // 애니메이션 동작 end->start
binding.buttonShownMotionLayout.transitionToStart()
}
}
}
// 모션 레이아웃에 setTransitionListener 를 등록해서 transition 상태를 감지한다.
// start 상태이면 flag 변수를 true , end 상태이면 false
binding.gatheringDigitalThingsLayout.setTransitionListener(object: MotionLayout.TransitionListener{
override fun onTransitionStarted(p0: MotionLayout?, p1: Int, p2: Int) {
isGatheringMotionAnimating = true
}
override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) {
}
override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) {
isGatheringMotionAnimating = false
}
override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) {}
})
}
// dp -> px 함수
fun Float.dpToPx(context: Context): Float =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, context.resources.displayMetrics)
}
코드에 대한 설명은 주석을 열심히 달아놓았으니,, 생략하겠습니다.
해당 프로젝트는 FastCampus의 30개 프로젝트로 배우는 Android 앱 개발 with Kotlin 초격차 패키지 Online. 을 수강하면서 만든 프로젝트 입니다.
https://developer.android.com/training/constraint-layout/motionlayout?hl=ko