# Android의 부동 창 4개: 부동 창

당신은 페이스북과 다른 응용 프로그램에서 사용하는 부동 창을 어떻게 만드는지 생각해 본 적이 있습니까?너는 너의 응용 프로그램에서 같은 기술을 사용할 생각을 했니?이것은 매우 간단하니, 나는 전과정을 너를 지도할 것이다.
저는 Floating Apps의 작가입니다.구글플레이의 첫 번째 동종 앱이자 800만 명이 넘는 다운로드를 기록한 가장 인기 있는 앱이다.6년간의 응용 개발을 거쳐 나는 그것에 대해 약간의 이해를 얻었다.때때로 매우 까다롭다. 나는 몇 달 동안 문서와 안드로이드 소스 코드를 읽고 실험을 했다.나는 수만 명의 사용자로부터 피드백을 받았고 서로 다른 버전의 안드로이드 휴대전화에서 각종 문제를 보았다.
이것은 내가 가는 길에 배운 것이다.
본문을 읽기 전에 먼저 읽는 것을 권장합니다Floating Windows on Android 3: Permissions.
본고에서, 나는 너에게 다른 응용 프로그램에 실제 부동 창을 표시하는 방법을 가르쳐 줄 것이다.

WindowManager

WindowManager는 창 관리자와 통신할 수 있는 응용 프로그램의 인터페이스입니다.
Android의 창 관리자는 화면에서 보이는 모든 것을 처리할 수 있습니다.다행히도, 보기를 직접 추가하고 삭제할 수 있습니다. 만약 우리가 정확한 매개 변수를 사용하여 보기를 추가한다면, 우리는 부동 창이 있습니다!
// Obtain WindowManager
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager

// Add view
windowManager.addView(rootView, windowParams)

// Remove view
windowManager.removeView(rootView)

레이아웃 매개변수


위의 간단한 소스 코드 예시에서 우리는 addView 를 호출했고 두 번째 매개 변수는 windowParams 유형의 WindowManager.LayoutParams 이다.정확한 매개 변수는 무엇입니까?
다음은 다음과 같습니다.
private val windowParams = WindowManager.LayoutParams(  
  0,  // Width
  0,  // Height
  0,  // X position
  0,  // Y position
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {  
    WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY  
  } else {  
    WindowManager.LayoutParams.TYPE_PHONE  
  },  
  WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or  
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or  
            WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or  
            WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,  
  PixelFormat.TRANSLUCENT  
)
앞의 네 개의 매개 변수는 창의 위치와 크기를 지정합니다.일반적으로, 나는 클래스 단계에서 레이아웃 파라미터를 정의하는 경향이 있기 때문에, 나는 이 네 개의 파라미터를 0으로 유지하고 잠시 후에 다시 계산할 것이다.기술적으로 말하자면, 우리는 그것들을 제자리에 설정할 수 있지만, 나는 차라리 이 코드를 변수 값 밖으로 옮기고 싶다.계산의 경우 다음과 같은 방법으로 화면 크기를 고려할 수도 있습니다.
private fun getCurrentDisplayMetrics(): DisplayMetrics {  
    val dm = DisplayMetrics()  
    windowManager.defaultDisplay.getMetrics(dm)  
    return dm  
}

