目的
こんな風にオーバーレイ表示させた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の位置を移動させるのではなく、
オーバーレイの表示領域自体を移動させることで解決しました。
まずはコードから。必要な部分だけ抜粋。
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
}
}
}
}
長くなってしまったけど、ポイントは二つだけです。
// ディスプレイのサイズを格納する
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にはオーバーレイの座標系を元にした座標が入ります。
そこで、タップ位置の座標系とオーバーレイの座標系を合わせるために
val centerX = x - (displaySize.x / 2)
val centerY = y - (displaySize.y / 2)
で、中心から移動させたい移動量を計算します。
ポイント2:移動させた分をupdateViewLayoutで更新する
ただparamsを変えただけだとオーバーレイの領域は変わらないので、
WindowManager#updateViewLayout()を呼んであげて、描画の更新をします。
表示される領域そのものが移動しているので、Viewの位置を移動させる必要はありません。
(Viewの位置は表示領域の左上を起点にして計算されるから)
Viewを移動させるよりもシンプルな実装で、実現することができましたね!