Exoplayer2 사용하기

컴퓨터공부/안드로이드 2020. 4. 12. 15:02 Posted by 아는 개발자

 

0. ExoPlayer란?

 

안드로이드에서 영상 재생을 위해 사용하는 플레이어로 기본 내장 라이브러리인 MediaPlayer가 있었는데 스트리밍 서비스가 주류를 이루면서 구글에서 DASH와 SmoothStreaming을 지원하는 ExoPlayer 라이브러리를 도입했다. 유튜브, 네이버 동영상 프레임들도 Exoplayer를 사용하고 있다고 하니 앞으로 안드로이드 동영상 플레이어는 ExoPlayer가 주류를 이룰 것 같은 예감이다. 아니면 이미 그런지도 모르겠고.

 

ExoPlayer는 MediaPlayer에서 이미 지원하는 기능에서 새로운 기능을 추가한 것이기 때문에 로컬/인터넷 동영상 파일 재생은 당연히 가능하고 Android Media Codec 기반으로 작업을 해서 Media Codec가 도입되기 시작한 안드로이드 기기 (API16 이상)에선 대부분 문제 없이 동작 한다. 물론 일부 기능은 더 높은 API 버전이 필요하긴 하지만 이는 거의 특수한 경우인 것 같다. 이번 포스트에서는 ExoPlayer를 사용하는 방법을 간단히 다룰 예정이다.

 

1. Components 

 

ExoPlayer: ExoPlayer의 라이브러리중 Renderer, 즉 화면에 뿌려주는 역할을 하는 컴포넌트다. ExoPlayer 인터페이스로 커스텀하게 만들 수 있으며 SimpleExoPlayer는 ExoPlayer에서 제공하는 컴포넌트다. 특별히 커스터마이즈 할 것이 아니면 이걸 그냥 가져다 쓰는게 좋다. 아래 함수를 통해 만들 수 있다.

 

val simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(requireContext(), trackSelector)

 

TrackSelector: SimpleExoPlayer 생성 과정에서 두번째 인자로 전달된 클래스는 영상의 Track 정보를 세팅하는 역할을 한다. 이 정보라면 예를 들면 선호하는 오디오 언어는 어떤 것인지, 비디오 사이즈는 무엇인지, 비디오 bitrate는 어떤 것으로 할지 등등 이런 것들을 말한다. 이것도 Renderer와 동일하게 따로 커스터마이즈 할 수 있긴 하나 특별한 이유가 없다면 라이브러리에서 기본으로 만들어 둔 것을 쓰는게 가장 좋다.

 

아래 코드는 TrackSelector를 만들 때 AdaptiveTrackSelection 팩토리를 사용한 예시다. AdaptiveTrackSelection 팩토리 클래스는 현재 bandwidth 정보를 이용해 현재 선택된 track에서 최상의 퀄리티를 제공하는 역할을 한다고 한다. 더 자세한 내용은 라이브러리 내부 주석을 살펴보는 것이 좋을 것 같다. Streaming 서비스를 한다면 이쪽 클래스를 주요하게 보게될 것 같다.

 

val bandwidthMeter = DefaultBandwidthMeter()
val videoTrackSelectionFactory = AdaptiveTrackSelection.Factory(bandwidthMeter)
val trackSelector = DefaultTrackSelector(videoTrackSelectionFactory)

 

MediaSource: 영상에 출력할 미디어 정보를 가져오는 클래스다. ExtractorFactory 클래스를 통해 만드는데 이 클래스는 DataSource 클래스를 주입해서 만든다. 여기서 사용한 DefaultDataSource도 다른 라이브러리처럼 ExoPlayer에서 uri 형태로된 데이터를 읽어오기 위해 기본적으로 제공하는 라이브러리다. 특별한 형태의 DataSource 클래스를 사용하고 싶다면 커스터마이즈가 가능하다. 

 

val extractorFactory = ExtractorMediaSource.Factory(DefaultDataSourceFactory(context, Util.getUserAgent(context, context!!.applicationInfo.packageName)))
val mediaSource = extractorFactory.createMediaSource(Uri.parse(mediaPath))

 

Player: 영상 재생을 위해선 미디어를 읽어오는 작업뿐만 아니라 영상을 UI 상에 뿌려줄 수 있는 뷰어가 필요한데 ExoPlayer용 뷰어가 따로 있다. 아래 코드를 XML에 넣으면 된다. 재생바, 앞으로 당기기기 같은 기본적인 UI 기능도 지원한다.

 

