如何创建一个浮动的 window like Clipboard Pro App?
How to create a floating window like Clipboard Pro App?
下图是 window 应用 https://play.google.com/store/apps/details?id=jp.snowlife01.android.clipboard。我将其命名为 main window.
main window 似乎不是正常的 window。它在其他 windows 之上,主要 window 无法移动。
点击主window中的最小化按钮会显示一个小点图标,小点图标可以移动,点击小可以恢复主window点图标。
我认为小点图标是SYSTEM_ALERT_WINDOW
,但是主要的window怎么样?
图片
会假定它的类型为 TYPE_APPLICATION_OVERLAY
。
可能与android:theme="@android:style/Theme.Dialog"
结合。
并且需要 Manifest.permission.SYSTEM_ALERT_WINDOW
.
除非从 Play 商店安装,否则您必须手动启用权限:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
startActivity(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION));
}
看documentation(适合的真的不多)
您可以使用 WindowManager
创建浮动视图,方法是仅授予 M
以上 API 的 Draw Overlays
权限。 (对于 23 以下的 API,始终授予此权限)
我已经开发了一个示例代码,您可以简单地使用它。 (在 GitHub 可用:https://github.com/aminography/FloatingWindowApp)
SimpleFloatingWindow.kt:
import android.content.Context
import android.content.Context.WINDOW_SERVICE
import android.graphics.PixelFormat
import android.os.Build
import android.view.*
import kotlinx.android.synthetic.main.layout_floating_window.view.*
import kotlin.math.abs
/**
* @author aminography
*/
class SimpleFloatingWindow constructor(private val context: Context) {
private var windowManager: WindowManager? = null
get() {
if (field == null) field = (context.getSystemService(WINDOW_SERVICE) as WindowManager)
return field
}
private var floatView: View =
LayoutInflater.from(context).inflate(R.layout.layout_floating_window, null)
private lateinit var layoutParams: WindowManager.LayoutParams
private var lastX: Int = 0
private var lastY: Int = 0
private var firstX: Int = 0
private var firstY: Int = 0
private var isShowing = false
private var touchConsumedByMove = false
private val onTouchListener = View.OnTouchListener { view, event ->
val totalDeltaX = lastX - firstX
val totalDeltaY = lastY - firstY
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
lastX = event.rawX.toInt()
lastY = event.rawY.toInt()
firstX = lastX
firstY = lastY
}
MotionEvent.ACTION_UP -> {
view.performClick()
}
MotionEvent.ACTION_MOVE -> {
val deltaX = event.rawX.toInt() - lastX
val deltaY = event.rawY.toInt() - lastY
lastX = event.rawX.toInt()
lastY = event.rawY.toInt()
if (abs(totalDeltaX) >= 5 || abs(totalDeltaY) >= 5) {
if (event.pointerCount == 1) {
layoutParams.x += deltaX
layoutParams.y += deltaY
touchConsumedByMove = true
windowManager?.apply {
updateViewLayout(floatView, layoutParams)
}
} else {
touchConsumedByMove = false
}
} else {
touchConsumedByMove = false
}
}
else -> {
}
}
touchConsumedByMove
}
init {
with(floatView) {
closeImageButton.setOnClickListener { dismiss() }
textView.text = "I'm a float view!"
}
floatView.setOnTouchListener(onTouchListener)
layoutParams = WindowManager.LayoutParams().apply {
format = PixelFormat.TRANSLUCENT
flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
@Suppress("DEPRECATION")
type = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ->
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
else -> WindowManager.LayoutParams.TYPE_TOAST
}
gravity = Gravity.CENTER
width = WindowManager.LayoutParams.WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT
}
}
fun show() {
if (context.canDrawOverlays) {
dismiss()
isShowing = true
windowManager?.addView(floatView, layoutParams)
}
}
fun dismiss() {
if (isShowing) {
windowManager?.removeView(floatView)
isShowing = false
}
}
}
layout_floating_window.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ff4444"
android:padding="32dp"
android:text="text"
android:textColor="#ffffff"
android:textSize="24sp" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/closeImageButton"
style="@style/Base.Widget.AppCompat.Button.Borderless"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="top|right"
android:src="@drawable/ic_close_white_24dp" />
</FrameLayout>
MainActivity.kt:
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
/**
* @author aminography
*/
class MainActivity : AppCompatActivity() {
private lateinit var simpleFloatingWindow: SimpleFloatingWindow
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
simpleFloatingWindow = SimpleFloatingWindow(applicationContext)
button.setOnClickListener {
if (canDrawOverlays) {
simpleFloatingWindow.show()
} else {
startManageDrawOverlaysPermission()
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
REQUEST_CODE_DRAW_OVERLAY_PERMISSION -> {
if (canDrawOverlays) {
simpleFloatingWindow.show()
} else {
showToast("Permission is not granted!")
}
}
}
}
private fun startManageDrawOverlaysPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:${applicationContext.packageName}")
).let {
startActivityForResult(it, REQUEST_CODE_DRAW_OVERLAY_PERMISSION)
}
}
}
companion object {
private const val REQUEST_CODE_DRAW_OVERLAY_PERMISSION = 5
}
}
Extensions.kt:
import android.content.Context
import android.os.Build
import android.provider.Settings
import android.widget.Toast
/**
* @author aminography
*/
private var toast: Toast? = null
fun Context.showToast(message: CharSequence?) {
message?.let {
toast?.cancel()
toast = Toast.makeText(this, message, Toast.LENGTH_SHORT).apply { show() }
}
}
val Context.canDrawOverlays: Boolean
get() = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays(this)
视觉结果:
下图是 window 应用 https://play.google.com/store/apps/details?id=jp.snowlife01.android.clipboard。我将其命名为 main window.
main window 似乎不是正常的 window。它在其他 windows 之上,主要 window 无法移动。
点击主window中的最小化按钮会显示一个小点图标,小点图标可以移动,点击小可以恢复主window点图标。
我认为小点图标是SYSTEM_ALERT_WINDOW
,但是主要的window怎么样?
图片
会假定它的类型为 TYPE_APPLICATION_OVERLAY
。
可能与android:theme="@android:style/Theme.Dialog"
结合。
并且需要 Manifest.permission.SYSTEM_ALERT_WINDOW
.
除非从 Play 商店安装,否则您必须手动启用权限:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
startActivity(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION));
}
看documentation(适合的真的不多)
您可以使用 WindowManager
创建浮动视图,方法是仅授予 M
以上 API 的 Draw Overlays
权限。 (对于 23 以下的 API,始终授予此权限)
我已经开发了一个示例代码,您可以简单地使用它。 (在 GitHub 可用:https://github.com/aminography/FloatingWindowApp)
SimpleFloatingWindow.kt:
import android.content.Context
import android.content.Context.WINDOW_SERVICE
import android.graphics.PixelFormat
import android.os.Build
import android.view.*
import kotlinx.android.synthetic.main.layout_floating_window.view.*
import kotlin.math.abs
/**
* @author aminography
*/
class SimpleFloatingWindow constructor(private val context: Context) {
private var windowManager: WindowManager? = null
get() {
if (field == null) field = (context.getSystemService(WINDOW_SERVICE) as WindowManager)
return field
}
private var floatView: View =
LayoutInflater.from(context).inflate(R.layout.layout_floating_window, null)
private lateinit var layoutParams: WindowManager.LayoutParams
private var lastX: Int = 0
private var lastY: Int = 0
private var firstX: Int = 0
private var firstY: Int = 0
private var isShowing = false
private var touchConsumedByMove = false
private val onTouchListener = View.OnTouchListener { view, event ->
val totalDeltaX = lastX - firstX
val totalDeltaY = lastY - firstY
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
lastX = event.rawX.toInt()
lastY = event.rawY.toInt()
firstX = lastX
firstY = lastY
}
MotionEvent.ACTION_UP -> {
view.performClick()
}
MotionEvent.ACTION_MOVE -> {
val deltaX = event.rawX.toInt() - lastX
val deltaY = event.rawY.toInt() - lastY
lastX = event.rawX.toInt()
lastY = event.rawY.toInt()
if (abs(totalDeltaX) >= 5 || abs(totalDeltaY) >= 5) {
if (event.pointerCount == 1) {
layoutParams.x += deltaX
layoutParams.y += deltaY
touchConsumedByMove = true
windowManager?.apply {
updateViewLayout(floatView, layoutParams)
}
} else {
touchConsumedByMove = false
}
} else {
touchConsumedByMove = false
}
}
else -> {
}
}
touchConsumedByMove
}
init {
with(floatView) {
closeImageButton.setOnClickListener { dismiss() }
textView.text = "I'm a float view!"
}
floatView.setOnTouchListener(onTouchListener)
layoutParams = WindowManager.LayoutParams().apply {
format = PixelFormat.TRANSLUCENT
flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
@Suppress("DEPRECATION")
type = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ->
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
else -> WindowManager.LayoutParams.TYPE_TOAST
}
gravity = Gravity.CENTER
width = WindowManager.LayoutParams.WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT
}
}
fun show() {
if (context.canDrawOverlays) {
dismiss()
isShowing = true
windowManager?.addView(floatView, layoutParams)
}
}
fun dismiss() {
if (isShowing) {
windowManager?.removeView(floatView)
isShowing = false
}
}
}
layout_floating_window.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ff4444"
android:padding="32dp"
android:text="text"
android:textColor="#ffffff"
android:textSize="24sp" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/closeImageButton"
style="@style/Base.Widget.AppCompat.Button.Borderless"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="top|right"
android:src="@drawable/ic_close_white_24dp" />
</FrameLayout>
MainActivity.kt:
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
/**
* @author aminography
*/
class MainActivity : AppCompatActivity() {
private lateinit var simpleFloatingWindow: SimpleFloatingWindow
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
simpleFloatingWindow = SimpleFloatingWindow(applicationContext)
button.setOnClickListener {
if (canDrawOverlays) {
simpleFloatingWindow.show()
} else {
startManageDrawOverlaysPermission()
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
REQUEST_CODE_DRAW_OVERLAY_PERMISSION -> {
if (canDrawOverlays) {
simpleFloatingWindow.show()
} else {
showToast("Permission is not granted!")
}
}
}
}
private fun startManageDrawOverlaysPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:${applicationContext.packageName}")
).let {
startActivityForResult(it, REQUEST_CODE_DRAW_OVERLAY_PERMISSION)
}
}
}
companion object {
private const val REQUEST_CODE_DRAW_OVERLAY_PERMISSION = 5
}
}
Extensions.kt:
import android.content.Context
import android.os.Build
import android.provider.Settings
import android.widget.Toast
/**
* @author aminography
*/
private var toast: Toast? = null
fun Context.showToast(message: CharSequence?) {
message?.let {
toast?.cancel()
toast = Toast.makeText(this, message, Toast.LENGTH_SHORT).apply { show() }
}
}
val Context.canDrawOverlays: Boolean
get() = Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays(this)