はじめに
この記事はロボP Advent Calendar 2018の2日目の記事です.
FBのMessengerって?
メッセージが来ると横にプヨって出てくるやつ
ChatHeadって言うらしい。
作る
画面に残るViewを作る
WindowManagerをServiceで動かして実装します。
通常のアプリケーションより高いレイヤーに描画するイメージ。
Permission
画面を重ねて描画するためのSYSTEM_ALERT_WINDOW
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
MainActivity
無くてもいけますが、テストしやすいため今回は使います。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//API23から権限が必要
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
checkPermission()
}
findViewById<TextView>(R.id.text).setOnClickListener {
val intent = Intent(application, OverlayService::class.java)
// Serviceの開始
// API26以上はNotificationが入るため処理が別
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
}
}
@RequiresApi(Build.VERSION_CODES.M)
fun checkPermission() {
if (!Settings.canDrawOverlays(this)) {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:$packageName"))
startActivityForResult(intent, 1000)
}
}
}
**Android8.0(Oreo|API26)**からは画面上に重ねて描画すると
Notificationが出るようになったため、startForegroundService()
でServiceを開始します。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
レイアウトは特に変えてません。
OverlayService
こいつが今回の肝です。
class OverlayService : Service() {
private var view: View? = null
private var windowManager: WindowManager? = null
private var dpScale: Int = 0
override fun onCreate() {
super.onCreate()
dpScale = resources.displayMetrics.density.toInt()
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val context = applicationContext
val channelId = "default"
val title = context.getString(R.string.app_name)
val pendingIntent = PendingIntent.getActivity(context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT)
//8.0(Oreo)以上用のNotification設定
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel(
channelId, title, NotificationManager.IMPORTANCE_DEFAULT)
if (notificationManager != null) {
notificationManager.createNotificationChannel(channel)
val notification = Notification.Builder(context, channelId)
.setContentTitle(title)
.setSmallIcon(android.R.drawable.btn_star)
.setContentText("APPLICATION_OVERLAY")
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.setWhen(System.currentTimeMillis())
.build()
startForeground(1, notification)
}
}
// inflaterの生成
val layoutInflater = LayoutInflater.from(this)
//8.0(Oreo)以上のみレイヤータイプを分ける
val typeLayer = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
else
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
windowManager = applicationContext
.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val params = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
typeLayer,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
PixelFormat.TRANSLUCENT)
val nullParent: ViewGroup? = null
view = layoutInflater.inflate(R.layout.layout_overlay, nullParent)
//クリックで解除できるように
view?.findViewById<Button>(R.id.btn_overlay)
?.setOnClickListener {
// Viewを削除
windowManager!!.removeView(view)
}
// ViewにTouchListenerを設定する
view!!.setOnTouchListener { v, event ->
Log.d("debug", "onTouch")
if (event.action == MotionEvent.ACTION_DOWN) {
Log.d("debug", "ACTION_DOWN")
}
if (event.action == MotionEvent.ACTION_UP) {
Log.d("debug", "ACTION_UP")
view!!.performClick()
stopSelf()
}
false
}
// Viewを画面上に追加
windowManager!!.addView(view, params)
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
super.onDestroy()
Log.d("debug", "onDestroy")
// Viewを削除
windowManager!!.removeView(view)
}
override fun onBind(intent: Intent): IBinder? {
return null
}
レイヤータイプが8.0以上とそれ以外で異なるので、注意してください。
//8.0(Oreo)以上のみレイヤータイプを分ける
val typeLayer = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
else
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
ここで描画するViewはここでinflateしてます。
view = layoutInflater.inflate(R.layout.layout_overlay, nullParent)
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<Button
android:id="@+id/btn_overlay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ボタン"/>
</android.support.constraint.ConstraintLayout>
実行!
UIをそれっぽくする
今回はMessengerの代わりにWebを出すようにします。
Permission
<uses-permission android:name="android.permission.INTERNET"/>
webViewを使うのでINTERNET
を追加します。
OverlayService
class OverlayService : Service() {
private var view: View? = null
private var windowManager: WindowManager? = null
private var dpScale: Int = 0
private var typeLayer: Int = 0
private var isBigView: Boolean = false
var overlayImgV: ImageView? = null
var webV: WebView? = null
override fun onCreate() {
super.onCreate()
dpScale = resources.displayMetrics.density.toInt()
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val context = applicationContext
val channelId = "default"
val title = context.getString(R.string.app_name)
val pendingIntent = PendingIntent.getActivity(context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT)
//8.0(Oreo)以上用のNotification設定
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel(
channelId, title, NotificationManager.IMPORTANCE_DEFAULT)
if (notificationManager != null) {
notificationManager.createNotificationChannel(channel)
val notification = Notification.Builder(context, channelId)
.setContentTitle(title)
.setSmallIcon(android.R.drawable.btn_star)
.setContentText("APPLICATION_OVERLAY")
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.setWhen(System.currentTimeMillis())
.build()
startForeground(1, notification)
}
}
// inflaterの生成
val layoutInflater = LayoutInflater.from(this)
//8.0(Oreo)以上のみレイヤータイプを分ける
typeLayer = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
else
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
windowManager = applicationContext
.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val params = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
typeLayer,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
PixelFormat.TRANSLUCENT)
params.gravity = Gravity.TOP or Gravity.LEFT
val nullParent: ViewGroup? = null
view = layoutInflater.inflate(R.layout.layout_overlay, nullParent)
//クリックで解除できるように
view?.findViewById<Button>(R.id.btn_overlay)
?.setOnClickListener {
// Viewを削除
windowManager!!.removeView(view)
}
overlayImgV = view?.findViewById<ImageView>(R.id.ic_overlay)
overlayImgV?.setOnClickListener {
if (!isBigView) {
openBigView()
} else {
closeBigView()
}
}
webV=view?.findViewById<WebView>(R.id.web_overlay)
webV?.visibility=View.GONE
webV?.webViewClient = WebViewClient()
webV?.loadUrl("https://www.google.com/")
// ViewにTouchListenerを設定する
view!!.setOnTouchListener { v, event ->
Log.d("debug", "onTouch")
if (event.action == MotionEvent.ACTION_DOWN) {
Log.d("debug", "ACTION_DOWN")
}
if (event.action == MotionEvent.ACTION_UP) {
Log.d("debug", "ACTION_UP")
view!!.performClick()
stopSelf()
}
false
}
// Viewを画面上に追加
windowManager!!.addView(view, params)
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
super.onDestroy()
Log.d("debug", "onDestroy")
// Viewを削除
windowManager!!.removeView(view)
}
override fun onBind(intent: Intent): IBinder? {
return null
}
private fun openBigView() {
val params = WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT,
typeLayer,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
PixelFormat.TRANSLUCENT)
params.gravity = Gravity.TOP or Gravity.LEFT
windowManager?.updateViewLayout(view, params)
val cx = overlayImgV?.left!! + overlayImgV?.width!! / 2
val cy = overlayImgV?.top!! + overlayImgV?.height!! / 2
val radius = Math.max(webV?.width!!, webV?.height!!)
val anim = android.view.ViewAnimationUtils.createCircularReveal(webV, cx, cy, 0f, radius.toFloat())
webV?.visibility = View.VISIBLE
TransitionManager.beginDelayedTransition(webV?.rootView as ViewGroup)
anim.start()
isBigView = true
}
private fun closeBigView() {
val cx = overlayImgV?.left!! + overlayImgV?.width!! / 2
val cy = overlayImgV?.top!! + overlayImgV?.height!! / 2
val radius = Math.max(webV?.width!!, webV?.height!!)
val anim = android.view.ViewAnimationUtils.createCircularReveal(webV, cx, cy, radius.toFloat(), 0f)
anim.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
webV?.visibility = View.GONE
TransitionManager.beginDelayedTransition(webV?.rootView as ViewGroup)
val params = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
typeLayer,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
PixelFormat.TRANSLUCENT)
params.gravity = Gravity.TOP or Gravity.LEFT
windowManager?.updateViewLayout(view, params)
}
})
anim.start()
isBigView = false
}
}
アニメーションを追加しました。
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<WebView
android:id="@+id/web_overlay"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="WebViewLayout" />
<ImageView
android:id="@+id/ic_overlay"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@mipmap/ic_launcher"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/btn_overlay"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@drawable/bg_oval"
android:text="×"
android:textColor="#ffffff"
android:textSize="20dp"
app:layout_constraintEnd_toEndOf="@id/ic_overlay"
app:layout_constraintTop_toTopOf="@id/ic_overlay" />
</android.support.constraint.ConstraintLayout>
ButtonをImageViewに買えて×ボタンとWebViewを追加しました。
実行!
参考
nyanのアプリ開発 | WindowManagerを使ってServiceから画像を表示させ続ける
AndroidでViewを他のアプリの上に表示する (APIバージョン別 対応方法まとめ)
めっちゃ参考にしました。ありがとうございます。