<com.google.android.exoplayer2.ui.PlayerView
        android:id="@+id/fr_main_player"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#000"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

 

2. Play Video 

 

앞서 소개한 컴포넌트들을 하나의 코드로 조합하면 재생이 가능하다. 코드의 순서는 설명한 순서와 조금 다른데 이는 클래스 생성 후에 주입하기 위함이다. exoPlayer.prepare(mediaSource)는 영상 정보를 가져오는 작업이고 fr_main_player.player.playWhenReady는 준비되면 영상을 시작하는 함수다.

 

val mediaPath = "http://somewhere...."
val bandwidthMeter = DefaultBandwidthMeter()
val videoTrackSelectionFactory = AdaptiveTrackSelection.Factory(bandwidthMeter)
val trackSelector = DefaultTrackSelector(videoTrackSelectionFactory)
val exoPlayer = ExoPlayerFactory.newSimpleInstance(requireContext(), trackSelector)
val extractorFactory = ExtractorMediaSource.Factory(
    DefaultDataSourceFactory(
        context,
        Util.getUserAgent(context, context!!.applicationInfo.packageName)
    )
)
val mediaSource = extractorFactory.createMediaSource(Uri.parse(mediaPath))

fr_main_player.player = exoPlayer
exoPlayer.prepare(mediaSource)
fr_main_player.player.playWhenReady = true

 

3. Extension 

 

Player.Listener: 영상 재생중 로딩에 실패하거나 Track 속성이 바뀌거나 혹은 영상 재생이 완료된 경우에 대해서 리스너를 등록해줄 수 있는데 이 경우들은 뷰어에 리스너를 등록해서 구현이 가능하다. 아래 코드를 통해 어떤 경우에 대해서 콜백 호출이 가능한지 확인 해볼 수 있다. 추가로 아래 코드에선 영상 재생이 완료된 경우 다시 재생하도록 구현했다.

 

fr_main_player.player.addListener(object: Player.EventListener{
    override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters?) {}
    override fun onSeekProcessed() {}
    override fun onTracksChanged(trackGroups: TrackGroupArray?, trackSelections: TrackSelectionArray?) {}
    override fun onPlayerError(error: ExoPlaybackException?) {}
    override fun onLoadingChanged(isLoading: Boolean) {}
    override fun onPositionDiscontinuity(reason: Int) {}
    override fun onRepeatModeChanged(repeatMode: Int) {}
    override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {}
    override fun onTimelineChanged(timeline: Timeline?, manifest: Any?, reason: Int) {}
    override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
        if (playbackState == Player.STATE_ENDED) {
            fr_main_player.player.seekTo(0)
            fr_main_player.player.playWhenReady = true
        }
    }
})

 

CacheDataSource: 인터넷으로 영상을 받는 경우 여러번 재생을 할 때 마다 동일한 데이터를 계속 인터넷으로 불러오게돼 데이터를 낭비할 수도 있는 문제가 있다. ExoPlayer에서는 이 문제점을 해결하고자 별도의 미디어 데이터 저장 공간으로 Cache를 뒀다. 이것도 다양하게 커스터마이즈 할 수 있으나 가장 기본적인 사용 방법은 아래 코드와 같다. 

 

ExtractorMediaSource.Factory 함수에서 호출 할 수 있도록 임의의 클래스를 DataSource.Factory의 인터페이스를 구현한 형태로 만든다. 리턴 값으로는 CacheDataSource가 되는데 여기서 생성자에서 캐시가 가져야할 정보를 입력하게 된다. 아래 코드 보면 캐시의 크기도 설정 할 수 있도 플래그를 넣을 수 있는 것도 확인 할 수 있다.

 

private class CacheDataSourceFactory internal constructor(
    private val context: Context,
    private val defaultDataSourceFactory: com.google.android.exoplayer2.upstream.DataSource.Factory,
    private val maxCacheSize: Long,
    private val maxFileSize: Long,
    private val url: String
) : com.google.android.exoplayer2.upstream.DataSource.Factory {
    override fun createDataSource(): com.google.android.exoplayer2.upstream.DataSource {
        val evictor = LeastRecentlyUsedCacheEvictor(maxCacheSize)
        val simpleCache = SimpleCache(File(context.cacheDir, "media"), evictor)
        return CacheDataSource(
            simpleCache,
            defaultDataSourceFactory.createDataSource(),
            FileDataSource(),
            CacheDataSink(simpleCache, maxFileSize),
            CacheDataSource.FLAG_BLOCK_ON_CACHE or CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
            null
        )
    }
}

 

