34
37

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

オーバーレイ表示したViewをドラッグ&ドロップする方法と、はまったポイント

Last updated at Posted at 2016-07-29

目的

overlay_2.gif

こんな風にオーバーレイ表示させたViewをドラッグ&ドロップで好きな位置に移動させたい。
かつ、他のレイヤーのタッチイベントも正常に動作させたい。

  • このViewの名前を便宜上movableViewと呼びます。
  • 以下で紹介するコードはKotlinで書いています。ポイントは基本的に変わらないので適宜読み替えてください。
  • オーバーレイの方法とViewのドラッグ&ドロップの方法は知っているという前提で話を進めちゃいます。

そもそもの話

そもそもどうやってオーバーレイやるの?って話はこちらを参考にしました。
画面上にアプリの情報を常時表示する

そもそもドラッグ&ドロップでViewを移動するにはどうするの?って話はこちらを参考に。
こんなに簡単だとは思わなかった!Viewのドラッグ方法

はまったぞいや。

上記の二つを組み合わせたようなものをやりたいんだけど、はまった。

単純にViewをオーバーレイさせるだけなら、
Viewのタッチイベントと他のレイヤーのタッチイベントを両方拾うことができる。

普通にmovableViewを置くだけだと、画面全体で移動させることはできなかった。

今回はViewを画面全体で動かしたいので、画面全体を覆うような親ビューの子にmovableViewを置いた。
そうすると、今度は他のレイヤーのタッチイベントが拾えなくなる。

まったく同じ悩みを持っている記事があったけど、結局明確な解決策は載ってなかった。
Androidでoverlay表示させたViewにタッチイベントを消費させないようにする

これを解決するためのポイントは以下。

・WindowManager#addView()でオーバーレイに適応される領域はaddViewしたときのViewの領域だけ。
・オーバーレイ表示領域はGravityの指定がなければ画面の中心になる。(Gravity.CENTERと同じ)
・addViewしたあとにViewを移動してもオーバーレイ領域は移動しない

解決法

結論から言うと、Viewの位置を移動させるのではなく、
オーバーレイの表示領域自体を移動させることで解決しました。

まずはコードから。必要な部分だけ抜粋。

OverlayService.kt

class OverlayService : Service() {

// オーバーレイ表示させるビュー
val overlayView: ViewGroup by lazy { LayoutInflater.from(this).inflate(R.layout.timer_overlay_layout, null) as ViewGroup }

// WindowManager
val windowManager: WindowManager by lazy { applicationContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager }

// WindowManagerに設定するレイアウトパラメータ
var params: WindowManager.LayoutParams? = null

// ディスプレイのサイズを格納する
val displaySize: Point by lazy {
    val display = windowManager.defaultDisplay
    val size = Point()
    display.getSize(size)
    size
}

// ロングタップ判定用
var isLongClick: Boolean = false

// 中略 //

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {

    overlayView.apply(clickListener())
    // オーバーレイViewの設定をする
    params = WindowManager.LayoutParams(
            WindowManager.LayoutParams.WRAP_CONTENT,
            WindowManager.LayoutParams.WRAP_CONTENT,
            WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
                    WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
                    WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
                    WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
            PixelFormat.TRANSLUCENT)

  // ここでビューをオーバーレイ領域に追加する
    windowManager.addView(overlayView, params)

    return START_STICKY
}

private fun clickListener(): View.() -> Unit {
    return {
        setOnLongClickListener { view ->
       // ロングタップ状態にする
            isLongClick = true
            // ロングタップ状態が分かりやすいように背景色を変える
            view.setBackgroundResource(R.color.selectedColor)
            false
        }.apply {
            setOnTouchListener { view, motionEvent ->

          // タップした位置を取得する
                val x = motionEvent.rawX.toInt()
                val y = motionEvent.rawY.toInt()

                when (motionEvent.action) {

           // Viewを移動させてるときに呼ばれる
                    MotionEvent.ACTION_MOVE -> {
                        if (isLongClick) {

                // 中心からの座標を計算する
                            val centerX = x - (displaySize.x / 2)
                            val centerY = y - (displaySize.y / 2)

                            // オーバーレイ表示領域の座標を移動させる
                            params?.x = centerX
                            params?.y = centerY

                // 移動した分を更新する
                            windowManager.updateViewLayout(overlayView, params)
                        }
                    }
                    
                    // Viewの移動が終わったときに呼ばれる
                    MotionEvent.ACTION_UP -> {
                        if (isLongClick) {

                            // 背景色を戻す
                            view.setBackgroundResource(android.R.color.transparent)
                        }
                        isLongClick = false
                    }
                }
                false
            }
        }
    }
}

長くなってしまったけど、ポイントは二つだけです。

points.kt
// ディスプレイのサイズを格納する
val displaySize: Point by lazy {
    val display = windowManager.defaultDisplay
    val size = Point()
    display.getSize(size)
    size
}

// タップした位置を取得する
val x = motionEvent.rawX.toInt()
val y = motionEvent.rawY.toInt()

// 中心からの座標を計算する
val centerX = x - (displaySize.x / 2)
val centerY = y - (displaySize.y / 2)

// オーバーレイ表示領域の座標を移動させる
params?.x = centerX
params?.y = centerY

// 移動した分を更新する
windowManager.updateViewLayout(overlayView, params)

ポイント1:中心からの移動量を計算して、paramsのxとyに入れる

注意すべきは、タップ位置計算の座標系と、オーバーレイ領域位置計算の座標系が異なる点。

タップ位置はディスプレイの左上が座標の起点(数学っぽく言うと(0,0))
オーバーレイ領域位置はディスプレイの中心の座標が起点になってる。
paramsのxとyにはオーバーレイの座標系を元にした座標が入ります。

これらをまとめるとこんなイメージ
0999541b-ed7b-dc27-d738-fd525be22412.png

そこで、タップ位置の座標系とオーバーレイの座標系を合わせるために
val centerX = x - (displaySize.x / 2)
val centerY = y - (displaySize.y / 2)

で、中心から移動させたい移動量を計算します。

ポイント2:移動させた分をupdateViewLayoutで更新する

ただparamsを変えただけだとオーバーレイの領域は変わらないので、
WindowManager#updateViewLayout()を呼んであげて、描画の更新をします。

表示される領域そのものが移動しているので、Viewの位置を移動させる必要はありません。
(Viewの位置は表示領域の左上を起点にして計算されるから)

Viewを移動させるよりもシンプルな実装で、実現することができましたね!

34
37
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
34
37

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?