androidkotlinandroid-windowmanagerandroid-window

How to correctly create a draggable floating view?


I use the following code to drag a view around the screen, and it works. But, when the user first touches moveIcon, the floatingView suddenly moves to the center of the screen, even though I want it to remain in its position. How can I fix this? I suspect the problem is in the updatePosition() method.

    windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
    val layoutInflater = LayoutInflater.from(this)
    floatingView = layoutInflater.inflate(R.layout.floating_layout, null)

    // Set up the layout parameters for the floating view
    val params = WindowManager.LayoutParams(
        WindowManager.LayoutParams.WRAP_CONTENT,
        WindowManager.LayoutParams.WRAP_CONTENT,
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
        } else {
            @Suppress("DEPRECATION")
            WindowManager.LayoutParams.TYPE_PHONE
        },
        WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
        PixelFormat.TRANSLUCENT
    )


    windowManager!!.addView(floatingView, params)

    // Moving the views:
    moveIcon = floatingView!!.findViewById(R.id.moveIcon)

    moveIcon.setOnTouchListener { view, event ->
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                // Save the initial touch coordinates relative to the moveIcon view
                initialTouchX = event.rawX - view.x
                initialTouchY = event.rawY - view.y
            }
            MotionEvent.ACTION_MOVE -> {
                // Calculate the new position based on the movement and initial touch coordinates
                val newX = event.rawX - initialTouchX
                val newY = event.rawY - initialTouchY

                updatePosition(newX.toInt(), newY.toInt())
            }
        }
        true
    }
}

private fun updatePosition(x: Int, y: Int) {
    val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager

    val layoutParams = floatingView!!.layoutParams as WindowManager.LayoutParams
    layoutParams.x = x
    layoutParams.y = y
    windowManager.updateViewLayout(floatingView, layoutParams)
}

Solution

  • The X/Y Coordinates of a view are defined in documentation as the translationX/Y plus the current left/top property. The Left/Top position of a view are relative to its parent.

    But, your floatingView is added directly through the WindowManger; and its parent here would be the ViewRootImpl. So, you'd not expect that the left/top position of that view has the true values you need because ViewRootImpl is the system wide root View that is not accessible to developers; so, if you logged down the view.x/y you'll see them always 0.

    Instead of accessing ViewRootImpl from the Views directly, they offered the WindowManger API for that purpose. I.e., when the floatingView added to the ViewRootImpl; we didn't do ViewRootImpl.addView(), but we did windowManager.addView().

    Similarly, we need to get the x/y parameters through the WindowManager as well using the WindowManager.LayoutParams to get the right values:

    ....
    MotionEvent.ACTION_DOWN -> {
    
    //    initialTouchX = event.rawX - view.x // Remove this
    //    initialTouchY = event.rawY - view.y // Remove this
    
        (floatingView.layoutParams as WindowManager.LayoutParams).let { params ->
            initialTouchX = event.rawX - params.x
            initialTouchY = event.rawY - params.y
        }
    
    }