위 코드를 이용한 호출부는 다음과 같다. 앞서 설명한 CacheDataSource 팩토리 클래스에서 두번째 인자로 DataSourceFactory를 넣었는데 아래 구현부 코드를 확인해보면 이전에 만든 DefaultDataSourceFactory 클래스를 넣는 것을 볼 수 있다. 외부 데이터를 불러오는 작업은 기존 데이터 클래스를 따라 간다는 뜻이다.

 

val extractorCacheFactory = ExtractorMediaSource.Factory(
    CacheDataSourceFactory(requireContext(), DefaultDataSourceFactory(
        context,
        Util.getUserAgent(context, context!!.applicationInfo.packageName)
    ), MAX_CACHE_SIZE, MIN_CACHE_SIZE, mediaPath)
)

val mediaSource = extractorCacheFactory.createMediaSource(Uri.parse(mediaPath))

 

 

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

Kotlin - Coroutine  (0) 2020.04.15
Kotlin으로 깔끔한 Builder를 만들어보자  (0) 2020.04.14
Exoplayer2 사용하기  (0) 2020.04.12
FragmentManagers Android  (1) 2020.04.06
ViewModelProviders.of deprecated  (0) 2020.04.06
Exoplayer에 stetho 적용하기  (0) 2020.03.16

FragmentManagers Android

컴퓨터공부/안드로이드 2020. 4. 6. 15:56 Posted by 아는 개발자

 

FragmentManager는 동적인 UI를 제공하기 위한 클래스인 Fragment를 관리하는 컨트롤러 역할을 한다. Manager라는 이름에서 예상 할 수 있듯이 FragmentManager를 사용하면 현재 UI에 Fragment를 추가할 수도 있고 있는 것을 교체할 수도 있으며 제거까지 가능하다. 호출하는 함수는 Activity인지, Fragment인지에 따라 다른데 일반적으로 Activity는 supportFragmentManager를 호출하게 되고, Fragment는 childFragmentManager 또는 parentFragmentManager를 통해 호출한다. 지금까지 개발 할 때는 각각의 차이를 확인하지 않고 '일단 동작부터 되도록' 에 주안점을 뒀는데 이번 포스트를 통해서 각각의 차이점과 적절한 쓰임새를 정리해보려고 한다.

 

0. 정의

 

FragmentManager의 경우에는 안드로이드 문서도 그닥 꼼꼼히 정리 되어있지 않고 스택오버플로우에서도 관심있게 다루는 주제가 아니라 참고할 만한 글이 별로 없었다. 그래서 지금까지 개발하면서 내가 나름대로 내린 사전적(?) 정의는 이렇다.

  • supportFragmentManager(SFM): Activity랑 상호작용하는(interacting) Fragment를 관리하는 클래스. Activity 클래스에서 호출이 가능하며 Activity 고유의 클래스다.

  • parentFragmentManager(PFM): 부모 UI 컴포넌트(Activity일 수도 있고 Fragment 일 수도 있다) 고유의 FragmentManager. Fragment 클래스에서 호출이 가능하다.

  • childFragmentManager(CFM): Fragment 고유의 FragmentManager 클래스. Fragment 별로 모두 다르다.

1. 그림 

 

SFM과 CFM과는 달리 PFM은 고유의 값이 아니라 특정 객체를 가르키는 값이다.  아래의 그림처럼 PartyActivity, PartyFragment 내부에 여러개의 PayFragment를 둔다고 해보자. 그러면 이들의 관계는 PartyActivity가 최상위 부모, PartyFragment는 부모, PayFragment는 자손이 되는 형태가 될 것이다. 

 

PartyActivity -> PartyFragment -> PayFragment

 

