MotionLayout을 이용한 통화 수신 화면 예제

이 화면의 받기/거절 버튼의 애니메이션을 만들어 보았다.

ViewBinding과 constraintlayout 을 넣었다.

buildFeatures {
  viewBinding true
}

dependencies {
  implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
}

화면의 레이아웃은 이렇게 잡았다. 애니메이션 시작, 종료 테스트를 위한 버튼과, 받기/거절의 위치를 조절하기 위한 빨간줄로 표시한 가이드라인용 뷰도 추가했다. 빨간줄은 실제 화면에서는 표시되지 않는다.

<?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"
  android:background="#00f"
  tools:context=".MainActivity">

  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:orientation="horizontal"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent">

    <Button
      android:id="@+id/startButton"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="start" />

    <Button
      android:id="@+id/endButton"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginStart="5dp"
      android:text="stop" />
  </LinearLayout>

  <androidx.constraintlayout.motion.widget.MotionLayout
    android:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layoutDescription="@xml/main_activity_scene"
    app:layout_constraintBottom_toBottomOf="parent">

    <View
      android:id="@+id/guide1"
      android:layout_width="0dp"
      android:layout_height="1dp"
      app:layout_constraintEnd_toStartOf="@id/acceptButton"
      app:layout_constraintHorizontal_chainStyle="spread"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="@id/acceptButton"
      app:layout_constraintWidth_percent="0.1"
      tools:background="#f00" />

    <View
      android:id="@+id/guide2"
      android:layout_width="0dp"
      android:layout_height="1dp"
      app:layout_constraintEnd_toStartOf="@id/rejectButton"
      app:layout_constraintStart_toEndOf="@id/acceptButton"
      app:layout_constraintTop_toTopOf="@id/guide1"
      app:layout_constraintWidth_percent="0.3"
      tools:background="#f00" />

    <View
      android:id="@+id/guide3"
      android:layout_width="0dp"
      android:layout_height="1dp"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toEndOf="@id/rejectButton"
      app:layout_constraintTop_toTopOf="@id/guide1"
      app:layout_constraintWidth_percent="0.1"
      tools:background="#f00" />

    <ImageView
      android:id="@+id/bgAcceptButton"
      android:layout_width="60dp"
      android:layout_height="60dp"
      android:alpha="1"
      android:background="@drawable/bg_btn_call_normal"
      app:layout_constraintBottom_toBottomOf="@id/acceptButton"
      app:layout_constraintEnd_toEndOf="@id/acceptButton"
      app:layout_constraintStart_toStartOf="@id/acceptButton"
      app:layout_constraintTop_toTopOf="@id/acceptButton" />

    <ImageView
      android:id="@+id/bgRejectButton"
      android:layout_width="60dp"
      android:layout_height="60dp"
      android:background="@drawable/bg_btn_call_normal"
      app:layout_constraintBottom_toBottomOf="@id/rejectButton"
      app:layout_constraintEnd_toEndOf="@id/rejectButton"
      app:layout_constraintStart_toStartOf="@id/rejectButton"
      app:layout_constraintTop_toTopOf="@id/rejectButton" />

    <ImageView
      android:id="@+id/acceptButton"
      android:layout_width="60dp"
      android:layout_height="60dp"
      android:layout_marginBottom="100dp"
      android:background="@drawable/bg_btn_call"
      android:padding="15dp"
      android:src="@drawable/ic_call_accept"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toStartOf="@id/guide2"
      app:layout_constraintStart_toEndOf="@id/guide1" />

    <ImageView
      android:id="@+id/rejectButton"
      android:layout_width="60dp"
      android:layout_height="60dp"
      android:background="@drawable/bg_btn_call"
      android:padding="15dp"
      android:src="@drawable/ic_call_reject"
      app:layout_constraintBottom_toBottomOf="@id/acceptButton"
      app:layout_constraintEnd_toStartOf="@id/guide3"
      app:layout_constraintStart_toEndOf="@id/guide2"
      app:layout_constraintTop_toTopOf="@id/acceptButton" />
  </androidx.constraintlayout.motion.widget.MotionLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

