Android realizes the floating window function

preface

Most of us can see the suspension window in two cases, one is the suspension window during video call, and the other is the suspension ball of 360 guard. There are many ways to realize this function. Here, take the requirements in the suspension window of video call as an example. The coding implementation uses kotlin. Just leave a message in the Java version.

Business scenario

Take wechat video call as an example. During video call, when we open other applications or click the home key to exit or click the zoom icon, the suspension window will be displayed on other applications. It gives the illusion that the call page becomes smaller. Click the suspension window to return to the through page, and the suspension window will disappear. Exit the call page and the floating window disappears.

Business scenario technical analysis

Before coding, we must sort out the process, which is more conducive to the implementation of coding. If it takes 10 minutes to implement a function, the thinking time is 7 minutes, and the coding time is only 3 minutes.

1. The suspended window can be displayed on other applications or launchers. This must require the suspended window permission, and the suspended window permission belongs to a special permission, so it can only be opened by guiding the user, and cannot be applied directly like the dangerous permission. If the background display can be achieved, it indicates that the floating window is a service.

2. When the call page is hidden, the floating window is displayed. When the call page is displayed, the floating window is hidden. It can be seen that the floating window is associated with the life cycle of the activity, so the service of the floating window and the activity of the call page are bound through bind.

3. Since the service and activity are bound through bind, it means that when the floating window is displayed, the call activity is still running although it is not visible.

Combined with the analysis of the above technical problems, we flashback one by one through coding

Implementation scheme of suspended window

Realization effect

preparation

First, we create a new project with two activities. We write a call simulation page in the second activity. The reasons on the second page will be discussed later.

How to put acitivity in the background

In fact, it's very simple. We can call a method

moveTaskToBack(true);

The meaning of this method is to put the current task war in the background. So, one of the reasons why I want to implement it in the second activity is that the default startup mode of the activity is the standard mode, and the above method will put the task stack in the background instead of a separate activity. Therefore, in order to display the floating window without affecting other functions of the operating software, We need to set the activity of the call page to singleinstance, so that when the above method is called, only the activity stack where the call page is located is placed in the background. If you don't know the startup mode, you can move to the previous article: the startup mode of activity.

Now we add the above code to the click event at the top right, and you can see that the activity of the call page has been running in the background.

Judge whether there is floating window permission

When clicking the icon in the upper left corner, we must first judge whether the current app has the permission of floating window. First, we add the permission of floating window in the configuration file.

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

(the titles of many articles are about how the floating window bypasses permissions and what the setting type is toast or phone. I want to say that it is impossible. Although some models of toast can be displayed, it is an ordinary tosat that will disappear automatically.)

So how do we judge whether we have the permission of floating window? The processing schemes of different manufacturers may be different. Here we use a general processing scheme. The test shows that except (vivo part) is invalid, most other models are OK. In addition, some vivo models will not pop up prompts during wechat calls (I'm relieved ~)

fun zoom(v: View) {
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    if (!Settings.canDrawOverlays(this)) {
      Toast.makeText(this,"当前无权限,请授权",Toast.LENGTH_SHORT)
      GlobalDialogSingle(this,"","当前未获取悬浮窗权限","去开启",DialogInterface.OnClickListener { dialog,which ->
        dialog.dismiss()
        startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,Uri.parse("package:" + packageName)),0)
      }).show()

    } else {
      moveTaskToBack(true)
      val intent = Intent(this@Main2Activity,FloatWinfowServices::class.java)
      hasBind = bindService(intent,mVideoServiceConnection,Context.BIND_AUTO_CREATE)
    }
  }
}

We use settings.candrawoverlays (this) to determine whether the current application has floating window permission. If not, we pop up a window to prompt you

startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,0)

Jump to the open suspended window permission page. If the suspended window permission is enabled, directly put the current task stack in the background and start the service.

In fact, the callback method does not directly tell us whether the authorization is successful, so we need to judge again in the callback

override fun onActivityResult(requestCode: Int,resultCode: Int,data: Intent) {
  if (requestCode == 0) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      if (!Settings.canDrawOverlays(this)) {
        Toast.makeText(this,"授权失败",Toast.LENGTH_SHORT).show()
      } else {
        Handler().postDelayed({
          val intent = Intent(this@Main2Activity,FloatWinfowServices::class.java)
          intent.putExtra("rangeTime",rangeTime)
          hasBind = bindService(intent,Context.BIND_AUTO_CREATE)
          moveTaskToBack(true)
        },1000)

      }
    }
  }
}

Here, we can see that the callback was delayed by 1 second, because the test found that some models responded "too fast". When receiving the callback, we thought that the authorization was not successful, but in fact it was successful.

