一 摘要
目前最容易实现虚浮的是让用户打开悬浮窗权限,但是这相当于让用户多了一步操作,有可能用户还不愿意去打开。有没有一种可以不用打开权限直接可以用的?答案肯定是有的,只是有点hook了系统代码,大部分开发场景只有debug包使用,比如说:测试环境性能检测工具DoraemonKit
,这样全局的一个功能。
先看一下成果
二 技术了解
2.1 先了解一下setContentView的流程
当我们调用 setContentView 的时候,首先会调用 PhoneWindow 的 setContentView(高版本可能会绕一大圈来调用,最终还是调用PhoneWindow), PhoneWindow 的 类中有个内部类 DecorView,DecorView是继承FramLayout的一个View,之后会根据不同的Theme去拿不同的根布局,但是这些根布局中都会用一个id为 android.R.id.content 的View,然后把咱们的xml所生成的View添加到 android.R.id.content 的View上面
2.2 ViewDragHelper
ViewDragHelper是针对 ViewGroup 中的拖拽和重新定位 views 操作时提供了一系列非常有用的方法和状态追踪。基本上使用在自定义ViewGroup处理拖拽中,可以实现拖拽功能
三 寻找方案
- 可以把我们悬浮的View 放到 DecorView 中 (笔者做实验,发现行不通)
- 可以把虚浮的View 放到 android.R.id.content 中
- 拖拽可以使用 ViewDragHelper 来实现
- 我们可以注册Activity的生命周期监听 context.registerActivityLifecycleCallbacks(this),在Activity的onStart中添加咱们的虚浮View,因为相当于在ContentView中重新new 了一个View,感觉没有内存泄漏,所以 onStop的时候不用移除
四 开始撸代码
先定义一个FloatViewManger 用来初始化虚浮View
在里面注册Activity的生命周期,在Activity的onStart的时候添加View,把 android.R.id.content 先拿到子View,并且移除,然后添加成咱们可以滑动的 DragViewParent,然后把刚才移除的View添加到咱们DragViewParent 中,代码如下
object FloatViewManger : Application.ActivityLifecycleCallbacks {
var height: Float = 0F
var width: Float = 0F
/**
* 从 0 - 1
*/
var proportionX: Float = 0F
/**
* 从 0 - 1
*/
var proportionY: Float = 0F
private lateinit var context: Context
var floatView: View? = null
fun init(context: Application) {
this.context = context
// 注册Activity生命周期的回调监听
context.registerActivityLifecycleCallbacks(this)
}
/**
* 给每个Activity的DecorView上添加所需要的View
* 因为使用了ViewDragHelper类 ,所以 找到 android.R.id.content
* 当咱们 setContentView 之后,会把view设置到上面的android.R.id.content中
* 之后咱们拿到 android.R.id.content 的第一个child 就是xml的view
* 然后 android.R.id.content 添加子View DragViewParent,DragViewParent 添加子View xml
*/
private fun addView(activity: Activity) {
if (floatView == null) {
return
}
val decorView = activity.window.decorView as ViewGroup
// 如果添加过 可以从 decorView 中找到 DragViewParent 就不用再继续初始化 DragViewParent
val tagView = decorView.findViewWithTag<View>(TAG)
var dragViewParent: DragViewParent
if (tagView != null) {
// 证明以前已经把 DragViewParent 添加到过 ContentIdView 里面了
dragViewParent = tagView as DragViewParent
} else {
// 初始化
dragViewParent = DragViewParent(context)
val contentView = decorView.findViewById<ViewGroup>(android.R.id.content)
// 这个就是咱们写setContentView的时候的View
val xmlView = contentView.getChildAt(0);
// 把这个View先从 contentView移除
contentView.removeView(xmlView)
// 把这个View添加到咱们自定义View中
dragViewParent.addView(xmlView)
// 把咱们自定义滑动的View添加到contentView中
contentView.addView(dragViewParent)
dragViewParent.tag = TAG
}
if (floatView!!.parent != null) {
// 先移除 用户需要虚浮的View,因为虚浮的View是唯一的,添加过之后必须得移除之后才能添加
val parentView = floatView!!.parent as ViewGroup
parentView.removeView(floatView)
}
// 把用户需要虚浮的View添加到 咱们自定义滑动DragViewParent中
dragViewParent.setDragViewChild(floatView!!, width, height)
}
fun show() {
floatView?.visibility = View.VISIBLE
}
fun hide() {
floatView?.visibility = View.GONE
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
}
override fun onActivityStarted(activity: Activity) {
addView(activity)
}
override fun onActivityResumed(activity: Activity) {
}
override fun onActivityPaused(activity: Activity) {
}
override fun onActivityStopped(activity: Activity) {
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}
override fun onActivityDestroyed(activity: Activity) {
}
}
复制代码
定义一个可拖拽的ParentView
利用 ViewDragHelper 来实现可以拖拽功能
**const val TAG = "tag"
/**
* 记录滑动过的最后的位置
*/
var lastX = -1f
var lastY = -1f
class DragViewParent(context: Context) : FrameLayout(context) {
private lateinit var dragHelper: ViewDragHelper
private lateinit var dragView: View
private val viewWidth = 0
private val viewHeight = 0
fun setDragViewChild(view: View, width: Float, height: Float) {
isClickable = false
dragView = view
var layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT
)
addView(view, layoutParams)
setViewDragHelper()
}
private fun setViewDragHelper() {
dragHelper = ViewDragHelper.create(this, 1.0f, object : ViewDragHelper.Callback() {
override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int {
val leftBound = paddingLeft
val rightBound: Int = width - dragView.width - leftBound
return Math.min(Math.max(left, leftBound), rightBound)
}
override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int {
val leftBound = paddingTop
val rightBound: Int = height - dragView.height - dragView.paddingBottom
return Math.min(Math.max(top, leftBound), rightBound)
}
//在边界拖动时回调
override fun onEdgeDragStarted(edgeFlags: Int, pointerId: Int) {}
override fun getViewHorizontalDragRange(child: View): Int {
return measuredWidth - child.measuredWidth
}
override fun getViewVerticalDragRange(child: View): Int {
return measuredHeight - child.measuredHeight
}
override fun tryCaptureView(child: View, pointerId: Int): Boolean {
return dragView === child
}
override fun onViewPositionChanged(
changedView: View,
left: Int,
top: Int,
dx: Int,
dy: Int
) {
super.onViewPositionChanged(changedView, left, top, dx, dy)
lastX = changedView.x
lastY = changedView.y
}
})
dragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT)
}
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
return dragHelper.shouldInterceptTouchEvent(event)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
dragHelper.processTouchEvent(event)
return true
}
override fun computeScroll() {
if (dragHelper.continueSettling(true)) {
invalidate()
}
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
super.onLayout(changed, l, t, r, b)
restorePosition()
}
private fun restorePosition() {
if (lastX == -1f && lastY == -1f) { // 初始位置
// lastX = (measuredWidth - (if (dragView.measuredWidth > 0) dragView.measuredWidth else viewWidth).toFloat())
// lastY = (measuredHeight - (if (dragView.measuredHeight > 0) dragView.measuredHeight else viewHeight).toFloat())
lastX = (measuredWidth-dragView.measuredWidth) * FloatViewManger.proportionX
lastY = (measuredHeight-dragView.measuredHeight) * FloatViewManger.proportionY
}
dragView.layout(
lastX.toInt(),
lastY.toInt(),
lastX.toInt() + dragView.measuredWidth,
lastY.toInt() + dragView.measuredHeight
)
}
}**
复制代码
其次是用到的工具文件
val Float.px: Float
get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
this,
Resources.getSystem().displayMetrics
)
fun getScreenHeight(): Int {
return Resources.getSystem().displayMetrics.heightPixels
}
fun getScreenWeith(): Int {
return Resources.getSystem().displayMetrics.widthPixels
}
复制代码
源码位置 FloatView
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END