그런 다음 애니메이션 scene을 넣어야하는데, res/xml 폴더에 scene 하나를 만든다.

Transition의 KeyAttribute는 속성값을 바꿀 수 있고, ConstraintSet은 크기를 바꿀 수 있다. 그래서 받기/거절과 같은 크기로 있던 반투명한 흰색 원이 점점 커지면서 투명해진다. 2초동안 커지고, 다시 반복한다.

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:motion="http://schemas.android.com/apk/res-auto">

  <Transition
    motion:autoTransition="animateToEnd"
    motion:constraintSetEnd="@+id/end"
    motion:constraintSetStart="@id/start"
    motion:duration="2000">
    <KeyFrameSet>
      <KeyAttribute
        android:alpha="0.4"
        motion:framePosition="60"
        motion:motionTarget="@+id/bgAcceptButton" />
      <KeyAttribute
        android:alpha="0"
        motion:framePosition="100"
        motion:motionTarget="@+id/bgAcceptButton" />
      <KeyAttribute
        android:alpha="0.4"
        motion:framePosition="70"
        motion:motionTarget="@+id/bgRejectButton" />
      <KeyAttribute
        android:alpha="0"
        motion:framePosition="100"
        motion:motionTarget="@+id/bgRejectButton" />
    </KeyFrameSet>
  </Transition>
  <Transition
    motion:autoTransition="animateToStart"
    motion:constraintSetEnd="@id/end"
    motion:constraintSetStart="@id/start" />

  <ConstraintSet android:id="@+id/start">
    <Constraint
      android:id="@+id/bgAcceptButton"
      android:layout_width="60dp"
      android:layout_height="60dp"
      motion:layout_constraintBottom_toBottomOf="@id/acceptButton"
      motion:layout_constraintEnd_toEndOf="@id/acceptButton"
      motion:layout_constraintStart_toStartOf="@id/acceptButton"
      motion:layout_constraintTop_toTopOf="@id/acceptButton" />
    <Constraint
      android:id="@+id/bgRejectButton"
      android:layout_width="60dp"
      android:layout_height="60dp"
      motion:layout_constraintBottom_toBottomOf="@id/rejectButton"
      motion:layout_constraintEnd_toEndOf="@id/rejectButton"
      motion:layout_constraintStart_toStartOf="@id/rejectButton"
      motion:layout_constraintTop_toTopOf="@id/rejectButton" />
  </ConstraintSet>

  <ConstraintSet android:id="@+id/end">
    <Constraint
      android:id="@+id/bgAcceptButton"
      android:layout_width="100dp"
      android:layout_height="100dp"
      motion:layout_constraintBottom_toBottomOf="@id/acceptButton"
      motion:layout_constraintEnd_toEndOf="@id/acceptButton"
      motion:layout_constraintStart_toStartOf="@id/acceptButton"
      motion:layout_constraintTop_toTopOf="@id/acceptButton" />
    <Constraint
      android:id="@+id/bgRejectButton"
      android:layout_width="100dp"
      android:layout_height="100dp"
      motion:layout_constraintBottom_toBottomOf="@id/rejectButton"
      motion:layout_constraintEnd_toEndOf="@id/rejectButton"
      motion:layout_constraintStart_toStartOf="@id/rejectButton"
      motion:layout_constraintTop_toTopOf="@id/rejectButton" />
  </ConstraintSet>
</MotionScene>

코드에서 할 일은 거의 없다.

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

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

    binding.startButton.setOnClickListener { startAnimation() }
    binding.endButton.setOnClickListener { stopAnimation() }
  }

  private fun startAnimation() {
    binding.motionLayout.transitionToEnd()
  }

  private fun stopAnimation() {
    binding.motionLayout.progress = 0f
  }
}

만든 예제는 https://github.com/susemi99/SamsungCallScreenSample 에 올려놨다.