To bind service, we need a serviceconnection object

internal var mVideoServiceConnection: ServiceConnection = object : ServiceConnection {
  override fun onServiceConnected(name: ComponentName,service: IBinder) {
    // 获取服务的操作对象
    val binder = service as FloatWinfowServices.MyBinder
    binder.service
  }
  override fun onServiceDisconnected(name: ComponentName) {}
}

The complete code of main2activity is as follows:

/**
 * @author Huanglinqing
 */
class Main2Activity : AppCompatActivity() {
  private val chronometer: Chronometer? = null
  private var hasBind = false
  private val rangeTime: Long = 0
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main2)
  }
  fun zoom(v: View) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      if (!Settings.canDrawOverlays(this)) {
        Toast.makeText(this,Toast.LENGTH_SHORT)
        GlobalDialogSingle(this,which ->
          dialog.dismiss()
          startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,0)
        }).show()
      } else {
        moveTaskToBack(true)
        val intent = Intent(this@Main2Activity,FloatWinfowServices::class.java)
        hasBind = bindService(intent,Context.BIND_AUTO_CREATE)
      }
    }
  }
  internal var mVideoServiceConnection: ServiceConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName,service: IBinder) {
      // 获取服务的操作对象
      val binder = service as FloatWinfowServices.MyBinder
      binder.service
    }
    override fun onServiceDisconnected(name: ComponentName) {}
  }
  override fun onActivityResult(requestCode: Int,data: Intent) {
    if (requestCode == 0) {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        if (!Settings.canDrawOverlays(this)) {
          Toast.makeText(this,Toast.LENGTH_SHORT).show()
        } else {
          Handler().postDelayed({
            val intent = Intent(this@Main2Activity,FloatWinfowServices::class.java)
            intent.putExtra("rangeTime",rangeTime)
            hasBind = bindService(intent,Context.BIND_AUTO_CREATE)
            moveTaskToBack(true)
          },1000)
        }
      }
    }
  }
  override fun onRestart() {
    super.onRestart()
    Log.d("RemoteView","重新显示了")
    //不显示悬浮框
    if (hasBind) {
      unbindService(mVideoServiceConnection)
      hasBind = false
    }
  }
  override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
  }
  override fun onDestroy() {
    super.onDestroy()
  }
}

New suspended window service

Create a new floating window service floatwinfowservices. Because we use bindservice, we initialize the layout in the service in the onbind method

override fun onBind(intent: Intent): IBinder? {
  initWindow()
  //悬浮框点击事件的处理
  initFloating()
  return MyBinder()
}

In service, we add a layout display through WindowManager.

/**
 * 初始化窗口
 */
private fun initWindow() {
  winManager = application.getSystemService(Context.WINDOW_SERVICE) as WindowManager
  //设置好悬浮窗的参数
  wmParams = params
  // 悬浮窗显示左上角为起始坐标
  wmParams!!.gravity = Gravity.LEFT or Gravity.TOP
  //悬浮窗的开始位置,因为设置的是从左上角开始,所以屏幕左上角是x=0;y=0
  wmParams!!.x = winManager!!.defaultDisplay.width
  wmParams!!.y = 210
  //得到容器,通过这个inflater来获得悬浮窗控件
  inflater = LayoutInflater.from(applicationContext)
  // 获取浮动窗口视图所在布局
  mFloatingLayout = inflater!!.inflate(R.layout.remoteview,null)
  // 添加悬浮窗的视图
  winManager!!.addView(mFloatingLayout,wmParams)
}

The parameters of the suspended window mainly set the type of the suspended window as

WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY

8.0 the following can be set as:

wmParams!!.type = WindowManager.LayoutParams.TYPE_PHONE

The code is as follows:

private //设置window type 下面变量2002是在屏幕区域显示,2003则可以显示在状态栏之上
    //设置可以显示在状态栏上
    //设置悬浮窗口长宽数据
val params: WindowManager.LayoutParams
  get() {
    wmParams = WindowManager.LayoutParams()
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      wmParams!!.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
    } else {
      wmParams!!.type = WindowManager.LayoutParams.TYPE_PHONE
    }
    wmParams!!.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
        WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR or
        WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
    wmParams!!.width = WindowManager.LayoutParams.WRAP_CONTENT
    wmParams!!.height = WindowManager.LayoutParams.WRAP_CONTENT
    return wmParams
  }

When you click the hover window, you return to the activity2 page and the hover window disappears, so we just need to add a click event to the hover window

linearLayout!!.setOnClickListener { startActivity(Intent(this@FloatWinfowServices,Main2Activity::class.java)) }

Remove the view when the service goes to ondestory. For the activity2 page, unbind the service when onresume and bind the service when onstop.

From the rendering, we can see that the suspended window can be dragged, so we also need to set the touch event. When the moving distance exceeds a certain value, let ontouch consume the event, so that the click event will not be triggered. This is the basic knowledge of view. I believe everyone understands it.

//开始触控的坐标,移动时的坐标(相对于屏幕左上角的坐标)
private var mTouchStartX: Int = 0
private var mTouchStartY: Int = 0
private var mTouchCurrentX: Int = 0
private var mTouchCurrentY: Int = 0
//开始时的坐标和结束时的坐标(相对于自身控件的坐标)
private var mStartX: Int = 0
private var mStartY: Int = 0
private var mStopX: Int = 0
private var mStopY: Int = 0
//判断悬浮窗口是否移动,这里做个标记,防止移动后松手触发了点击事件
private var isMove: Boolean = false
private inner class FloatingListener : View.OnTouchListener {
  override fun onTouch(v: View,event: MotionEvent): Boolean {
    val action = event.action
    when (action) {
      MotionEvent.ACTION_DOWN -> {
        isMove = false
        mTouchStartX = event.rawX.toInt()
        mTouchStartY = event.rawY.toInt()
        mStartX = event.x.toInt()
        mStartY = event.y.toInt()
      }
      MotionEvent.ACTION_MOVE -> {
        mTouchCurrentX = event.rawX.toInt()
        mTouchCurrentY = event.rawY.toInt()
        wmParams!!.x += mTouchCurrentX - mTouchStartX
        wmParams!!.y += mTouchCurrentY - mTouchStartY
        winManager!!.updateViewLayout(mFloatingLayout,wmParams)
        mTouchStartX = mTouchCurrentX
        mTouchStartY = mTouchCurrentY
      }
      MotionEvent.ACTION_UP -> {
        mStopX = event.x.toInt()
        mStopY = event.y.toInt()
        if (Math.abs(mStartX - mStopX) >= 1 || Math.abs(mStartY - mStopY) >= 1) {
          isMove = true
        }
      }
      else -> {
      }
    }
    //如果是移动事件不触发OnClick事件,防止移动的时候一放手形成点击事件
    return isMove
  }
}

All codes of floatwinfowservices are as follows:

