어플리케이션의 음원 소스 기반이 유튜브이므로,
최대한 유튜브의 디자인을 따라하고 싶었습니다.
드래그, 또는 터치를 통해 동영상 플레이어의 창을 내리고 올릴 수 있어야 합니다.
<MainActivity.xml>
<?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"
android:id="@+id/main_motion_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
app:layoutDescription="@xml/main_scene">
<FrameLayout
android:id="@+id/player_fragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/bottom_navigation_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
// 관계가 없는 코드이므로 접어놓겠습니다.
<frameLayout...>
<LinearLayout...>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:background="@color/blue_background"
app:itemIconTint="@color/white"
app:itemTextColor="@color/white"
app:labelVisibilityMode="labeled"
app:menu="@menu/bottom_navi_item"/>
</androidx.constraintlayout.motion.widget.MotionLayout>
액티비티 전체를 motionLayout으로 감싸줘야 합니다.
동영상 플레이어 프레그먼트를 넣을 frameLayout, 액티비티 밑에 위치하는 bottom NavigationView가 존재합니다.
motionLayout은 레이아웃의 모든 모션 설명을 포함하는 MotionScene xml 파일을 필요로 합니다.
xml에 main_scene.xml 파일을 만들어 주었습니다.
<main_scene.xml>
<?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"
xmlns:app="http://schemas.android.com/apk/res-auto"
>
// 동영상 플레이어가 최소화된 상태 "start"
<ConstraintSet android:id="@+id/start">
...
<Constraint android:id="@+id/bottom_navigation_view"
motion:layout_constraintEnd_toEndOf="parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:visibilityMode="ignore"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toStartOf="parent" />
</ConstraintSet>
// 동영상 플레이어가 최대화가 된 상태 "end"
<ConstraintSet android:id="@+id/end">
...
<Constraint android:id="@id/bottom_navigation_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:translationY="56dp" // 56dp 만큼 y축으로 내리기
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toStartOf="parent"/>
<Constraint
android:id="@+id/player_fragment"
android:layout_width="match_parent"
android:layout_height="0dp"
motion:layout_constraintTop_toTopOf="parent"
motion:layout_constraintBottom_toBottomOf="parent"/>
</ConstraintSet>
<Transition
app:constraintSetEnd="@id/end"
app:constraintSetStart="@+id/start"
app:duration="1000"
motion:layoutDuringTransition="honorRequest"
/>
</MotionScene>
동영상 플레이어 역할을 하는 fragment_player.xml 파일을 만들어 주었습니다.
<fragment_player.xml>
// 포스팅과 관련이 없는 코드는 ...로 표시하였습니다.
<?xml version="1.0" encoding="utf-8"?>
<com.myFile.Transpose.PlayerMotionLayout
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="@xml/fragment_player_scene"
android:id="@+id/player_motion_layout">
// 터치할 영역이 되는 부분 mainContainerLayout
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/mainContainerLayout"
android:layout_width="0dp"
android:layout_height="250dp"
android:background="@color/blue_background"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
// 영상을 재생하는 exoPlayer
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/playerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@id/mainContainerLayout"
app:layout_constraintStart_toStartOf="@id/mainContainerLayout"
app:layout_constraintTop_toTopOf="@id/mainContainerLayout"
app:resize_mode="fill"
app:show_buffering="always"/>
<ImageView... />
<ProgressBar... />
// 동영상 플레이어가 최소화가 되었을 때 띄울 x 버튼
<ImageView
android:id="@+id/bottomPlayerCloseButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:padding="12dp"
android:src="@drawable/ic_baseline_close_24"
app:layout_constraintBottom_toBottomOf="@id/mainContainerLayout"
app:layout_constraintEnd_toEndOf="@id/mainContainerLayout"
app:layout_constraintTop_toTopOf="@id/mainContainerLayout" />
// 동영상 플레이어가 최소화가 되었을 때 띄울 재생 버튼
<ImageView
android:id="@+id/bottomPlayerPauseButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"
android:background="?attr/selectableItemBackground"
android:src="@drawable/ic_baseline_pause_24"
app:layout_constraintBottom_toBottomOf="@id/mainContainerLayout"
app:layout_constraintStart_toEndOf="@id/bottomTitleTextView"
app:layout_constraintEnd_toStartOf="@id/bottomPlayerCloseButton"
app:layout_constraintTop_toTopOf="@id/mainContainerLayout" />
// 동영상 플레이어가 최소화가 되었을 때 띄울 영상 타이틀
<TextView
android:id="@+id/bottomTitleTextView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="12dp"
android:ellipsize="end"
android:maxLines="1"
android:singleLine="true"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="@id/bottomPlayerCloseButton"
app:layout_constraintEnd_toStartOf="@id/bottomPlayerPauseButton"
app:layout_constraintStart_toEndOf="@id/playerView"
app:layout_constraintTop_toTopOf="@id/bottomPlayerCloseButton"
tools:text="Title." />
// 영상의 세부정보, 재생 목록을 띄워줄 scrollView
<androidx.core.widget.NestedScrollView...
</androidx.core.widget.NestedScrollView>
</com.myFile.Transpose.PlayerMotionLayout>
동영상 플레이어는 정해진 위치를 드래그 할 때에만 최소화가 되어야 합니다.
기본적인 motionLayout에서는 해당 설정이 불가능 하므로, 따로 PlayerMotionLayout 클래스를 만들어야 합니다.
우선 모션 설명을 위한 fragment_player_scene.xml 을 만들어 주었습니다.
<fragment_player_scene.xml>
<?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"
>
// 동영상 플레이어 최대화 상태 "end"
<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="200"
>
// 동영상 플레이어가 최소화 되었을 때만 보여주기 위한 표준 속성 설정
<KeyFrameSet>
<KeyAttribute
motion:motionTarget="@+id/bottomPlayerPauseButton"
motion:framePosition="10"
android:alpha="0.0" />
<KeyAttribute
motion:motionTarget="@+id/bottomTitleTextView"
motion:framePosition="10"
android:alpha="0.0" />
<KeyAttribute
motion:motionTarget="@+id/bottomPlayerCloseButton"
motion:framePosition="10"
android:alpha="0.0" />
<KeyPosition
motion:motionTarget="@+id/playerView"
motion:framePosition="10"
motion:keyPositionType="deltaRelative"
motion:curveFit="linear"
motion:percentWidth="1"
motion:percentX="1" />
</KeyFrameSet>
// 드래그를 통해 모션 상태를 바꿈
<OnSwipe
motion:touchAnchorId="@+id/mainContainerLayout"
motion:touchAnchorSide="bottom"
motion:nestedScrollFlags="disableScroll"
/>
</Transition>
// 동영상 플레이어 최소화 상태 "start"
<ConstraintSet android:id="@+id/start">
// 터치할 영역
<Constraint
android:id="@+id/mainContainerLayout"
android:layout_width="0dp"
android:layout_height="56dp" //최소화 되었을 때, 플레이어의 height 는 56dp로 설정
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent"
motion:layout_constraintVertical_bias="1.0" />
// 영상이 재생되는 부분
<Constraint
android:id="@+id/playerView"
android:layout_width="0dp"
android:layout_height="0dp"
motion:visibilityMode="ignore"
motion:layout_constraintBottom_toBottomOf="@id/mainContainerLayout"
motion:layout_constraintDimensionRatio="H, 1:2.3"
motion:layout_constraintStart_toStartOf="@id/mainContainerLayout"
motion:layout_constraintTop_toTopOf="@id/mainContainerLayout" />
</ConstraintSet>
// 동영상 플레이어 최대화 상태 "end"
<ConstraintSet android:id="@+id/end">
//영상이 재생되는 부분
<Constraint
android:id="@+id/playerView"
android:layout_width="0dp"
android:layout_height="0dp"
motion:visibilityMode="ignore"
motion:layout_constraintEnd_toEndOf="@id/mainContainerLayout"
motion:layout_constraintBottom_toBottomOf="@id/mainContainerLayout"
motion:layout_constraintTop_toTopOf="@id/mainContainerLayout"
motion:layout_constraintStart_toStartOf="@id/mainContainerLayout" />
// 터치할 영역
<Constraint
android:id="@+id/mainContainerLayout"
motion:layout_constraintEnd_toEndOf="parent"
android:layout_width="0dp"
android:layout_height="250dp"
motion:layout_constraintTop_toTopOf="parent"
motion:layout_constraintStart_toStartOf="parent" />
// 영상의 세부정보, 재생목록을 나타낼 scrollView
<Constraint
android:id="@+id/player_fragment_scroll_view"
android:layout_width="match_parent"
android:layout_height="0dp"
motion:layout_constraintTop_toBottomOf="@id/mainContainerLayout"
motion:layout_constraintBottom_toBottomOf="parent"/>
// 재생 버튼
<Constraint
android:id="@+id/bottomPlayerPauseButton"
motion:layout_constraintEnd_toEndOf="@id/mainContainerLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
motion:layout_constraintBottom_toBottomOf="@id/mainContainerLayout"
android:alpha="0"
android:layout_marginEnd="24dp"
android:visibility="gone"
motion:layout_constraintTop_toTopOf="@id/mainContainerLayout" />
// 닫힘 버튼
<Constraint
android:id="@+id/bottomPlayerCloseButton"
motion:layout_constraintEnd_toEndOf="@id/mainContainerLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
motion:layout_constraintBottom_toBottomOf="@id/mainContainerLayout"
android:alpha="0"
android:layout_marginEnd="24dp"
android:visibility="gone"
motion:layout_constraintTop_toTopOf="@id/mainContainerLayout" >
</Constraint>
</ConstraintSet>
</MotionScene>
모션이 바뀌는 트리거를 OnSwipe, 즉 드래그로 설정을 하였습니다.
처음에는 OnSwipe, OnClick 을 각각 트리거로 설정을 하였지만, 작동을 하지 않았습니다.
원인은 motionLayout이 내부적으로 터치 이벤트를 처리하고 소비하는 방식 때문이라 합니다.
Fragment에서는 모션 상태가 바뀌었을 때 Activity에게 그 사실을 전달해야 합니다.
<PlayerFragment.kt>
class PlayerFragment: Fragment() {
lateinit var activity: Activity
... 생략
override fun onAttach(context: Context) {
super.onAttach(context)
activity = context as Activity
}
private fun initMotionLayout() { // 프레그먼트의 state가 바뀌었을 때 activity에게 알려주기
binding.playerMotionLayout.setTransitionListener(object :
MotionLayout.TransitionListener {
override fun onTransitionStarted(p0: MotionLayout?, p1: Int, p2: Int) {
}
override fun onTransitionChange(
motionLayout: MotionLayout?,
startId: Int,
endId: Int,
progress: Float
) {
(activity).also { main ->
main.findViewById<MotionLayout>(mainBinding.mainMotionLayout.id).progress =
abs(progress)
}
}
override fun onTransitionCompleted(p0: MotionLayout?, p1: Int) {
// 화면이 축소된 상태에서는 엑소 플레이어의 컨트롤러 없애기
// 무시하셔도 됩니다.
if (binding.playerMotionLayout.currentState == R.id.start){
settingBottomPlayButton()
binding.playerView.useController = false
}
else
binding.playerView.useController = true
}
override fun onTransitionTrigger(p0: MotionLayout?, p1: Int, p2: Boolean, p3: Float) {
}
})
}
}
동영상 프레그먼트를 감싸는 PlayerMotionLayout.kt 파일을 만들어야 합니다.
<PlayerMotionLayout.kt>
package com.myFile.Transpose
import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.widget.ImageView
import androidx.constraintlayout.motion.widget.MotionLayout
class PlayerMotionLayout(context: Context, attributeSet: AttributeSet? = null) : MotionLayout(context, attributeSet) {
private var motionTouchStarted = false // 터치가 되었는지 알려주는 변수
private var startX: Float? = null
private var startY: Float? = null
private val mainContainerView by lazy { // 터치할 영역
findViewById<View>(R.id.mainContainerLayout)
}
private val hitRect = Rect()
init {
// 동영상 프레그먼트가 생성될 경우, 최대화 상태로 시작을 해야한다.
transitionToState(R.id.end) // end 상태로 transition
}
override fun onTouchEvent(event: MotionEvent): Boolean {
// 손가락이 화면으로 부터 떼어질 경우, motionTouchStarted 변수를 false로 초기화
when (event.actionMasked) {
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
motionTouchStarted = false
return super.onTouchEvent(event)
}
}
if (!motionTouchStarted) {
mainContainerView.getHitRect(hitRect) // 해당 뷰의 클릭 영역 hitRect에 저장
motionTouchStarted = hitRect.contains(event.x.toInt(), event.y.toInt())
}
return super.onTouchEvent(event) && motionTouchStarted
}
private val gestureListener by lazy {
object : GestureDetector.SimpleOnGestureListener() {
override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
mainContainerView.getHitRect(hitRect)
return hitRect.contains(e1.x.toInt(), e1.y.toInt())
}
}
}
private val gestureDetector by lazy {
GestureDetector(context, gestureListener)
}
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
return gestureDetector.onTouchEvent(event!!)
}
}
원하는 기능을 추가하기 위해서는 먼저 안드로이드의 터치 이벤트 구조를 이해해야 합니다.
참고 링크: https://readystory.tistory.com/185
View: 안드로이드 화면에 보이는 각 요소들
ViewGroup: View를 포함하여 화면에 적절히 배치하기 위한 일종의 컨테이너
activity -> viewGroup -> view 순서로 터치 이벤트를 전달합니다.
dispatchTouchEvent() -> onInterceptTouchEvent() 순으로 터치 이벤트를 다음 그룹에 전달해줄지 결정합니다.
onInterceptTouchEvent() 에서 true를 리턴할 경우 해당 그룹에서의 이벤트가 활성화 되며, 하위 view에 touchEvent를
전달하지 않습니다.
제 코드에 있어서 viewGroup은 motionLayout에 해당하며,
프레그먼트 내의 각 버튼들과 playerView, textView 등이 View에 해당합니다.
지정한 영역을 드래그 할 경우, 터치 이벤트는 viewGroup인 motionLayout에서 이루어져야 합니다.
onInterceptTouchEvent()를 통해 이벤트를 가로채고, touchEvent를 발생시켜 motionLayout의 상태를 바꿔야 합니다.
우선 드래그를 인식하기 위해 gestureDetector 객체를 이용합니다.
private val gestureListener by lazy {
object : GestureDetector.SimpleOnGestureListener() {
override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
mainContainerView.getHitRect(hitRect) // 터치할 영역 설정
return hitRect.contains(e1.x.toInt(), e1.y.toInt()) // 스크롤한 부분이 터치 영역에 포함되면 true를 리턴
}
}
}
private val gestureDetector by lazy {
GestureDetector(context, gestureListener)
}
onInterceptTouchEvent() 함수를 수정해줍니다.
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
return gestureDetector.onTouchEvent(event!!)
}
지정한 영역에서 드래그를 할 경우, gestureDetector.onTouchEvent(event!!) 를 리턴합니다.
이 때의 onTouchEvent() 매소드는 gestureDetector의 자체 매소드로,
view나 viewGroup의 onTouchEvent 매소드와 별개입니다.
스크롤 한 부분이 터치 영역에 포함이 되면, gestureDetector.onTouchEvent(event!!)는 true가 됩니다.
따라서 onInterceptTouchEvent() 는 true를 반환하게 되며, motionLayout이 터치 이벤트를 가로채게 됩니다.
마지막으로 터치 이벤트를 활성화할 onTouchEvent() 를 작성합니다.
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
motionTouchStarted = false
return super.onTouchEvent(event)
}
}
if (!motionTouchStarted) {
mainContainerView.getHitRect(hitRect) // 해당 뷰의 클릭 영역 hitRect에 저장
motionTouchStarted = hitRect.contains(event.x.toInt(), event.y.toInt()) // 영역에 해당되면 true
}
return super.onTouchEvent(event) && motionTouchStarted
}
터치 이벤트가 전달되어 지정된 영역과 일치할 경우 motionTouchStarted 변수가 true 가 되며
결과적으로 true를 리턴하게 되고, motionLayout의 상태가 바뀌게 됩니다.
위로 드래그 한 후, 손가락은 화면으로부터 떨어질 것이며 이 때 motionTouchStarted 변수를 false로 다시 바꿔줍니다.
흐름을 이해하기 위해 로그창을 많이 이용했었습니다.
어이 없는 이유로 작동이 되지 않아 고생을 많이 했었는데,
바로 gestureDetector.onTouchEvent(event!!) 을 로그창으로 확인하는 부분이었습니다..
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
Log.d("gestureDetector 확인","${gestureDetector.onTouchEvent(event!!)}")
return gestureDetector.onTouchEvent(event!!)
}
이럴 경우 로그창 이후 gestureDetector의 touchEvent는 초기화 되어 false를 반환하게 되고,
결과적으로 onInterceptTouchEvent() 는 false를 반환하게 됩니다.
별거 없어 보이지만.. 이 오류 덕분에 온갖 삽질을 했습니다. 이틀 날린 것 같아요.
동영상 플레이어 부분을 터치 하면 인식이 되어 여러 버튼들이 뜨지만,
드래그 할 경우 motionLayout이 작동하는 것을 볼 수 있습니다.
viewGroup에서 터치 이벤트를 잘 가로채는 것을 확인할 수 있습니다.
이제 드래그 뿐만 아니라, 터치할 경우에도 동영상 플레이어가 최대화 되는 기능을 추가해야 합니다.
처음에는 gestureDetector의 onDown 매소드를 추가했습니다.
onDown은 짧은 터치를 의미합니다.
private val gestureListener by lazy {
object : GestureDetector.SimpleOnGestureListener() {
override fun onDown(e: MotionEvent?): Boolean {
mainContainerView.getHitRect(hitRect)
if (playerMotionLayout.currentState == R.id.start) // 최소화 상태일 때
if (hitRect.contains(e!!.x.toInt(), e.y.toInt())) // 영역에 해당할 때
playerMotionLayout.transitionToEnd() // 최대화
return false // false를 반환
}
override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
mainContainerView.getHitRect(hitRect)
Log.d("onScroll","${hitRect.contains(e1.x.toInt(), e1.y.toInt())}")
return hitRect.contains(e1.x.toInt(), e1.y.toInt())
}
}
}
이 때 false 를 리턴하게 만들었는데,
최대화 상태일 때는 터치 이벤트가 View 영역에서 이루어져야 하기 때문입니다.
최소화 상태일 때만 터치를 하면, 최대화 상태가 되는 것을 기대하였습니다.
onDown이 호출되는 동시에, onScroll 또한 호출이 되는 문제가 있었습니다.
터치와 스크롤의 경계가 애매하여 생기는 문제라고 생각합니다.
드래그를 인식하는 부분과 분리하여 코드를 작성해야 한다는 생각이 들었습니다.
onInterceptTouchEvent() 매소드 이전에 호출되는 dispatchTouchEvent() 매소드를 이용하였습니다.
// 전역 변수로 선언
private val bottomPlayerPlayButton by lazy{ // 최소화 시 일시정지 버튼
findViewById<ImageView>(R.id.bottomPlayerPauseButton)
}
private val bottomPlayerCloseButton by lazy{ // 최소화 시 닫기 버튼
findViewById<ImageView>(R.id.bottomPlayerCloseButton)
}
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
bottomPlayerCloseButton.getHitRect(hitRect)
if (hitRect.contains(ev.x.toInt(), ev.y.toInt())) // 재생 버튼 클릭 시
return super.dispatchTouchEvent(ev)
bottomPlayerPlayButton.getHitRect(hitRect)
if (hitRect.contains(ev.x.toInt(), ev.y.toInt())) // 닫기 버튼 클릭 시
return super.dispatchTouchEvent(ev)
mainContainerView.getHitRect(hitRect)
if (hitRect.contains(ev.x.toInt(), ev.y.toInt())){
when (ev.action){
MotionEvent.ACTION_DOWN -> {
startX = ev.x
startY = ev.y
}
MotionEvent.ACTION_UP -> {
val endX = ev.x
val endY = ev.y
if (playerMotionLayout.currentState == R.id.start){
if (isAClick(startX!!, endX, startY!!, endY)){
playerMotionLayout.transitionToEnd()
motionTouchStarted = false
return true
}
}
}
}
}
return super.dispatchTouchEvent(ev)
}
private fun isAClick(startX: Float, endX: Float, startY: Float, endY: Float): Boolean {
val differenceX = Math.abs(startX - endX)
val differenceY = Math.abs(startY - endY)
return !(differenceX > 200 || differenceY > 200)
}
최소화 상태일 때 보이는 재생 버튼과 닫기 버튼은 View에 해당하므로,
해당 영역 클릭 시에는 View 까지 터치 이벤트를 전달해 주어야 합니다.
버튼을 제외한 나머지 영역을 클릭할 때 동영상 플레이어가 최소화 상태일 경우, 최대화 상태로 변환시켜줍니다.
클릭 시에는 dispatchTouchEvent() 에서, 드래그 시에는 viewGroup의 onTouchEvent() 에서 처리가 됩니다.
여기서 중요한 것은, motionTouchStarted 변수를 다시 false로 초기화 시켜줘야 한다는 것입니다.
dispatchTouchEvent() 에서 true를 리턴할 시, 하위 view로 터치 이벤트가 전달되지 않습니다.
onInterceptTouchEvent() 매소드 또한 호출되지 않고, 결과적으로 onTouchEvent()가 호출되지 않으며
motionTouchStarted 가 true 인 채로 남아있게 됩니다.
따라서 동영상 플레이어를 터치를 통해 최대화 시켰다가, 드래그가 아닌 뒤로 가기 버튼을 통해 최소화를 시킬 경우
터치 이벤트가 motionLayout에 의해 가로채지는 오류가 발생합니다.
전체 코드입니다.
<PlayerMotionLayout.kt>
class PlayerMotionLayout(context: Context, attributeSet: AttributeSet? = null) : MotionLayout(context, attributeSet) {
private var motionTouchStarted = false
private var startX: Float? = null
private var startY: Float? = null
private val mainContainerView by lazy {
findViewById<View>(R.id.mainContainerLayout)
}
private val bottomPlayerPlayButton by lazy{
findViewById<ImageView>(R.id.bottomPlayerPauseButton)
}
private val bottomPlayerCloseButton by lazy{
findViewById<ImageView>(R.id.bottomPlayerCloseButton)
}
private val hitRect = Rect()
init {
transitionToState(R.id.end)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
motionTouchStarted = false
return super.onTouchEvent(event)
}
}
if (!motionTouchStarted) {
mainContainerView.getHitRect(hitRect)
motionTouchStarted = hitRect.contains(event.x.toInt(), event.y.toInt())
}
return super.onTouchEvent(event) && motionTouchStarted
}
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
bottomPlayerCloseButton.getHitRect(hitRect)
if (hitRect.contains(ev.x.toInt(), ev.y.toInt()))
return super.dispatchTouchEvent(ev)
bottomPlayerPlayButton.getHitRect(hitRect)
if (hitRect.contains(ev.x.toInt(), ev.y.toInt()))
return super.dispatchTouchEvent(ev)
mainContainerView.getHitRect(hitRect)
if (hitRect.contains(ev.x.toInt(), ev.y.toInt())){
when (ev.action){
MotionEvent.ACTION_DOWN -> {
startX = ev.x
startY = ev.y
}
MotionEvent.ACTION_UP -> {
val endX = ev.x
val endY = ev.y
if (currentState == R.id.start){
if (isAClick(startX!!, endX, startY!!, endY)){
transitionToState(R.id.end)
motionTouchStarted = false
return true
}
}
}
}
}
return super.dispatchTouchEvent(ev)
}
private fun isAClick(startX: Float, endX: Float, startY: Float, endY: Float): Boolean {
val differenceX = Math.abs(startX - endX)
val differenceY = Math.abs(startY - endY)
return !(differenceX > 200 || differenceY > 200)
}
private val gestureListener by lazy {
object : GestureDetector.SimpleOnGestureListener() {
override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
mainContainerView.getHitRect(hitRect)
return hitRect.contains(e1.x.toInt(), e1.y.toInt())
}
}
}
private val gestureDetector by lazy {
GestureDetector(context, gestureListener)
}
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
return gestureDetector.onTouchEvent(event!!)
}
}
작동이 잘 되는 것을 확인할 수 있습니다
'안드로이드 프로젝트 > 유튜브 음정 조절 어플리케이션' 카테고리의 다른 글
[Android] #5 SearchView를 통한 검색창 구현 #2 (0) | 2023.03.20 |
---|---|
[Android] #4 searchView를 통한 검색창 구현 #1 (0) | 2023.03.20 |
[Android] #3 recyclerView 및 scrollView 스크롤 시 motionLayout이 작동하는 이슈 + visiblity 설정 (0) | 2023.03.20 |
[Android] #2 toolBar의 아이콘이 밀리는 이슈 해결(motionLayout) (0) | 2023.03.12 |
[Android] 유튜브 음정(키, 피치) 높낮이 조절 모바일 어플리케이션 (0) | 2023.01.27 |
댓글