그러면 각각의 UI 요소들이 갖고 있는 FragmentManager는 아래와 같은 형태를 따르게 된다. 붉은 선으로 표현한 부분은 서로 동일한 객체인 것을 의미한다. Activity의 SFM을 이용해서 PartyFragment를 관리하고 있으므로 PFM는 부모인 PartyActivity 의 고유 FragmentManager, SFM을 가리키게 된다. 마찬가지로 PayFragment는 PartyFragment의 CFM으로부터 생성 됐으므로 PayFragment의 PFM은 부모인 PartyFragment의 CFM을 가리키게 된다.

 

PayFragment를 PartyFragment의 CFM을 이용해서 생성한 경우

 

로그를 통해 객체의 값을 확인해보면 위와 같은 구조를 가지는 것을 확인 할 수 있다. PartyFragment의 PFM 값은 PartyActivity의 SFM과 동일하고, PayFragment의 PFM값은 PartyFragment의 CFM과 동일하다.

 

UI 클래스 별로 PFM, SFM, CFM의 값을 출력한 결과

2. 주의점

 

PayFragment를 PartyFragment의 CFM으로 생성하지 않고 PFM으로 생성하는 경우 다음과 같이 그림이 달라진다. PartyFragment의 PFM은 PartyActivity의 SFM이며 PayFragment의 생성주체는 PartyActivity의 SFM이기 때문에, PayFragment의 PFM은 자연스레 PartyActivity의 SFM을 가리키게 된다.

 

PayFragment를 PartyFragment의 PFM으로 생성한 경우

 

물론 이런 형태여도 동작하는데는 큰 문제가 없을 것이다. 하지만 FragmentManager는 Fragment와 생성주기를 함께 하기 때문에, PartyFragment가 삭제되도 PayFragment는 SFM의 인스턴스로 남아있게 된다. 물론 이게 남아있는다고 동작상에 크게 흠을 주거나 메모리 릭을 유발하는 것은 아니지만 의도하지 않은 형태로 개발을 하다보면 정체모를 버그가 튀어나올 수 있으니 염두에두고 있는게 좋을 것 같다.

 

  1. 안발자 2020.07.21 12:03  댓글주소  수정/삭제  댓글쓰기

    Fragment와 Activity 사이에 fragmentManager 사용에 관해 고민이 있었는데 관련해서 좋은 인사이트가 되었습니다 감사합니다 :)

ViewModelProviders.of deprecated

컴퓨터공부/안드로이드 2020. 4. 6. 15:01 Posted by 아는 개발자

ViewModel을 주입할 때 주로 사용하는 ViewModelProviders 클래스는 lifecycle-extension 라이브러리가 2.2.0 버전업 되면서 통째로 Deprecated가 됐다. 하지만 ViewModelProvider(뒤에 s만 빠진 클래스가 맞다) 클래스를 통해 동일한 기능을 수행하도록 할 수 있다.

 

BEFORE

@Module(includes = [BaseActivityModule::class])
abstract class MainActivityModule {
    @Binds
    abstract fun provideActivity(activity: MainActivity): FragmentActivity

    @Module
    companion object {
        @Provides
        @JvmStatic
        fun provideViewModel(activity: FragmentActivity, viewModelFactory: ViewModelFactory): MainViewModel
                = ViewModelProviders.of(activity).get(MainViewModel::class.java)
    }
}

AFTER

@Module(includes = [BaseActivityModule::class])
abstract class MainActivityModule {
    @Binds
    abstract fun provideActivity(activity: MainActivity): FragmentActivity

    @Module
    companion object {
        @Provides
        @JvmStatic
        fun provideViewModel(activity: FragmentActivity, viewModelFactory: ViewModelFactory): MainViewModel
                = ViewModelProvider(activity, viewModelFactory).get(MainViewModel::class.java)
    }
}

 

fragment의 부모 activity 를 넘겨서 fragment와 activity가 동일한 viewmodel을 바라보게하는 기능도 정상적으로 동작한다

 

@Module(includes = [BaseFragmentModule::class])
abstract class PayFragmentModule {
    @Binds
    abstract fun provideFragment(fragment: PayFragment): Fragment

    @Module
    companion object {
        @Provides
        @JvmStatic
        fun provideViewModel(fragment: Fragment, viewModelFactory: ViewModelFactory): PartyViewModel 
                = ViewModelProvider(fragment.requireActivity(), viewModelFactory).get(PartyViewModel::class.java)
    }
}

 