class FloatWinfowServices : Service() {
   private var winManager: WindowManager? = null
  private var wmParams: WindowManager.LayoutParams? = null
  private var inflater: LayoutInflater? = null
  //浮动布局
  private var mFloatingLayout: View? = null
  private var linearLayout: LinearLayout? = null
  private var chronometer: Chronometer? = null
  override fun onBind(intent: Intent): IBinder? {
    initWindow()
    //悬浮框点击事件的处理
    initFloating()
    return MyBinder()
  }
  inner class MyBinder : Binder() {
    val service: FloatWinfowServices
      get() = this@FloatWinfowServices
  }
  override fun onCreate() {
    super.onCreate()
  }
  /**
   * 悬浮窗点击事件
   */
  private fun initFloating() {
    linearLayout = mFloatingLayout!!.findViewById<LinearLayout>(R.id.line1)
    linearLayout!!.setOnClickListener { startActivity(Intent(this@FloatWinfowServices,Main2Activity::class.java)) }
    //悬浮框触摸事件,设置悬浮框可拖动
    linearLayout!!.setOnTouchListener(FloatingListener())
  }
  //开始触控的坐标,移动时的坐标(相对于屏幕左上角的坐标)
  private var mTouchStartX: Int = 0
  private var mTouchStartY: Int = 0
  private var mTouchCurrentX: Int = 0
  private var mTouchCurrentY: Int = 0
  //开始时的坐标和结束时的坐标(相对于自身控件的坐标)
  private var mStartX: Int = 0
  private var mStartY: Int = 0
  private var mStopX: Int = 0
  private var mStopY: Int = 0
  //判断悬浮窗口是否移动,这里做个标记,防止移动后松手触发了点击事件
  private var isMove: Boolean = false
  private inner class FloatingListener : View.OnTouchListener {
    override fun onTouch(v: View,event: MotionEvent): Boolean {
      val action = event.action
      when (action) {
        MotionEvent.ACTION_DOWN -> {
          isMove = false
          mTouchStartX = event.rawX.toInt()
          mTouchStartY = event.rawY.toInt()
          mStartX = event.x.toInt()
          mStartY = event.y.toInt()
        }
        MotionEvent.ACTION_MOVE -> {
          mTouchCurrentX = event.rawX.toInt()
          mTouchCurrentY = event.rawY.toInt()
          wmParams!!.x += mTouchCurrentX - mTouchStartX
          wmParams!!.y += mTouchCurrentY - mTouchStartY
          winManager!!.updateViewLayout(mFloatingLayout,wmParams)
          mTouchStartX = mTouchCurrentX
          mTouchStartY = mTouchCurrentY
        }
        MotionEvent.ACTION_UP -> {
          mStopX = event.x.toInt()
          mStopY = event.y.toInt()
          if (Math.abs(mStartX - mStopX) >= 1 || Math.abs(mStartY - mStopY) >= 1) {
            isMove = true
          }
        }
        else -> {
        }
      }
      //如果是移动事件不触发OnClick事件,防止移动的时候一放手形成点击事件
      return isMove
    }
  }
  /**
   * 初始化窗口
   */
  private fun initWindow() {
    winManager = application.getSystemService(Context.WINDOW_SERVICE) as WindowManager
    //设置好悬浮窗的参数
    wmParams = params
    // 悬浮窗显示左上角为起始坐标
    wmParams!!.gravity = Gravity.LEFT or Gravity.TOP
    //悬浮窗的开始位置,因为设置的是从左上角开始,所以屏幕左上角是x=0;y=0
    wmParams!!.x = winManager!!.defaultDisplay.width
    wmParams!!.y = 210
    //得到容器,通过这个inflater来获得悬浮窗控件
    inflater = LayoutInflater.from(applicationContext)
    // 获取浮动窗口视图所在布局
    mFloatingLayout = inflater!!.inflate(R.layout.remoteview,null)
    chronometer = mFloatingLayout!!.findViewById<Chronometer>(R.id.chronometer)
    chronometer!!.start()
    // 添加悬浮窗的视图
    winManager!!.addView(mFloatingLayout,wmParams)
  }
  private //设置window type 下面变量2002是在屏幕区域显示,2003则可以显示在状态栏之上
      //设置可以显示在状态栏上
      //设置悬浮窗口长宽数据
  val params: WindowManager.LayoutParams
    get() {
      wmParams = WindowManager.LayoutParams()
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        wmParams!!.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
      } else {
        wmParams!!.type = WindowManager.LayoutParams.TYPE_PHONE
      }
      wmParams!!.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
          WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR or
          WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
      wmParams!!.width = WindowManager.LayoutParams.WRAP_CONTENT
      wmParams!!.height = WindowManager.LayoutParams.WRAP_CONTENT
      return wmParams
    }
  override fun onStartCommand(intent: Intent,flags: Int,startId: Int): Int {
    return super.onStartCommand(intent,flags,startId)
  }
  override fun onDestroy() {
    super.onDestroy()
    winManager!!.removeView(mFloatingLayout)
  }
}

Some other problems to be considered in practical application

In the process of using, we will certainly encounter other problems:

1. The user may press the home key directly during use. How to prompt at this time?

Cause of the problem: after the user presses the home key, the developer cannot rewrite the logic of the home key. At this time, the application is not running in the foreground and cannot pop up a window to remind. At this time, the user clicks the app icon to enter the first stack. At this time, the user does not enter the entrance of the call page.

Solution:

The first solution we can follow the example of wechat is to open a foreground notification during the whole call process, and the user will enter the call page when clicking the notification.

The second solution is to detect whether the application is in the foreground. When the call page is running and the application returns to the foreground, we broadcast to other pages and prompt permission guidance.

2. On the call page (singleinstance mode), click home

When the application is running in the background, the call ends and the activity is finished. At this time, switch back to the application from the task program, and you will find that the call page opens!

The problem is simply that if you call someone on the call page, press the home key during the call, and then hang up. At this time, you switch back to the application from the task program and call the person again, that is, you return to the oncreate method again in this state.

Causes of problems:

1. Because the call page is in singleinstance mode, there are two task stacks. Press home and then switch back from the task program. At this time, the application only retains the second task stack and has lost its relationship with the first task stack. After finishing, it cannot return to the first task stack.

Solution:

1. (not recommended) the singleinstance mode is not used on the call page. In this case, other functions of the software cannot be operated during the call, which is generally not taken.

2. (my current solution) set a flag bit to mark whether the call is currently in progress. In oncreate, if the call has ended, jump to a transition page (standard mode) and finish in the transition page. The reason for adding the transition page is that we don't know where the previous page is, because the incoming call may be any page, After the transition page find, we return to the first task stack again.

summary

The above is what Xiaobian introduced to you. Android realizes the floating window function. I hope it will help you. If you have any questions, please leave me a message, and Xiaobian will reply to you in time. Thank you very much for your support to our website! If you think this article is helpful to you, welcome to reprint, please indicate the source, thank you!

The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
THE END
分享
二维码
< <上一篇
下一篇>>