湘江北上 發表於 2022-7-20 14:10:00

Android开发 悬浮窗开发

<h1><span style="color: rgba(0, 128, 128, 1)">版权声明</span></h1>
<p>本文来自博客园,作者:观心静&nbsp;,转载请注明原文链接:https://www.cnblogs.com/guanxinjing/p/16497765.html</p>
<div>本文版权归作者和博客园共有,欢迎转载,但必须给出原文链接,并保留此段声明,否则保留追究法律责任的权利。</div>
<h1><span style="color: rgba(0, 128, 128, 1)">前言</span></h1>
<p><span style="color: rgba(0, 0, 0, 1)">  此博客讲解的悬浮窗开发,悬浮窗开发需要众多权限。首先就需要在设置-应用-权限里被允许弹出悬浮窗。</span></p>
<p>&nbsp;</p>
<h1><span style="color: rgba(0, 128, 128, 1)">使用服务或者</span><span style="color: rgba(0, 128, 128, 1)">Application</span><span style="color: rgba(0, 128, 128, 1)">启动悬浮窗</span></h1>
<p><span style="color: rgba(0, 0, 0, 1)"><span style="color: rgba(255, 0, 0, 1)"><strong>悬浮窗部分需要注意的地方</strong></span>,<strong>Service与</strong></span><strong><span style="color: rgba(0, 0, 0, 1)">Application</span></strong><strong>的Context</strong>是没有前台View的(如果你传入的是正在运行的Activity的Context可以不关注下面TYPE_SYSTEM_ALERT部分)。所以,你如果不增加&nbsp;</p>
<div class="cnblogs_code">
<pre>layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY</pre>
</div>
<p>或者</p>
<div class="cnblogs_code">
<pre>layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT</pre>
</div>
<p>就会出现如下报错:</p>
<div class="cnblogs_code">
<pre>Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token <span style="color: rgba(0, 0, 255, 1)">null</span> is not valid; is your activity running?</pre>
</div>
<p>所以,如果你需要使用服务启动悬浮窗layoutParams,必定需要根据Android版本添加上面的其中一项。 另外你还需要在AndroidManifest.xml中添加权限:</p>
<div class="cnblogs_code">
<pre>    <span style="color: rgba(0, 0, 255, 1)">&lt;</span><span style="color: rgba(128, 0, 0, 1)">uses-permission </span><span style="color: rgba(255, 0, 0, 1)">android:name</span><span style="color: rgba(0, 0, 255, 1)">="android.permission.SYSTEM_ALERT_WINDOW"</span> <span style="color: rgba(0, 0, 255, 1)">/&gt;</span></pre>
</div>
<p><span style="color: rgba(0, 0, 0, 1)">关于</span>TYPE_SYSTEM_ALERT的官方注解如下:</p>
<div class="cnblogs_code">
<pre>    <span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
    窗口类型:
    应用程序覆盖窗口显示在所有活动窗口上方(类型介于 FIRST_APPLICATION_WINDOW 和 LAST_APPLICATION_WINDOW 之间),
    但在状态栏或 IME 等关键系统窗口下方。系统可以随时更改这些窗口的位置、大小或可见性,以减少用户的视觉混乱并管理资源。
    需要 android.Manifest.permission.SYSTEM_ALERT_WINDOW 权限。
    系统将调整具有此窗口类型的进程的重要性,以减少低内存杀手杀死它们的机会。在多用户系统中,仅在拥有用户的屏幕上显示。
   </span><span style="color: rgba(0, 128, 0, 1)">*/</span></pre>
