Search code examples
javaandroidkotlinfloatingandroid-window

How to create a floating window like Clipboard Pro App?


The following image is a window of the app https://play.google.com/store/apps/details?id=jp.snowlife01.android.clipboard. i named it as main window.

It's seem that main window is not a normal window. it's on the top of other windows, the main window can't be moved.

A small point icon will be displayed when I click the minimized button in the main window, the small point icon can be moved, the main window can be restored when I click the small point icon.

I think the small point icon is a SYSTEM_ALERT_WINDOW, but how about the main window?

Image

enter image description here


Solution

  • You can create a floating view using WindowManager by granting Draw Overlays permission only for APIs above M. (For APIs below 23, this permission is always granted)

    I have developed a sample code that you can use it simply. (Available on 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)
    


    Visual Result:

    enter image description here