应用内悬浮窗

一 摘要

目前最容易实现虚浮的是让用户打开悬浮窗权限,但是这相当于让用户多了一步操作,有可能用户还不愿意去打开。有没有一种可以不用打开权限直接可以用的?答案肯定是有的,只是有点hook了系统代码,大部分开发场景只有debug包使用,比如说:测试环境性能检测工具DoraemonKit
,这样全局的一个功能。

先看一下成果

1623816865103880.gif

二 技术了解

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上面
demo.png

2.2 ViewDragHelper

ViewDragHelper是针对 ViewGroup 中的拖拽和重新定位 views 操作时提供了一系列非常有用的方法和状态追踪。基本上使用在自定义ViewGroup处理拖拽中,可以实现拖拽功能

三 寻找方案

  1. 可以把我们悬浮的View 放到 DecorView 中 (笔者做实验,发现行不通)
  2. 可以把虚浮的View 放到 android.R.id.content 中
  3. 拖拽可以使用 ViewDragHelper 来实现
  4. 我们可以注册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
喜欢就支持一下吧
点赞0 分享