Simple Method Example of Android to Realize Floating Window

  • 2021-12-19 06:54:55
  • OfStack

Catalogue 1. Preface 2. Principle 3. Concrete realization 3.1 Floating Window Layout
3.2 Implementation of Floating Window
1. Use service Service2. Get WindowManager and set LayoutParams3. Create View and add it to WindowManager4. Drag and close floating window 5. Communicate with broadcast 6. Set permissions 3.3 Complete code
4. Summary

1. Preface

Nowadays, many applications have the function of small floating window. For example, when watching live broadcast, return to the desktop through Home key, and the small window of live broadcast can still be displayed on the screen. The following will introduce a simple implementation of the lower floating window.

2. Principle

We should be familiar with Window, it is an interface class, the specific implementation class for PhoneWindow, it can manage View. WindowManager is an interface class that inherits from ViewManager. It is known from its name that it is used to manage Window, and its implementation class is WindowManagerImpl. If we want to add, update and delete Window (View), we can use WindowManager, and WindowManager will leave the specific work to WindowManagerService. Here we just need to know that WindowManager can be used to manage Window.

WindowManager is an interface class that inherits from ViewManager, and ViewManager defines three methods distributed to add, update, and delete View, as follows:


public interface ViewManager {
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

WindowManager also inherits these methods, and the parameters passed in by these methods are of type View, indicating that Window exists in the form of View.

3. Concrete realization

3.1 Floating Window Layout

For a simple layout of the floating window, please refer to the file layout_floating_window. xml below. The FrameLayout layout of the dark part of the top layer is used to realize the drag and drop function of the floating window. Click ImageView in the upper right corner to close the floating window, and the rest area displays the content. Here, the text content is simply displayed, and no complicated things are done, so only TextView is set.


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <FrameLayout
        android:id="@+id/layout_drag"
        android:layout_width="match_parent"
        android:layout_height="15dp"
        android:background="#dddddd">
        <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/iv_close"
            android:layout_width="15dp"
            android:layout_height="15dp"
            android:layout_gravity="end"
            android:src="@drawable/img_delete"/>
    </FrameLayout>

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/tv_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center_horizontal"
        android:background="#eeeeee"
        android:scrollbars="vertical"/>
</LinearLayout>

3.2 Implementation of Floating Window

1. Using the service Service

Service is an application component that can run for a long time in the background without providing an interface. It can be started by other application components and will continue to run in the background even if the user switches to other applications. To ensure that the application in the background, the floating window can still be displayed normally, so Service can be used here.

2. Get WindowManager and set LayoutParams

private lateinit var windowManager: WindowManager
private lateinit var layoutParams: WindowManager.LayoutParams
override fun onCreate() {
    //  Get WindowManager
    windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
    layoutParams = WindowManager.LayoutParams().apply {
        //  Realize the display of floating windows above other applications and windows 
        type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
        } else {
            WindowManager.LayoutParams.TYPE_PHONE
        }
        format = PixelFormat.RGBA_8888
        //  Set the size and position of the floating window 
        gravity = Gravity.START or Gravity.TOP
        flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
        width = 600
        height = 600
        x = 300
        y = 300
    }
}

3. Create View and add it to WindowManager

private lateinit var floatingView: View
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    if (Settings.canDrawOverlays(this)) {
        floatingView = LayoutInflater.from(this).inflate(R.layout.layout_floating_window.xml, null)
        windowManager.addView(floatingView, layoutParams)
    }  
    return super.onStartCommand(intent, flags, startId)
}

4. Drag and close the floating window

//  Coordinates of floating window 
private var x = 0
private var y = 0

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {   
    if (Settings.canDrawOverlays(this)) {
    floatingView = LayoutInflater.from(this).inflate(R.layout.layout_floating_window.xml, null)
    windowManager.addView(floatingView, layoutParams)

    //  Click the Close button in the upper right corner of the floating window to close the floating window 
    floatingView.findViewById<AppCompatImageView>(R.id.iv_close).setOnClickListener {
     windowManager.removeView(floatingView)
    }
    //  Realize the dragging function of floating window ,  By changing layoutParams To realize 
    floatingView.findViewById<AppCompatImageView>(R.id.layout_drag).setOnTouchListener { v, event ->
     when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                x = event.rawX.toInt()
                y = event.rawY.toInt()
            }
            MotionEvent.ACTION_MOVE -> {
                val currentX = event.rawX.toInt()
                val currentY = event.rawY.toInt()
                val offsetX = currentX - x
                val offsetY = currentY - y
                x = currentX
                y = currentY
                layoutParams.x = layoutParams.x + offsetX
                layoutParams.y = layoutParams.y + offsetY
                //  Update floatingView
                windowManager.updateViewLayout(floatingView, layoutParams)
            }
        }
        true
    }
    return super.onStartCommand(intent, flags, startId)
}

5. Communication by Broadcast

private var receiver: MyReceiver? = null
override fun onCreate() {
    //  Registered broadcast 
    receiver = MyReceiver()
    val filter = IntentFilter()
    filter.addAction("android.intent.action.MyReceiver")
    registerReceiver(receiver, filter)
}