</div>
<h1><span style="color: rgba(0, 128, 128, 1)">悬浮窗的layoutParams.flags</span></h1>
<table style="height: 262px; width: 881px" border="0">
<tbody>
<tr>
<td>WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE</td>
<td>表示Window不需要获取焦点,也不需要接受各种输入事件,此标记会同时启用FLAG_NOT_TOUCH_MODAL,最终事件会直接传递给下层的具有焦点的Window</td>
</tr>
<tr>
<td>&nbsp;WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL</td>
<td>在此模式下,系统会将当前Window区域以外的单击事件传递给底层的Window,当前Window区域以内的单击事件则自己处理。这个标记很重要,一般来说都需要开启此标记,否则其他Window将无法收到单击事件</td>
</tr>
<tr>
<td>&nbsp;WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED</td>
<td>开启此模式可以让Window显示在锁屏的界面上</td>
</tr>
<tr>
<td>&nbsp;WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON&nbsp;</td>
<td>保存屏幕常亮</td>
</tr>
<tr>
<td>&nbsp;WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS</td>
<td>允许窗口延伸到屏幕之外</td>
</tr>
<tr>
<td>&nbsp;WindowManager.LayoutParams.FLAG_DIM_BEHIND</td>
<td>此窗口后面的所有内容都将变暗。使用 dimAmount 来控制暗淡的量</td>
</tr>
<tr>
<td>&nbsp;WindowManager.LayoutParams.FLAG_FULLSCREEN&nbsp;</td>
<td>全屏显示</td>
</tr>
<tr>
<td>&nbsp;WindowManager.LayoutParams.FLAG_SECURE&nbsp; &nbsp;</td>
<td>安全模式,将窗口内容视为安全,防止其出现在屏幕截图中或在非安全显示器上查看</td>
</tr>
<tr>
<td>&nbsp;WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED&nbsp;</td>
<td>硬件加速</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<h1><span style="color: rgba(0, 128, 128, 1)">实现代码</span></h1>
<p><span style="color: rgba(0, 0, 0, 1)">代码则很简单,关键点下面有注释</span></p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> android.app.Service
</span><span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> android.content.Context
</span><span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> android.content.Intent
</span><span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> android.graphics.PixelFormat
</span><span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> android.os.Build
</span><span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> android.os.IBinder
</span><span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> android.view.Gravity
</span><span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> android.view.LayoutInflater
</span><span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> android.view.View
</span><span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> android.view.WindowManager
</span><span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> com.lwlx.ai.databinding.AiDialogAiBinding
</span><span style="color: rgba(0, 0, 255, 1)">import</span><span style="color: rgba(0, 0, 0, 1)"> com.lwlx.common.expand.setOnIntervalClickListener

</span><span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)"> AiService : Service() {
    val binding by lazy { AiDialogAiBinding.inflate(LayoutInflater.from(application)) }
    val windowManager by lazy { application.getSystemService(WINDOW_SERVICE) as WindowManager }

    companion object {
      </span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
         * 启动服务
         </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
      fun startService(context: Context) {
            val intent </span>= Intent(context, AiService::<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">.java)
            context.startService(intent)
      }
    }

    override fun onBind(intent: Intent): IBinder {
      TODO(</span>"Return the communication channel to the service."<span style="color: rgba(0, 0, 0, 1)">)
    }

    override fun onCreate() {
      </span><span style="color: rgba(0, 0, 255, 1)">super</span><span style="color: rgba(0, 0, 0, 1)">.onCreate()
    }

    override fun onStartCommand(intent: Intent</span>?<span style="color: rgba(0, 0, 0, 1)">, flags: Int, startId: Int): Int {
      initView()
      showView()
      </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">super</span><span style="color: rgba(0, 0, 0, 1)">.onStartCommand(intent, flags, startId)
    }

    </span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> fun initView(){
      binding.button.setOnIntervalClickListener {
            hideView()
      }
    }

    fun showView() {
      windowManager.addView(binding.root, getWindowLayoutParams())
    }

    fun hideView() {
      windowManager.removeView(binding.root)
    }

    </span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> fun getWindowLayoutParams(): WindowManager.LayoutParams {
      val layoutParams </span>=<span style="color: rgba(0, 0, 0, 1)"> WindowManager.LayoutParams()
      </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">设置宽高</span>
      layoutParams.width =<span style="color: rgba(0, 0, 0, 1)"> WindowManager.LayoutParams.MATCH_PARENT
      layoutParams.height </span>= 300
      <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">设置显示位置</span>
      layoutParams.gravity =<span style="color: rgba(0, 0, 0, 1)"> Gravity.BOTTOM or Gravity.END
      layoutParams.format </span>=<span style="color: rgba(0, 0, 0, 1)"> PixelFormat.RGBA_8888
      </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">设置状态栏与导航栏效果</span>
      layoutParams.systemUiVisibility =<span style="color: rgba(0, 0, 0, 1)"> View.SYSTEM_UI_FLAG_FULLSCREENor View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or View.SYSTEM_UI_FLAG_IMMERSIVE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
      </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">设置悬浮窗效果</span>
      <span style="color: rgba(0, 0, 255, 1)">if</span> (Build.VERSION.SDK_INT &gt;=<span style="color: rgba(0, 0, 0, 1)"> Build.VERSION_CODES.O) {
            layoutParams.flags </span>=<span style="color: rgba(0, 0, 0, 1)"> WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
                  WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
                  WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
            layoutParams.type </span>=<span style="color: rgba(0, 0, 0, 1)"> WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
      } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
            layoutParams.type </span>=<span style="color: rgba(0, 0, 0, 1)"> WindowManager.LayoutParams.TYPE_SYSTEM_ALERT
            layoutParams.flags </span>=<span style="color: rgba(0, 0, 0, 1)"> WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
                  WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
                  WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
                  WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
                  WindowManager.LayoutParams.FLAG_FULLSCREEN
      }
      </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> layoutParams
    }
}</span></pre>
</div>
<h1><span style="color: rgba(22, 145, 121, 1)">使用最新的Compose开发悬浮窗</span></h1>
<p><span style="color: rgba(0, 0, 0, 1)">其他与上面的一样,难点是Compose是需要生命周期的,通常情况下这个生命周期是Activity提供的,但是在后台服务中没有这样的生命周期。所以我们需要自己创建生命周期类并且自己控制。</span></p>
<div>
<pre class="highlighter-hljs" data-dark-theme="true"><code>
import android.os.Bundle
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner

class ComposeViewLifecycle :LifecycleOwner, SavedStateRegistryOwner {
    private val savedStateRegistryController = SavedStateRegistryController.create(this)
    override val savedStateRegistry: SavedStateRegistry = savedStateRegistryController.savedStateRegistry

    private val lifecycleRegistry = LifecycleRegistry(this)
    override val lifecycle: Lifecycle get() = lifecycleRegistry

    init {
      savedStateRegistryController.performRestore(null)
      // 初始化 Lifecycle 为 RESUMED 状态
      lifecycleRegistry.currentState = Lifecycle.State.RESUMED
    }

    fun onDestroy() {
      lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
    }

    fun performSave(outBundle: Bundle) {
      savedStateRegistryController.performSave(outBundle)
    }
}</code></pre>
</div>
<p><span style="color: rgba(0, 0, 0, 1)">在悬浮窗中导入使用</span></p>
<div>
<div>
<pre class="highlighter-hljs" data-dark-theme="true"><code>private fun showMemoryInfoWindow() {
    mWindowManager.addView(getWindowView(), getWindowLayoutParams())
}

private fun hideMemoryInfoWindow() {
    if(isStart) {
      mWindowManager.removeView(getWindowView())
      mComposeView = null
    }
}

private fun updateMemoryInfo(mode:Int) {
    if(mode == 0){
      mLayoutParams.width = 50
      mLayoutParams.height = 50
    } else {
      mLayoutParams.width = WindowManager.LayoutParams.MATCH_PARENT
      mLayoutParams.height = 400
    }
    mWindowManager.updateViewLayout(getWindowView(), mLayoutParams)
}

private fun getWindowView(): ComposeView {
    if (mComposeView == null) {
      val compose = ComposeViewLifecycle()
      mComposeView = ComposeView(mApplication).apply {
            setViewTreeLifecycleOwner(compose)
            setViewTreeSavedStateRegistryOwner(compose)
      }
      mComposeView?.setContent {
            MemoryView()
      }
    }
    return mComposeView!!
}</code></pre>
</div>
</div>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><span style="color: rgba(0, 0, 0, 1)">End</span></p>

</div>
<div id="MySignature" role="contentinfo">
    <div style="text-align: center">
    <p style="color:orange;font-size:16px;" >本文来自博客园,作者:观心静 ,转载请注明原文链接:https://www.cnblogs.com/guanxinjing/p/16497765.html </p>
    <div style="color:orange;font-size:16px;">本文版权归作者和博客园共有,欢迎转载,但必须给出原文链接,并保留此段声明,否则保留追究法律责任的权利。 </div>
</div><br><br>
来源:https://www.cnblogs.com/guanxinjing/p/16497765.html
頁: [1]
查看完整版本: Android开发 悬浮窗开发