// Set LayoutParams for a window that is placed in the center
// of the screen. 
private fun calculateSizeAndPosition(  
  params: WindowManager.LayoutParams,  
  widthInDp: Int,  
  heightInDp: Int  
) {  
  val dm = getCurrentDisplayMetrics()  
  // We have to set gravity for which the calculated position is relative.  
  params.gravity = Gravity.TOP or Gravity.LEFT  
  params.width = (widthInDp * dm.density).toInt()  
  params.height = (heightInDp * dm.density).toInt()  
  params.x = (dm.widthPixels - params.width) / 2  
  params.y = (dm.heightPixels - params.height) / 2  
}
다음 매개 변수는 창의 유형입니다.이 점은 매우 관건적이다. 정확한 유형을 사용하면 안드로이드가 우리의 보기를 어떻게 처리해야 하는지 알려준다.Android O에 앞서 권장되는 유형은 WindowManager.LayoutParams.TYPE_PHONE 입니다.다른 창 우선순위를 실현하기 위해 혼합해서 사용할 수 있는 다른 유형도 있다.그러나 Android O에서는 사용할 수 없기 때문에 사용하지 않는 것이 좋습니다.Android O에서 권장하는 유형은 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY이고 다른 유형은 사용합니다.
다음은 flags입니다. 이것들도 매우 중요합니다. 왜냐하면 이것은 안드로이드에게 우리의 창이 터치, 버튼, 버튼 입력과 어떻게 상호작용하기를 원하는지 알려주기 때문입니다.
  • FLAG_LAYOUT_NO_LIMITS - 창을 화면 외부로 확장할 수 있습니다.이것은 선택할 수 있지만, 나는 그것을 사용하고 스스로 한계를 계산하는 경향이 있다.
  • FLAG_NOT_FOCUSABLE - 창에 키 입력 초점이 영원히 표시되지 않으므로 사용자는 키 또는 다른 버튼 이벤트를 보낼 수 없습니다.포커스 가능한 모든 창으로 이동합니다.이것은 우리가 부동 창 뒤의 응용 프로그램을 제어할 수 있기 때문에 매우 중요하다.
  • FLAG_NOT_TOUCH_MODAL - 창 밖의 모든 포인터 이벤트를 뒤에 있는 창으로 보낼 수 있습니다.
  • FLAG_WATCH_OUTSIDE_TOUCH - 창 밖에서 발생한 터치 이벤트를 수신합니다.이 점은 미래에 매우 중요하다.
  • 마지막 매개변수는 픽셀 형식입니다.안드로이드에 반투명을 지원하는 형식을 선택하라고 알려주기 때문에 PixelFormat.TRANSLUCENT 추천합니다.창문 부분을 투명하게 하는 것이 재미있다.

    배치


    불행하게도, 우리는 Jetpack Compose를 부동 창에 사용할 수 없습니다. 왜냐하면 우리는 보기만 필요하고 활동이 없기 때문에 생명주기가 없습니다.
    그러나 우리는 오래된 양호한 레이아웃 XML을 사용할 수 있다.이를 사용하려면 LayoutInflater 인스턴스를 가져와 뷰를 확장해야 합니다.
    val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
    
    // The second parameter is null as we don't have any ViewGroup
    // to attach our newly created view to. 
    val rootView = layoutInflater.inflate(R.layout.window,  null)
    
    프레젠테이션 목적으로 의존 LinearLayout.그것은 창 레이아웃의 구조를 잘 보여 주었다.부동 응용 프로그램에서 나는 창의 기본 레이아웃을 사용하고 내용을 동적 삽입하지만, 창 유형이 하나이기 때문에 레이아웃 파일은 하나일 수 있습니다.
    <?xml version="1.0" encoding="utf-8"?>  
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
      android:layout_width="match_parent"  
      android:layout_height="match_parent"  
      android:orientation="vertical"  
      android:weightSum="1">  
    
      <LinearLayout android:id="@+id/window_header"
        android:layout_width="match_parent"  
        android:layout_height="wrap_content"  
        android:background="@color/windowHeader"  
        android:orientation="horizontal"  
        android:weightSum="1">  
    
        <TextView android:id="@+id/window_title"  
          android:layout_width="0dp"  
          android:layout_height="wrap_content"  
          android:layout_gravity="start|center_vertical"  
          android:layout_weight="1"  
          android:paddingStart="8dp"  
          android:paddingTop="4dp"  
          android:paddingEnd="8dp"  
          android:paddingBottom="4dp"  
          android:text="@string/add_note"  
          android:textColor="@color/windowHeaderText" />  
    
        <ImageButton android:id="@+id/window_close"  
          android:layout_width="24dp"  
          android:layout_height="24dp"  
          android:layout_gravity="end|center_vertical"  
          android:layout_margin="4dp"  
          android:background="?android:attr/selectableItemBackground"  
          android:padding="0dp"  
          android:src="@drawable/baseline_highlight_off_black_24"  
          android:tint="@color/windowHeaderClose"  
          android:tintMode="src_in" />  
    
     </LinearLayout>  
    
     <LinearLayout android:id="@+id/window_content"  
       android:layout_width="match_parent"  
       android:layout_height="0dp"  
       android:layout_weight="1"  
       android:background="@color/windowBody"  
       android:orientation="horizontal">  
    
       <EditText  android:id="@+id/content_text"  
         android:layout_width="0dp"  
         android:layout_height="wrap_content"  
         android:layout_weight="1" />  
    
       <ImageButton  android:id="@+id/content_button"  
         android:layout_width="32dp"  
         android:layout_height="32dp"  
         android:layout_gravity="end|center_vertical"  
         android:layout_margin="4dp"  
         android:background="?android:attr/selectableItemBackground"  
         android:src="@drawable/baseline_send_black_24"  
         android:tint="@color/windowSend"  
         android:tintMode="src_in" />  
    
      </LinearLayout>  
    
    </LinearLayout>
    
    나는 좋은 디자이너가 아니기 때문에 몇 개의 반랜덤 색깔의 창 디자인만 선택했다.
    <resources>  
     <color name="windowHeader">#FF888888</color>  
     <color name="windowHeaderText">#FFFFFFFF</color>  
     <color name="windowHeaderClose">#FFEE7777</color>  
     <color name="windowBody">#FFDDDDDD</color>  
     <color name="windowSend">#FF448844</color>  
    </resources>
    
    왈라, 나의 디자인 기교의 결과:

    부동 창


    우리는 이미 보기, 레이아웃 매개 변수, 창 관리자를 준비했다.이제 코드를 함께 놓으면 우리의 첫 번째 부동 창이 준비됩니다!
    전체 논리를 봉인하기 위해 Window 클래스를 만듭니다.전체 소스 코드:
    class Window(private val context: Context) {
    
      private val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
      private val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
      private val rootView = layoutInflater.inflate(R.layout.window, null)
    
      private val windowParams = WindowManager.LayoutParams(
          0,
          0,
          0,
          0,
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
              WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
          } else {
              WindowManager.LayoutParams.TYPE_PHONE
          },
          WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS or
                  WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
                  WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
                  WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
          PixelFormat.TRANSLUCENT
      )
    
      private fun getCurrentDisplayMetrics(): DisplayMetrics {
        val dm = DisplayMetrics()
        windowManager.defaultDisplay.getMetrics(dm)
        return dm
      }
    
      private fun calculateSizeAndPosition(
          params: WindowManager.LayoutParams,
          widthInDp: Int,
          heightInDp: Int
      ) {
        val dm = getCurrentDisplayMetrics()
        // We have to set gravity for which the calculated position is relative.
        params.gravity = Gravity.TOP or Gravity.LEFT
        params.width = (widthInDp * dm.density).toInt()
        params.height = (heightInDp * dm.density).toInt()
        params.x = (dm.widthPixels - params.width) / 2
        params.y = (dm.heightPixels - params.height) / 2
      }
    
      private fun initWindowParams() {
        calculateSizeAndPosition(windowParams, 300, 80)
      }
    
      private fun initWindow() {
        // Using kotlin extension for views caused error, so good old findViewById is used
        rootView.findViewById<View>(R.id.window_close).setOnClickListener { close() }
        rootView.findViewById<View>(R.id.content_button).setOnClickListener {
          Toast.makeText(context, "Adding notes to be implemented.", Toast.LENGTH_SHORT).show()
        }
      }
    
      init {
        initWindowParams()
        initWindow()
      }
    
      fun open() {
        try {
          windowManager.addView(rootView, windowParams)
        } catch (e: Exception) {
          // Ignore exception for now, but in production, you should have some
          // warning for the user here.
        }
      }
    
      fun close() {
        try {
          windowManager.removeView(rootView)
        } catch (e: Exception) {
          // Ignore exception for now, but in production, you should have some
          // warning for the user here.
        }
      }
    
    }
    

    부동 어플리케이션


    단순 부동 창 뒤의 논리가 얼마나 복잡한지에 관심이 있다면 Floating Apps 배경 지식을 참고하세요.
    많은 미니 응용 프로그램이 있다.각 파일에는 로컬 이름, 내부 식별자, 아이콘, 필수 권한 목록, 시작 기본 설정, 창 사전 설정 등의 필수 정보가 들어 있는 헤더 파일이 있습니다. 헤더 파일은 메모리에 저장되어 사용 가능한 응용 프로그램을 나열하는 데 사용됩니다.
    프로그램이 시작될 때, 헤더 파일에서 온 정보는 프로그램을 만드는 실례와 Window 실례에 사용됩니다.
    각 응용 프로그램은 확장Application되어 라이프 사이클 관리, 메뉴 정의, 처리 창 크기, 위치, 최소화 등 기본적인 기능을 제공합니다. 또한 Application 클래스는 부동 기술의 많은 단점을 자동으로 해결할 수 있습니다.다음 문장에서, 나는 당신에게 이 모든 문제에 관한 더 많은 내용을 소개할 것입니다.
    또한 실행 중인 모든 응용 프로그램은 활성 창의 글로벌 목록에 등록되어 있습니다. 이것은 많은 감동적인 기능을 허용합니다. - 모든 활성 응용 프로그램을 열거하고, 그 중 일부 응용 프로그램만 한 번 실행하고, 실행 중인 응용 프로그램을 다시 실행하는 것이 아니라 전체 응용 프로그램의 상태 갱신 등입니다.
    보시다시피 거대한 논리가 있을 수 있습니다.일반적인 안드로이드 응용 프로그램은 시스템에 의존하여 이러한 기능을 제공하기 때문에, 나는 처음부터 부동 응용 프로그램에 이 모든 기능을 다시 써야 한다.

    결과와 결핍 부분


    아래의 애니메이션에서 보듯이 우리는 새로운 부동 창을 열었고 심지어 프로그램을 전환했다.창문은 여전히 그곳에 있다. 그들의 위에서 볼 수 있다.
    그러나 다음과 같은 두 가지 주요 문제가 있습니다.
  • 창은 화면 중앙에 있으므로 어디에서든 이동할 수 없습니다.
  • 텍스트를 입력할 수 없습니다.키보드가 활성화되지 않고 표시되지 않습니다.
  • 우리는 다음 문장에서 이 두 문제를 토론할 것이다.

    소스 코드


    본고의 모든 원본 코드는 available on Github이다.

    기대해주세요.


    안드로이드 개발에 대한 더 많은 정보를 원하십니까?트위터에서 저()와 Localazy(), 또는 유사Localazy on Facebook를 팔로우하세요.

    시리즈


    이 글은 Android 시리즈 부동 창의 일부입니다.
  • 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
  • 좋은 웹페이지 즐겨찾기