함수단위면 몰라도 클래스 하나를 통째로 Deprecated 하는 것은 흔치 않는 일인 것 같은데 기존에 있는 ViewModelProvider 클래스에만 집중해서 개선하기 위함이지 않을까 조심스럽게 추측해본다. 그리고 기존에 있던 ViewModelProviders 코드도 대부분 ViewModelProvider를 호출하는 형태였기 때문에 둘이 겹치는 점도 많았던 것 같고. 

Exoplayer에 stetho 적용하기

컴퓨터공부/안드로이드 2020. 3. 16. 10:54 Posted by 아는 개발자

0. 소개

 

Exoplayer는 안드로이드 영상 재생플레이어로 많이 사용되는 오픈소스 프로젝트다. 요즘처럼 스트리밍으로 조각된 영상을 받는 경우엔 클라이언트의 재생 플레이어에서도 서버로 여러번 영상에 대한 요청을 보내게 되는데 이때 안드로이드 네트워크 인스펙터인 stetho를 사용하면 Exoplayer에서 보낸 요청들을 볼 수 있어서 디버깅 할 때 편리하다.

 

implementation 'com.google.android.exoplayer:extension-okhttp:2.7.0'

 

1. Exoplayer + Stetho

 

build.gradle 파일에 exoplayer extension 라이브러리를 추가한다. 감사하게도 exoplayer에서 stetho를 이용해 디버깅을 할 수 있도록 사전 작업을 해두었다.

 

 

라이브러리를 추가한 다음에는 안드로이드 Exoplayer 코드에서 http 요청 부분을 아래의 코드로 변경한다. OkHttpDataSourceFacotry는 DefaultHttpDataSourceFactory와 거의 동일해 동작에는 큰 차이가 없다고 봐도 무방하다.

 

+//    private val mMediaDataSourceFactory = DefaultDataSourceFactory(context, bandwidthMeter,
+//            DefaultHttpDataSourceFactory(Util.getUserAgent(context, context.applicationInfo.packageName), bandwidthMeter))
+
+    private val mMediaDataSourceFactory = DefaultDataSourceFactory(context, bandwidthMeter,
+            OkHttpDataSourceFactory(OkHttpClient.Builder().addNetworkInterceptor(StethoInterceptor()).build(), Util.getUserAgent(context, context.applicationInfo.packageName), bandwidthMeter))

 

2. Stetho

 

만약 애플리케이션에 StethoInterceptor를 사용하지 않고 있었다면 아래의 작업을 추가해야한다. build.gradle 파일에 stetho 라이브러리를 추가하고

 

implementation "com.facebook.stetho:stetho-okhttp3:1.5.1"

 

Application으로 선언된 클래스에 Stetho를 초기화해준다

 