inner class MyReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val content = intent.getStringExtra("content") ?: ""

        //  Pass Handler Update UI
        val message = Message.obtain()
        message.what = 0
        message.obj = content
        handler.sendMessage(message)
    }
}

val handler = Handler(this.mainLooper) { msg ->
    tvContent.text = msg.obj as String
    false
}

Information can be broadcast to Service in Activity


fun sendMessage(view: View?) {
    Intent("android.intent.action.MyReceiver").apply {
        putExtra("content", "Hello, World!")
        sendBroadcast(this)
    }
}

6. Set permissions

The display of the floating window requires permission, add in AndroidManefest. xml:


<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

In addition, the permissions are set dynamically through Settings.ACTION_MANAGE_OVERLAY_PERMISSION, which is set in Activity.


// MainActivity.kt
fun startWindow(view: View?) {
    if (!Settings.canDrawOverlays(this)) {
        startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName")), 0)
    } else {
        startService(Intent(this@MainActivity, FloatingWindowService::class.java))
    }
}
​
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == 0) {
        if (Settings.canDrawOverlays(this)) {
            Toast.makeText(this, " Suspended Window Permission Authorization Successfully ", Toast.LENGTH_SHORT).show()
            startService(Intent(this@MainActivity, FloatingWindowService::class.java))
        }
    }
}

3.3 Complete code


class FloatingWindowService : Service() {
    private lateinit var windowManager: WindowManager
    private lateinit var layoutParams: WindowManager.LayoutParams
    private lateinit var tvContent: AppCompatTextView
    private lateinit var handler: Handler

    private var receiver: MyReceiver? = null
    private var floatingView: View? = null
    private val stringBuilder = StringBuilder()

    private var x = 0
    private var y = 0

    //  Used to judge floatingView Whether or not attached  To  window manager , prevent 2 Times removeView Lead to collapse 
    private var attached = false

    override fun onCreate() {
        super.onCreate()
        //  Registered broadcast 
        receiver = MyReceiver()
        val filter = IntentFilter()
        filter.addAction("android.intent.action.MyReceiver")
        registerReceiver(receiver, filter);

        //  Get windowManager And set the layoutParams
        windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
        layoutParams = WindowManager.LayoutParams().apply {
            type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
            } else {
                WindowManager.LayoutParams.TYPE_PHONE
            }
            format = PixelFormat.RGBA_8888
//            format = PixelFormat.TRANSPARENT
            gravity = Gravity.START or Gravity.TOP
            flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
            width = 600
            height = 600
            x = 300
            y = 300
        }
        handler = Handler(this.mainLooper) { msg ->
            tvContent.text = msg.obj as String
            //  Automatically scroll when the text goes beyond the screen, ensuring that the text is at the bottom 
            val offset = tvContent.lineCount * tvContent.lineHeight
            floatingView?.apply {
                if (offset > height) {
                    tvContent.scrollTo(0, offset - height)
                }
            }
            false
        }
    }

    override fun onBind(intent: Intent?): IBinder? {
        return null
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        if (Settings.canDrawOverlays(this)) {
            floatingView = LayoutInflater.from(this).inflate(R.layout.layout_show_log, null)
            tvContent = floatingView!!.findViewById(R.id.tv_log)
            floatingView!!.findViewById<AppCompatImageView>(R.id.iv_close).setOnClickListener {
                stringBuilder.clear()
                windowManager.removeView(floatingView)
                attached = false
            }
            //  Settings TextView Scroll 
            tvContent.movementMethod = ScrollingMovementMethod.getInstance()

            floatingView!!.findViewById<FrameLayout>(R.id.layout_drag).setOnTouchListener { v, event ->
                when (event.action) {
                    MotionEvent.ACTION_DOWN -> {
                        x = event.rawX.toInt()
                        y = event.rawY.toInt()
                    }
                    MotionEvent.ACTION_MOVE -> {
                        val currentX = event.rawX.toInt()
                        val currentY = event.rawY.toInt()
                        val offsetX = currentX - x
                        val offsetY = currentY - y
                        x = currentX
                        y = currentY
                        layoutParams.x = layoutParams.x + offsetX
                        layoutParams.y = layoutParams.y + offsetY
                        windowManager.updateViewLayout(floatingView, layoutParams)
                    }
                }
                true
            }

            windowManager.addView(floatingView, layoutParams)
            attached = true
        }
        return super.onStartCommand(intent, flags, startId)
    }

    override fun onDestroy() {
        //  Unregister the broadcast and delete the floating window 
        unregisterReceiver(receiver)
        receiver = null
        if (attached) {
            windowManager.removeView(floatingView)
        }
    }

    inner class MyReceiver : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            val content = intent.getStringExtra("content") ?: ""
            stringBuilder.append(content).append("\n")
            val message = Message.obtain()
            message.what = 0
            message.obj = stringBuilder.toString()
            handler.sendMessage(message)
        }
    }
}

4. Summary

The above is a simple implementation of Android floating window. If you need to realize other 1-point complicated functions, such as playing video, you can also complete it on this basis.


Related articles: