由于众所周知的原因,Android系统虽然提供了悬浮窗的功能,但使用之前需要用记授权,有些手机对这个授权还要再次确认,以至于很多用户出于谨慎的目地就不去打开了。但我们在实际开发当中却又需要这个功能是该怎么?
既然直接使用是不行了,那只能考虑折中的办法了。首先能想到的是把悬浮窗作在XML布局里面,不用时隐藏,需要时显示。但如果我们遇到一批需要悬浮窗的界面时要怎么办?有一个比较稳妥的方法;从DecorView下手!
我们知道Activity对于界面的加载是交给自己的手下PhoneWindow去处理的,PhoneWindow再交给自己的手下DecorView。DecorView虽然是一个FrameLayout的自定义类,理论上由它来加载就可以了,但DecorView还挺能整活,它不直接加载我们定义的XML布局,而是先加载了一个系统内置的布局,这个内置的布局是LinearLayout结构的,上面是一个标题区,下面是内容区,内容区就是一个FrameLayout,并且有一个系统级的ID作为标记。这个内置布局结构如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
我们自定义的XML布局就是被加载到这个ID为content的FrameLayout里的。由于这是一个系统内置的布局,每一个Activity都有,所以我们可以充分利用这一便利来给实现我们的悬浮窗效果。
大致的思路是这样的:
- 自定义一个Activity子类专门用于处理悬浮窗的操作,在项目中需要用到悬浮窗的地方就继承这个类。
- 在自定义的Activity子类中通过findViewById<ViewGroup>(android.R.id.content)获取到系统内置的内容布局(ContentFrameLayout)。
- 自定义一个View作为我们的悬浮窗,然后将这个View添加到内容布局。
在上述步骤之前ID为content的内容布局之下原本只有一个子布局,即我们在setContentView里指定的XML布局资源,执行了上述步骤之后内容布局又会多出一个子布局,由于内容布局本身就是一个FrameLayout,所以后加的布局自然放在最上层,这就是悬浮窗的效果了。
下面就来实例个带拖动效果的悬浮窗
悬浮窗带拖动效果
先自定义一个FloatActivity,封装一下悬浮窗的功能。由于我们的悬浮窗是带有拖动功能的,所以还需要把窗体的坐标保存在配置文件里,以便于Activity在跳转的时候可以读取得到:
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import kotlinx.android.synthetic.main.float_window.view.*
/**
* 带悬浮窗操作的Activity
*/
open class FloatActivity : AppCompatActivity() {
private var offX = 0
private var offY = 0
private var isPressing = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 根据Activity的生命周期显示或移除悬浮窗
this.lifecycle.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume() {
// 在Activity可见时加载悬浮窗
showFloatWindow(isShowing(applicationContext))
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onStop() {
// 由于悬浮窗是每个Activity都有的,所以在暂停时移除以释放资源
removeFloatWindow()
}
})
}
/**
* 定义一个悬浮窗,这个悬浮窗只是一个普通的View对象
* 可以根据需求定义不同的窗体
*/
@SuppressLint("ClickableViewAccessibility")
private fun buildFloatWindow(): View {
val view = LayoutInflater.from(this).inflate(R.layout.float_window, null, false)
view.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
view.btnClose.setOnClickListener { showFloatWindow(false) }
view.ivIcon.setOnTouchListener { v, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
offX = event.rawX.toInt() - view.x.toInt()
offY = event.rawY.toInt() - view.y.toInt()
isPressing = true
return@setOnTouchListener true
}
MotionEvent.ACTION_UP -> {
isPressing = false
saveLocation(this, view.x.toInt(), view.y.toInt())
return@setOnTouchListener true
}
MotionEvent.ACTION_MOVE -> {
if (isPressing) {
view.x = event.rawX - offX
view.y = event.rawY - offY
}
return@setOnTouchListener true
}
}
false
}
return view
}
/**
* 设置悬浮窗的显示或隐藏
*/
protected fun showFloatWindow(isShow: Boolean) {
val vgContent = findViewById<ViewGroup>(android.R.id.content)
if (vgContent.childCount == 1) {
vgContent.addView(FrameLayout(this))
}
val vgFloatContainer = vgContent.getChildAt(1) as ViewGroup
if (isShow) {
if (vgFloatContainer.childCount == 0) {
val viewFloat = buildFloatWindow()
viewFloat.x = loadLocationX(this).toFloat()
viewFloat.y = loadLocationY(this).toFloat()
vgFloatContainer.addView(viewFloat)
}
} else {
vgFloatContainer.removeAllViews()
}
setShowing(this, isShow)
}
/**
* 删除悬浮窗
*/
protected fun removeFloatWindow() {
val vgContent = findViewById<ViewGroup>(android.R.id.content)
if (vgContent.childCount == 2)
vgContent.removeViewAt(1)
}
/**
* 保存和读取悬浮窗的参数到配置文件
*/
companion object {
private const val FILE = "floatWindow"
fun isShowing(ctx: Context): Boolean {
return ctx.getSharedPreferences(FILE, Context.MODE_PRIVATE)
.getBoolean("show", false)
}
private fun setShowing(ctx: Context, isShowing: Boolean) {
ctx.getSharedPreferences(FILE, Context.MODE_PRIVATE)
.edit { putBoolean("show", isShowing) }
}
private fun loadLocationX(ctx: Context): Int {
return ctx.getSharedPreferences(FILE, Context.MODE_PRIVATE)
.getInt("x", 0)
}
private fun loadLocationY(ctx: Context): Int {
return ctx.getSharedPreferences(FILE, Context.MODE_PRIVATE)
.getInt("y", 0)
}
private fun saveLocation(ctx: Context, x: Int, y: Int) {
ctx.getSharedPreferences(FILE, Context.MODE_PRIVATE)
.edit {
putInt("x", x)
putInt("y", y)
}
}
}
}
我们自定义了一个View作为悬浮窗,然后把这个View添加到一个全屏的FrameLayout里面,悬浮窗的移动都是在这个FrameLayout里实现的,最后再把这个FrameLayout添加到内容布局之中。
float_window.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:elevation="6dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="12dp">
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:id="@+id/ivIcon"
android:src="@mipmap/ic_launcher_round" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="12dp"
android:text="点击图标拖拽" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/btnClose"
android:text="关闭" />
</LinearLayout>
在项目中只需简单的调用即可,MainActivity.kt
import android.content.Intent
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : FloatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
btnSecond.setOnClickListener { startActivity(Intent(this, SecondActivity::class.java)) }
chkShowFloatWindow.setOnCheckedChangeListener { _, isChecked ->
showFloatWindow(isChecked) //根据勾选值显示或隐藏悬浮窗
}
}
override fun onResume() {
super.onResume()
chkShowFloatWindow.isChecked = FloatActivity.isShowing(this)
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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">
<Button
android:id="@+id/btnSecond"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="跳转SecondActivity"
android:textAllCaps="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<CheckBox
android:id="@+id/chkShowFloatWindow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="显示悬浮窗"
app:layout_constraintLeft_toLeftOf="@+id/btnSecond"
app:layout_constraintRight_toRightOf="@id/btnSecond"
app:layout_constraintTop_toBottomOf="@id/btnSecond" />
</androidx.constraintlayout.widget.ConstraintLayout>
SecondActivity.kt
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_second.*
class SecondActivity : FloatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
btnBack.setOnClickListener { onBackPressed() }
chkShowFloatWindow.setOnCheckedChangeListener { _, isChecked -> showFloatWindow(isChecked) }
}
override fun onResume() {
super.onResume()
chkShowFloatWindow.isChecked = FloatActivity.isShowing(this)
}
}
activity_second.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".SecondActivity">
<Button
android:id="@+id/btnBack"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="返回"
android:textAllCaps="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<CheckBox
android:id="@+id/chkShowFloatWindow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="显示悬浮窗"
app:layout_constraintLeft_toLeftOf="@+id/btnBack"
app:layout_constraintRight_toRightOf="@id/btnBack"
app:layout_constraintTop_toBottomOf="@id/btnBack" />
</androidx.constraintlayout.widget.ConstraintLayout>
点击链接加入QQ群聊:https://jq.qq.com/?_wv=1027&k=5z4fzdT
或关注微信公众号:口袋里的安卓
网友评论