public class DemoApp extends DaggerApplication {
@Override
    public void onCreate() {
        super.onCreate();
        Stetho.initializeWithDefaults(this);

 

3. 결과 

 

테스트 한 결과 영상 파일들을 쪼개서 보낸 요청들을 Stetho를 이용해서 확인 할 수 있었다

 

 

 

0. 소개

 

MotionLayout은 ConstraintLayout 안에 있는 객체들에 대해서 XML 파일들만 추가해서 간단하게 레이아웃 애니메이션 효과를 줄 수 있는 툴 정도로 이해하면 될 것 같다. 예제로 구글 소개글에 있는 예제들 봐도 되고 아래 gif 이미지를 참고해도 좋다. 2018년 말에 나온 기능인데 이제 와서 글을 쓰고 있으니 아주 뒷북인 감이 없지 않다.

 

스와이프로 빨간 사각형을 움직이는 애니메이션을 줄 수 있다.

1. 원리

 

MotionLayout 은 완전히 새로운 기능으로 도입 된 것은 아니고 원래 ConstraintLayout에서 애니메이션 효과를 주기 위해 사용한 ConstraintSet + TransitionManager를 좀 더 쉽게 사용할 수 있는 툴로 도입 됐다. ConstarintSet + TransitionManager에 대해 생소하신 분들은 이 유튜브 영상을 참고하자. 기존 효과보다 더 좋아진 점은 ConstraintSet + TransitionManager 조합에서는 간단한 효과의 경우에도 액티비티, 프래그먼트단에서 코드를 추가해야 했는데 MotionLayout을 사용하면 XML 코드단에서만 수정하면 돼서 변경의 범위를 최소화 할 수 있는 것 같다.

 

ConstarintLayout의 확장 기능으로 도입된 만큼 MotionLayout은 ConstraintLayout의 일부 요소들을 상속 받고 있다. 아래 그림에서 MotionLayout 의 기본 요소인 MotionScene이 ConstraintSet의 속성들을 포함하고 있는 것을 볼 수 있다. 실제 코드에서는 이 요소를 활용해 ConstraintLayout 내부의 객체들의 효과를 주는 일을 한다. 그 아래 Transition 속성을 보면 OnClick과 OnSwipe가 있는데 이는 클릭과 스와이프 인터랙션에 대해서 콜백을 줄 수 있는 것으로 이해하면 된다. 이 글에서는 MotionLayout은 ConstraintLayout 의 속성들을 이용해 애니메이션 할 수 있다는 점이란 것을 기억하고 자세한 내용은 개발 문서를 참고하도록 하자. 

 

2. 예제

 

MotionLayout을 사용하려면 우선 라이브러리를 추가해야한다. build.gradle에 아래의 코드를 추가해 최신 ConstraintLayout 소스를 불러오자. 번외로 2018년도 즈음에 MotionLayout이 ConstraintLayout 2.0.0 라이브러리에 포함되기 시작했는데 아직도 beta 버전에 머무르고 있는거 보니 다른 feature들과 같이 정식으로 릴리즈 되려는 모양인가보다.

 

implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'

 

정상적으로 라이브러리를 불러 왔다면 이제 레이아웃 파일을 수정할 때다. 아래 코드는 기존에 있던 xml 파일을 MotionLayout을 적용할 수 있도록 변경한 것이다.

 

<?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"
    app:layoutDescription="@anim/motion_scene"
    tools:context=".MainActivity">
    <View
        android:id="@+id/button"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:background="@color/colorAccent"
        android:text="Button"/>
</androidx.constraintlayout.motion.widget.MotionLayout>

 

원래는 ConstraintLayout을 사용했었는데 MotionLayout을 사용하고자 클래스 이름을 MotionLayout으로 변경했다. MotionLayout이 ConstraintLayout을 상속받은 클래스이기 때문에 자식 뷰에서 특별히 바꿔야할 것은 없다. 새롭게 추가한 코드는 app:layoutDescription인데 여기에 Motion 효과를 명시한 xml 파일을 넣었다. 

 

motion_scene 파일은 다음과 같다.

 

<?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:constraintSetStart="@+id/start"
        motion:constraintSetEnd="@+id/end"
        motion:duration="1000">
        <OnSwipe
            motion:touchAnchorId="@+id/button"
            motion:touchAnchorSide="right"
            motion:dragDirection="dragRight" />
    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/button"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginStart="8dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/button"
            android:layout_width="64dp"
            android:layout_height="64dp"
            android:layout_marginEnd="8dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintTop_toTopOf="parent" />
    </ConstraintSet>
</MotionScene>

Transition 속성을 보면 motion:constraintSetStart와 motion:constraintSetEnd 속성이 있는데 이는 애니메이션 효과 동안 constraint의 시작과 끝에 대한 정보를 나타낸다. 아래 코드 보면 두개의 ConstraintSet이 있는 것을 볼 수 있는데 시작할 때의 값과 끝의 값이 다른 것을 알 수 있다. 이 포스트 상단에 있는 gif 파일과 일치하는 것을 볼 수 있다.

 

그 아래 OnSwipe는 특정 뷰를 스와이프 할 때 줄 수 있는 효과를 명시했다. touchAnchorId의 값이 +@id/button으로 설정돼있는데 이는 MotionLayout에서 button 이란 id를 가진 뷰에게는 swipe 효과를 줄 것이라는 뜻이다. motion:touchAnchorSide와 motion:dragDirection이 있는데 이 값들을 이용해서 스와이프에 추가로 효과를 줄 수 있다.

 

3. 짧은 평

 

Android Studio 4.0에서는 XML파일에 MotionLayout 미리보기 화면에서도 애니메이션 효과를 보여줄 예정이라고 하니(4.0-beta 버전 참고) 구글에서도 MotionLayout을 사용하는 것을 적극 권장하는 것 같다. 앞으로 MotionLayout에서 추가된 기능이 나올 것 같으니 지금부터 프로젝트에 도입하는 것을 목표로 해야겠다.