How to create a floating window like Clipboard Pro App?
Asked Answered
F

2

9

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

Fraught answered 25/10, 2018 at 2:29 Comment(0)
T
21

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

Tufa answered 31/10, 2018 at 21:49 Comment(20)
Thanks! I test your code in Android 5.0, 6.0, it's Ok, but it crash in Android 9.0Fraught
You're welcome. Please paste the error log in hastebin.com and share me the link.Tufa
Do you want to open it outside an activity? I mean a service, etc.Tufa
I need to open the float window from an activity.Fraught
I have changed the answer a bit. All context interactions are done through an activity instance.Tufa
Thanks! But I still get error, you can see it at hastebin.com/lutoxinayo.sqlFraught
And more , final FloatViewManager floatViewManager = new FloatViewManager(getApplicationContext()); is wrong, I have changed it as final FloatViewManager floatViewManager = new FloatViewManager(this);Fraught
Oh yes, I forgot to change it. Is the problem solved?Tufa
Thanks! hanks! But I still get error, you can see it at hastebin.com/lutoxinayo.sql when I run it in Android 9.0Fraught
Do you finish the activity after showing float view immediately? Is it possible to share parts of your code that you are trying to show float view in it?Tufa
The problem is that when you create the FloatViewManager, the activity is still alive and when you are trying to show it, the activity was destroyed. I have updated the answer. mWindowManager is initialized when you are calling showFloatView. Be sure about state of activity when showFloatView is called. I have no access to android 9 device until tomorrow, so please check it yourself.Tufa
Thanks. API 22 is OK, both API 25 and API28 crashed, you can see the logs at hastebin.com/purafixijo.sqlFraught
I have changed it again. I think the problem should not be occurred, Sorry for that.Tufa
@HelloCW: Do you use it?Tufa
Thanks! Your code works well in API 28, 24, and 23 after I add <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> , but crashed at API 22 , you can see it at hastebin.com/icahuwufic.rbFraught
Update MainActivity code. We should check build os version before calling Settings.canDrawOverlays.Tufa
Can I place a fragment in FloatView? I do this but get an errorTyrannicide
@ch65: The floating view should be shown through an instance of context, not an activity (like above :) ), to be able to show it from everywhere accessing a context. Since there is no FragmentManager accessible in this case, so I think you can't use a Fragment.Tufa
Please create a sample project for this. I always got fail. :(Devisal
@frozenade: Check this: github.com/aminography/FloatingWindowAppTufa
S
0

would assume, that it's of type TYPE_APPLICATION_OVERLAY.

possibly combined with android:theme="@android:style/Theme.Dialog".

and it requires Manifest.permission.SYSTEM_ALERT_WINDOW.

unless installing from the Play Store, you'd have to manually enable the permission:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    startActivity(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION));
}

see the documentation (there's really not too many which would suit).

Steatopygia answered 28/10, 2018 at 3:49 Comment(5)
Thanks! but I install it outside Play Store with Android 8.0,it can work well, it seems that it doesn't require the permission .ACTION_MANAGE_OVERLAY_PERMISSIONFraught
It requires "Allow displau over apps" permission when I run it outside Google Play installed!Fraught
SYSTEM_ALERT_WINDOW is "allow display over apps". an intent with action ACTION_MANAGE_OVERLAY_PERMISSION just opens that settings dialog (it doesn't behave alike the regular run-time permissions, which one can request within the app).Steatopygia
Thanks! Can I create a TYPE_APPLICATION_OVERLAY window without Activity ?Fraught
Do I must create a TYPE_APPLICATION_OVERLAY window without Activity from a service?Fraught

© 2022 - 2024 — McMap. All rights reserved.