Android上で4つのフローティングウィンドウ


あなたはFacebookの頭と他のアプリで使用されるこれらの浮動ウィンドウを作る方法を疑問に思ったことがありますか?今まであなたのアプリで同じ技術を使用したいですか?それは簡単だし、私は全体のプロセスをご案内します.
私はFloating Apps ; Googleの演劇と800万以上のダウンロードで最も人気のあるものにその種の最初のアプリ.アプリの開発の6年後、私はそれについて少し知っている.それは時々トリッキーです、そして、私はドキュメントとAndroidソースコードを読んで、数ヶ月を過ごしました.私は数万人のユーザーからのフィードバックを受け、さまざまなAndroidのバージョンで別の携帯電話上で様々な問題を参照してください.
私が道に沿って学んだことは、ここにあります.
この記事を読む前にFloating Windows on Android 3: Permissions .
この記事では、私はどのように他のアプリの上に実際のフローティングウィンドウを表示する方法をお教えします.

ウィンドウマネージャ

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 2番目のパラメータwindowParams 活字WindowManager.LayoutParams . 正しいparamsは何ですか?
はい、
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  
)
最初の4つのパラメータはウィンドウの位置とサイズを指定します.通常、私はクラスレベルで定義されたレイアウトパームを持つ傾向があります.技術的には、適切に設定することができますが、変数の代入の外でこのコードを動かします.計算のために、スクリーンサイズも考慮するためにこのような使い方を使うことができます.
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 . 他のタイプがあり、Windowsの異なる優先順位を達成するためにそれらをミックスすることが可能です.しかし、AndroidのOから利用できないので、私はそれを使用することをお勧めしません.アンドロイドOから推奨タイプWindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY , そして他のタイプはそれにフォールバック.
次は来るflags , そして、彼らが我々が我々のウインドウがタッチ、ボタンとキー入力と相互作用することを望む方法をAndroidに教えるので、彼らも重要です.
  • FLAG_LAYOUT_NO_LIMITS - ウィンドウを画面の外側に拡張できます.これはオプションですが、私はそれを使用し、自分の限界を計算する傾向がある.
  • FLAG_NOT_FOCUSABLE - ウィンドウがキー入力フォーカスを取得しないので、ユーザーはそれにキーまたは他のボタンのイベントを送信することはできません.その代わりに、フォーカス可能な窓がどんな後ろにあるかについて行きます.これは、我々はフローティングウィンドウの背後にあるアプリケーションを制御することができますので、非常に重要です.
  • FLAG_NOT_TOUCH_MODAL - ウィンドウの外のどんなポインタイベントもそれの後の窓に送られるのを許してください.
  • FLAG_WATCH_OUTSIDE_TOUCH - あなたのウインドウの外で起こるタッチのためにイベントを受けてください.これは将来重要です.
  • 最後のパラメータはピクセル形式です.おすすめしますPixelFormat.TRANSLUCENT Androidが半透明性をサポートする形式を選択するよう指示します.そして、部分的に透明なウィンドウを持つことは楽しい.

    レイアウト


    残念なことに、我々は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 . ウィンドウレイアウトがどのように構成されているかをよく示します.浮動小数点のアプリケーションでは、ウィンドウの基本レイアウトを使用して動的にコンテンツを挿入しますが、1つのウィンドウタイプだけがあるので、1つのレイアウトファイルを持つことができます.
    <?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>
    
    そして、私のデザインスキルの結果、Vo

    浮動窓


    ビュー、レイアウトパラメータ、ウィンドウマネージャを用意しました.さて、一緒にコードを入れてみましょう、我々の非常に最初のフローティングウィンドウは準備ができている!
    つくりましょう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 クラスは自動的に浮動小数点技術の欠点の多くを解決することができます.私は次の記事のいずれかでこれらのすべての問題についての詳細をお教えします.
    また、すべての実行中のアプリケーションは、アクティブなウィンドウのグローバルリストに登録されており、それは多くのエキサイティングな機能を可能にする-すべてのアクティブなアプリを一覧表示、一度だけそれらのいくつかを実行し、それらを再実行する代わりにアプリを実行して再起動、クロスアプリの状態をリフレッシュなど.
    ご覧の通り、広大な論理がある.通常のAndroidアプリは、これらの機能の多くを提供するシステムに依存して、私はゼロから浮動小数点のアプリのこれらの機能のすべてを書き直さなければならなかった.

    結果と行方不明部分


    以下のアニメーションで見ることができるように、我々は新しいフローティングウィンドウを開き、さらにスイッチのアプリ.窓はまだ彼らの上に見える.
    しかし、2つの主要な問題があります.
  • ウィンドウは画面の中央に留まり、どこにも移動できません.
  • テキストを入力することはできません.キーボードが起動していません.
  • 次の記事ではこれらの問題に取り組む予定です.

    ソースコード


    この記事の全体のソースコードはavailable on Github .

    滞在する


    Androidの開発についてもっと知りたいですか?さえずりの上で私と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