#5 Android의 플로팅 윈도우: 윈도우 이동

Facebook Heads 및 기타 앱에서 사용하는 떠 있는 창을 만드는 방법에 대해 생각해 본 적이 있습니까? 앱에서 동일한 기술을 사용하고 싶었던 적이 있습니까? 간단합니다. 전체 과정을 안내해 드리겠습니다.

저는 Floating Apps의 저자입니다. Google Play에서 최초의 앱이자 800만 다운로드 이상을 기록한 가장 인기 있는 앱입니다. 앱을 개발한 지 6년이 지난 후, 나는 그것에 대해 조금 알게 되었습니다. 때로는 까다로워서 문서와 Android 소스 코드를 읽고 실험하는 데 몇 달을 보냈습니다. 수만 명의 사용자로부터 피드백을 받았고 Android 버전이 다른 여러 휴대폰에서 다양한 문제를 확인했습니다.

여기에서 내가 배운 것이 있습니다.

이 기사를 읽기 전에 Floating Windows on Android 4: Floating Window 을 읽는 것이 좋습니다.

이 기사에서는 헤더를 드래그하여 창을 이동 가능하게 만드는 방법을 알려 드리겠습니다.

레이아웃 업데이트



이전 기사에서 WindowManager 와 함께 LayoutParams 에 뷰를 추가하는 방법을 소개했습니다. 단, 창이 추가되는 순간부터 LayoutParams에 대한 변경 사항은 반영되지 않습니다.

변경 사항을 적용하려면 updateViewLayoutWindowManager 메서드를 호출해야 합니다. 따라서 창 위치를 변경하기 위해 Window 클래스에 대해 두 가지 추가 메서드를 구현해야 합니다.

private fun setPosition(x: Int, y: Int) {  
  windowParams.x = x  
  windowParams.y = y  
  update()  
}  

private fun update() {  
  try {  
    windowManager.updateViewLayout(rootView, windowParams)  
  } catch (e: Exception) {  
    // Ignore exception for now, but in production, you should have some  
    // warning for the user here.  
  }  
}

물론 창의 크기, 불투명도 등을 변경하는 데에도 동일한 원리를 사용할 수 있습니다.

사용자 정의 OnTouchListener



창을 이동하기 위해 초기 지점의 이동을 추적할 수 있는 사용자 지정OnTouchListener을 구현해 보겠습니다. 또한 기본 기능인 클릭 및 긴 클릭을 유지하여 예상되는 시스템과 같은 방식으로 작동하도록 합시다.

우리의 구현은 멀티터치를 지원하지 않지만 창을 이동하는 방법을 보여주기에는 충분합니다.

새로운DraggableTouchListener 소스 코드는 다음과 같습니다.

class DraggableTouchListener(
    context: Context,
    private val view: View,
    private val initialPosition: () -> Point,
    private val positionListener: (x: Int, y: Int) -> Unit
) : View.OnTouchListener {

  private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
  private val longClickInterval = ViewConfiguration.getLongPressTimeout()
  private var pointerStartX = 0
  private var pointerStartY = 0
  private var initialX = 0
  private var initialY = 0
  private var moving = false
  private var longClickPerformed = false
  private var timer: Timer? = null

  init {
    view.setOnTouchListener(this)
  }

  private fun scheduleLongClickTimer() {
    if (timer == null) {
      timer = Timer()
      timer?.schedule(timerTask {
          if (!moving && !longClickPerformed) {
              view.post {
                  view.performLongClick()
              }
              longClickPerformed = true
          }
          cancelLongClickTimer()
      }, longClickInterval.toLong())
    }
  }

  private fun cancelLongClickTimer() {
    timer?.cancel()
    timer = null
  }

  override fun onTouch(view: View, motionEvent: MotionEvent): Boolean {

    when (motionEvent.action) {

        MotionEvent.ACTION_DOWN -> {
            pointerStartX = motionEvent.rawX.toInt()
            pointerStartY = motionEvent.rawY.toInt()
            with(initialPosition()) {
                initialX = x
                initialY = y
            }
            moving = false
            longClickPerformed = false
            scheduleLongClickTimer()
        }

        MotionEvent.ACTION_MOVE -> {
            if (!longClickPerformed) {
                val deltaX = motionEvent.rawX - pointerStartX
                val deltaY = motionEvent.rawY - pointerStartY
                if (moving || hypot(deltaX, deltaY) > touchSlop) {
                    cancelLongClickTimer()
                    positionListener(initialX + deltaX.toInt(), initialY + deltaY.toInt())
                    moving = true
                }
            }
        }

        MotionEvent.ACTION_UP -> {
            cancelLongClickTimer()
            if (!moving && !longClickPerformed) {
                view.performClick()
            }
        }

    }

    return true
  }

}

좀 더 쉽게 사용할 수 있도록 약간의 Kotlin 매직(확장 기능)을 추가할 수 있습니다.

fun View.registerDraggableTouchListener(  
  initialPosition: () -> Point,  
  positionListener: (x: Int, y: Int) -> Unit  
) {  
  DraggableTouchListener(context, this, initialPosition, positionListener)  
}

이론적으로 위의 코드로 무엇이든 이동할 수 있습니다. 보기는 여기에서 트리거 역할만 합니다.

창문을 움직이자



우리가 해야 할 일은 새로 생성된DraggableTouchListener 뷰를 창 이동 핸들로 사용하려는 뷰에 연결하는 것입니다.

데스크톱 운영 체제의 경우 일반적으로 창의 제목 표시줄이 이러한 용도로 사용되므로 동일한 추세를 따르도록 하겠습니다.

rootView.findViewById<View>(R.id.window_header).registerDraggableTouchListener(  
  initialPosition = { Point(windowParams.x, windowParams.y) },  
  positionListener = { x, y -> setPosition(x, y) }  
)

그리고 그게 다야!

경계



프로덕션 용도에서는 일부 경계를 구현해야 하므로 창이 화면 밖으로 너무 많이 이동할 수 없습니다.

Floating Apps에서는 사용자가 화면 영역 외부로 창의 50%를 이동할 수 있도록 합니다.

결과



창의 제목 표시줄을 누르고 원하는 곳으로 이동하세요!



소스 코드



이 기사의 전체 소스 코드는 available on Github 입니다.

계속 지켜봐 주세요



Android 개발에 대해 자세히 알고 싶으십니까? Twitter에서 저( )와 Localazy( )를 팔로우하거나 좋아요Localazy on Facebook를 하세요.

시리즈



이 문서는 Android의 Floating Windows 시리즈의 일부입니다.
  • Floating Windows on Android 1: Jetpack Compose & Room
  • Floating Windows on Android 2: Foreground Service
  • Floating Windows on Android 3: Permissions
  • Floating Windows on Android 4: Floating Window
  • Floating Windows on Android 5: Moving Window
  • Floating Windows on Android 6: Keyboard Input
  • Floating Windows on Android 7: Boot Receiver
  • Floating Windows on Android 8: The Final App
  • Floating Windows on Android 9: Shortcomings
  • Floating Windows on Android 10: Tips & Tricks
  • 좋은 웹페이지 즐겨찾기