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

# 目的
![overlay_2.gif](https://qiita-image-store.s3.amazonaws.com/0/132699/031d9cc0-a052-7c0c-a943-5db97b507987.gif)

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

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

# そもそもの話

そもそもどうやってオーバーレイやるの？って話はこちらを参考にしました。
[画面上にアプリの情報を常時表示する](http://techbooster.org/android/ui/13182/)

そもそもドラッグ＆ドロップでViewを移動するにはどうするの？って話はこちらを参考に。
[こんなに簡単だとは思わなかった！Viewのドラッグ方法](http://kurukurupapa.hatenablog.com/entry/20120422/1335098811)

# はまったぞいや。

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

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

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

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

まったく同じ悩みを持っている記事があったけど、結局明確な解決策は載ってなかった。
[Androidでoverlay表示させたViewにタッチイベントを消費させないようにする](
http://ja.stackoverflow.com/questions/14544/android%E3%81%A7overlay%E8%A1%A8%E7%A4%BA%E3%81%95%E3%81%9B%E3%81%9Fview%E3%81%AB%E3%82%BF%E3%83%83%E3%83%81%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%82%92%E6%B6%88%E8%B2%BB%E3%81%95%E3%81%9B%E3%81%AA%E3%81%84%E3%82%88%E3%81%86%E3%81%AB%E3%81%99%E3%82%8B)

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

・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](https://qiita-image-store.s3.amazonaws.com/0/132699/b97158fc-1b7d-74b0-7948-233c1fb2c984.png)

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

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

### ポイント2：移動させた分をupdateViewLayoutで更新する
ただparamsを変えただけだとオーバーレイの領域は変わらないので、
WindowManager#updateViewLayout()を呼んであげて、描画の更新をします。

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

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