权限

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

Allows an app to create windows using the type TYPE_SYSTEM_ALERT, shown on top of all other apps. Very few apps should use this permission; these windows are intended for system-level interaction with the user. Note: If the app targets API level 23 or higher, the app user must explicitly grant this permission to the app through a permission management screen. The app requests the user’s approval by sending an intent with action ACTION_MANAGE_OVERLAY_PERMISSION. The app can check whether it has this authorization by calling Settings.canDrawOverlays(). Protection level: signature Constant Value: “android.permission.SYSTEM_ALERT_WINDOW”

Window设置

  1. dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);

如果是 M0 以前的系统,这个时候就可以 show 了;但自 M0 开始这么 show 就会报错了。
如前面注释所说,自M0开始,显示系统级对话框,需要单独开启这个权限。这时我们可以引导用户手动打开权限,注释中也给出了提示,这里提供一个方案:

  1. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  2. if (!Settings.canDrawOverlays(this)) {
  3. Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
  4. Uri.parse("package:" + getPackageName()));
  5. startActivityForResult(intent, 100);
  6. }
  7. }

上面的代码就会跳转到相应的设置界面,当用户返回到APP时,我们还应该通过如下代码再次确认权限是否已打开:

  1. @Override
  2. protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  3. if (100 == requestCode) {
  4. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  5. if (!Settings.canDrawOverlays(this)) {
  6. // SYSTEM_ALERT_WINDOW permission not granted...
  7. }
  8. else{
  9. //执行其它逻辑
  10. }
  11. }
  12. }
  13. }

踩坑

1、MIUI 问题

MIUI 的权限管理改的有点厉害,权限问题表现和原版Android,甚至大多数ROM表现都不太一致,在适配 MIUI 的时候一定要多小心。

2、AlertDialog 导包问题

这里我写 AlertDialog 时,AS 提示有如下两个类可选:

  1. android.support.v7.app.AlertDialog
  2. android.app.AlertDialog

一开始我想,support 包的应该兼容性更好,就用了 support 包的,结果 run 的时候报错了:

Process: com.example.sevenun.littledemo, PID: 2085 java.lang.RuntimeException: Unable to start receiver xxx: java.lang.IllegalStateException: You need to use a Theme.AppCompat theme (or descendant) with this activity.

提示信息已经很明确了,theme 不对,可怎么解呢?

一开始我想可能是 Service 的 context 没有 Theme,或者 Theme 不对,于是试了
ApplicationContext,发现还是不行。
然后尝试 create dialog 的时候,手动设置一个 Theme.AppCompat 的 Theme,结果还是不行。
最后才想到会不会是导包不对,然后换了 android.app.AlertDialog,运行就OK了。

3、Looper 问题

如果要在 service 使用,直接调用即可,如果是在 service 的线程中显示对话框,因为自行创建的线程没有 Looper,因此需要添加代码如下:

  1. Looper.prepare();
  2. //TODO: showDialog()
  3. Looper.loop();

若不添加 Looper,则会报错。

4、AlertDialog自定义布局问题

实际使用中,有时候需要自定义 Dialog 布局,一般情况下我们是这么做的:

  1. val view = View.inflate(this, R.layout.dialog_test, null) //填充布局
  2. val title = view.findViewById<TextView>(R.id.title) //绑定控件
  3. title.text = "这里是Title" //设置数据
  4. val dialog = android.app.AlertDialog.Builder(this)
  5. .setView(view) //设置自定义布局给Dialog
  6. .create()
  7. dialog.show()

这样做没毛病,就是有点繁琐,有没有更好的办法呢,比如像 Preference 替换自定义布局一样,只需要设置资源ID即可。

我们知道 Preference 替换布局资源时,自需要将对应控件的 ID 设为系统默认的 ID,即可自动完成绑定,就像这样:(注意ID的写法)

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. android:layout_width="match_parent"
  3. android:layout_height="match_parent"
  4. android:gravity="center_horizontal"
  5. android:orientation="vertical"
  6. android:padding="8dp">
  7. <TextView
  8. android:id="@android:id/title"
  9. android:layout_width="wrap_content"
  10. android:layout_height="wrap_content"
  11. android:layout_marginTop="8dp"
  12. android:gravity="center"
  13. android:textColor="@color/colorAccent"
  14. android:textSize="24sp" />
  15. <TextView
  16. android:id="@android:id/message"
  17. android:layout_width="wrap_content"
  18. android:layout_height="wrap_content"
  19. android:layout_marginTop="8dp"
  20. android:gravity="center"
  21. android:textColor="@color/lighter_gray" />
  22. </LinearLayout>

用相同的方法,试下 Dialog:

  1. val dialog = android.app.AlertDialog.Builder(this)
  2. .setView(R.layout.dialog_test)
  3. .setTitle("Title")
  4. .setMessage("Message")
  5. .create()
  6. dialog.show()

可实际情况是,Title 还是原来的 Title,Message 还是原来的 Message,新的布局并没有替换原始的,而是成为了原始布局的一部分,被添加在底部了

于是就想,是不是因为Builder不支持这么替换布局呢?那就换个方式:

  1. val dialog = android.app.AlertDialog.Builder(this).create()
  2. dialog.setContentView(R.layout.dialog_wifi_fence)
  3. dialog.setTitle("Title")
  4. dialog.setMessage("Message")
  5. dialog.show()

重点来了!

这样写,看起来没问题,但实际运行就会报错了:

requestFeature() must be called before adding content

requestFeature 这个方法我们在 Activity 里面设置无标题栏时用过:

  1. protected void onCreate(Bundle savedInstanceState) {
  2. super.onCreate(savedInstanceState)
  3. window.requestFeature(Window.FEATURE_NO_TITLE)
  4. this.setContentView(R.layout.activity_debug)
  5. }

我们知道,这个时候如果 setContentView -> requestFeature 就会报同样的错误。
Android 上,能够 show 给用户的,都和 Window 有联系,那 Dialog 是什么时候和 Window 联系上的呢?
简单看了下实现过程,发现是 show 的时候,才被add上来:

  1. public void show() {
  2. ······
  3. mWindowManager.addView(mDecor, l);
  4. mShowing = true;
  5. ······
  6. }

按照 Activity 那样先和 Window 联系上,再设置自己的布局的方式,把 Dialog 修改一下:

  1. val dialog = android.app.AlertDialog.Builder(this).create()
  2. dialog.show()
  3. dialog.setContentView(R.layout.dialog_wifi_fence)
  4. dialog.setTitle("Title")
  5. dialog.setMessage("Message")
  6. //或者
  7. val dialog = android.app.AlertDialog.Builder(this).create()
  8. dialog.setTitle("Title")
  9. dialog.setMessage("Message")
  10. dialog.show()
  11. dialog.setContentView(R.layout.dialog_wifi_fence)
  12. //或者
  13. val dialog = android.app.AlertDialog.Builder(this).create()
  14. dialog.show()
  15. dialog.setTitle("Title")
  16. dialog.setMessage("Message")
  17. dialog.setContentView(R.layout.dialog_wifi_fence)

结果发现,只要 show -> setContentView 就不会报错,但是 setTitle、setMessage 却不会生效,看来 Dialog 替换默认布局不能像 Preference 那么玩。

参考链接