Android 8(O)

通知渠道

在过去,用户是没办法对信息做区分的,要么同意接受所有信息,要么屏蔽所有信息,这也是 Android 通知功能的痛点。于是,Android 8.0 系统引入了通知渠道这个概念。

通知渠道,就是每条通知都要属于一个对应的渠道。每个应用程序都可以自由地创建当前应用拥有哪些通知渠道,但是这些通知渠道的控制权是掌握在用户手上的。用户可以自由地选择这些通知渠道的重要程度,是否响铃、是否振动或者是否要关闭这个渠道的通知。

通知渠道一旦创建之后就不能再修改了,因此开发者需要仔细分析自己的应用程序一共有哪些类型的通知,然后再去创建相应的通知渠道。

创建通知渠道

需要一个 NotificationManager 对通知进行管理,可以通过调用 Context 的 getSystemService() 获取。getSystemService() 接收一个字符串参数用于确定获取系统的哪个服务,这里传入 Context.NOTIFICATION_SERVICE 即可。

1
2
// 获取 NotificationManager 的实例
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用 NotificationChannel 类构建一个通知渠道,
// 并调用 NotificationManager 的 createNotificationChannel() 完成创建。
// NotificationChannel 类和 createNotificationChannel() 都是 Android 8.0 系统中新增的 API
// 所以在使用时需要先进行版本判断
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.0){
// 创建一个通知渠道至少需要渠道 ID、渠道名称以及重要等级这 3 个参数:
// 渠道 ID 可以随便定义,只需要保证全局唯一性。
// 渠道名称是给用户看的,需要可以清楚地表达这个渠道的用途。
// 通知的重要等级主要有 IMPORTANCE_HIGH、IMPORTANCE_DEFAULT、IMPORTANCE_LOW、IMPORTANCE_MIN
// 对应的重要程度依次从高到低。不同等级会决定通知的不同行为。
//(这里只是初始状态下的等级,用户可随时手动更改某个通知的重要等级,开发者是无法干预的)
val channel = NotificationChannel(channelId,channelName,importance)
manager.createNotificationChannel(channel)
}

通知的基本用法

相比于 BroadcastReceiver 和 Service,在 Activity 里创建通知的场景还是比较少的,因为一般只有当程序进入后台时才需要使用通知。

AndroidX 库中提供了一个 NotificationCompat 类,使用这个类的构造器创建 Notification 对象,可以保证程序在所有 Android 系统版本上都能正常工作。

1
2
3
// 使用 Builder 构造器创建 Notification 对象。
// 第二个参数是渠道 ID,需要和我们创建通知渠道时指定的渠道 ID 相匹配。
val notification = NotificationCompat.Builder(context,channelId).build()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 基本设置
val notification = NotificationCompat.Builder(context,channelId)
.setContentTitle("This is content title")
.setContentText("This is content text")
// 只能使用纯 alpha 图层的图片进行设置,小图标会显示在系统状态栏上。
.setSmallIcon(R.drawable.small_icon)
// 大图标,当下拉系统状态栏时,可以看到。
.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.drawable.large_icon))
.build()

// 显示通知
// 第一个参数:id,要保证为每个通知指定的 id 都是不同的。
// 第二个参数:Notification 对象。
manager.notify(1,notification)

示例

首先获取了 NotificationManager 的实例,并创建了一个 ID 为 normal 通知渠道。

在按钮的点击事件里完成了通知的创建工作,通过 PendingIntent 添加点击效果,取消通知的方式有两种。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

// 这里是英文字母 O,不是数字 0,尴尬。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
// 创建了一个 ID 为 normal 通知渠道。
// 创建通知渠道的代码只在第一次执行时才会创建,当下次再执行创建代码时,
// 系统会检测到该通知渠道已存在,因此不会重复创建,也并不会影响运行效率。
val channel = NotificationChannel("normal","Normal",
NotificationManager.IMPORTANCE_DEFAULT)
manager.createNotificationChannel(channel)
}
// 注意在手机设置里,开启消息通知.
btnSendNotice.setOnClickListener{
// 表达想要启动 CameraAlbumActivity 的意图。
// 可在 CameraAlbumActivity 中显示地调用 NotificationManager 的 cancel() 取消通知。
// val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// 点击这个通知时,通知会自动取消。否则通知消息会一直留存在系统状态栏上。
// 这个 1 是指在创建通知时给每条通知指定的 id,想取消哪条通知,就传入该通知的 id 即可。
// manager.cancel(1)
val intent = Intent(this,CameraAlbumActivity::class.java)
// PendingIntent 和 Intent 有些类似,它们都可以指明某一个"意图",都可以用于启动 Activity、启动 Service 以及发送广播等。
// 不同的是,Intent 倾向于立即执行某个动作。而 PendingIntent 倾向于在某个合适的时机执行某个动作,也可以简单地理解为延迟执行的 Intent。
// 第二个参数一般用不到,传入 0 即可。
// 第三个参数是一个 Intent 对象,可以通过这个对象构建出 PendingIntent 的"意图"。
// 第四个参数用于确定 PendingIntent 的行为,一把传入 0 即可。(其它可选值:FLAG_ONE_SHOT,FLAG_CANCEL_CURRENT,FLAG_NO_CREATE,FLAG_UPDATE_CURRENT)
val pi = PendingIntent.getActivity(this,0,intent,0)

val notification = NotificationCompat.Builder(this,"normal")
.setContentTitle("This is content title")
.setContentText("This is content text")
.setSmallIcon(R.drawable.small_icon)
.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.drawable.large_icon))
.setContentIntent(pi) // 添加点击效果
// .setAutoCancel(true) // 点击这个通知时,通知会自动取消。否则通知消息会一直留存在系统状态栏上。
.build()

manager.notify(1,notification)
}

通知的进阶技巧

NotificationCompat.Builder 中提供了非常丰富的 API,以便我们创建出更加多样的通知效果。

setStyle():这个方法允许我们构建出富文本的通知内容。也就是说,通知中不光可以有文字和图标,还可以包含更多的东西。接收一个 NotificationCompat.Style 参数,用来构建具体的富文本信息,如长文字、图片等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 我的手机品牌是 iQOO 第一代,系统为 Android 10。
* 程序安装后,需要手动到设置内通知管理将通知打开,否则点击按钮是没反应的。
* 长文字信息和大图,默认是折叠状态,需要手动下拉一下才能看到效果。
*/
val notification = NotificationCompat.Builder(this,"normal")
// 使用 setStyle() 替代,展示长文字信息,或者是显示一张大图
// .setContentText("This is content text")
// 展示长文字
.setStyle(NotificationCompat.BigTextStyle()
.bigText("Learn how to build notifications, send and sync data, and use voice actions, Get the official Android IDE and developer tools to build apps for Android."))
// 展示大图
// .setStyle(NotificationCompat.BigPictureStyle()
// .bigPicture(BitmapFactory.decodeResource(resources,R.drawable.big_image)))
.build()

重要等级

通知渠道的重要等级越高,发出的通知就越容易获得用户的注意。比如高重要等级的通知渠道发出的通知可以弹出横幅、发出声音,而低重要等级的通知渠道发出的通知不仅可能会在某些情况下被隐藏、而且可能会被改变显示的顺序。

并且,开发者只能在创建通知渠道时为它指定初始的重要等级,如果用户不认可这个重要等级的话,可以随时进行修改,开发者对此无权再进行调整和变更,因为通知渠道一旦创建就不能再通过代码修改了。

示例(Kotlin)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class NotificationActivity:BaseActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_notification)

initData()
}

private fun initData() {
// 使用 Builder 构造器创建 Notification 对象。
// 第二个参数是渠道 ID,需要和我们创建通知渠道时指定的渠道 ID 相匹配。
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

// 这里是英文字母 O,不是数字 0,尴尬。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
// 创建了一个 ID 为 normal 通知渠道。
// 创建通知渠道的代码只在第一次执行时才会创建,当下次再执行创建代码时,
// 系统会检测到该通知渠道已存在,因此不会重复创建,也并不会影响运行效率。
val channel = NotificationChannel("normal","Normal",
NotificationManager.IMPORTANCE_DEFAULT)
// 创建一个重要等级的通知渠道。
val channel2 = NotificationChannel("important","Important",
NotificationManager.IMPORTANCE_HIGH)
manager.createNotificationChannel(channel)
manager.createNotificationChannel(channel2)
}

// 注意在手机设置里,开启消息通知.
btnSendNotice.setOnClickListener{
// 表达想要启动 CameraAlbumActivity 的意图。
// 可在 CameraAlbumActivity 中显示地调用 NotificationManager 的 cancel() 取消通知。
val intent = Intent(this, CameraAlbumActivity::class.java)
// PendingIntent 和 Intent 有些类似,它们都可以指明某一个"意图",都可以用于启动 Activity、启动 Service 以及发送广播等。
// 不同的是,Intent 倾向于立即执行某个动作。而 PendingIntent 倾向于在某个合适的时机执行某个动作,也可以简单地理解为延迟执行的 Intent。
// 第二个参数一般用不到,传入 0 即可。
// 第三个参数是一个 Intent 对象,可以通过这个对象构建出 PendingIntent 的"意图"。
// 第四个参数用于确定 PendingIntent 的行为,一把传入 0 即可。(其它可选值:FLAG_ONE_SHOT,FLAG_CANCEL_CURRENT,FLAG_NO_CREATE,FLAG_UPDATE_CURRENT)
val pi = PendingIntent.getActivity(this,0,intent,0)

val notification = NotificationCompat.Builder(this,"normal")
.setContentTitle("This is content title")
// .setContentText("This is content text") // 使用 setStyle() 替代,展示长文字信息,或者是显示一张大图
.setStyle(NotificationCompat.BigTextStyle() // 展示长文字,在 iQOO Android 10 的手机上,默认折叠。
.bigText("Learn how to build notifications, send and sync data, and use voice actions, Get the official Android IDE and developer tools to build apps for Android."))
.setSmallIcon(R.drawable.small_icon)
.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.drawable.large_icon))
.setContentIntent(pi) // 添加点击效果
// .setAutoCancel(true) // 点击这个通知时,通知会自动取消。否则通知消息会一直留存在系统状态栏上。这里使用了另一种方式。
.build()
manager.notify(1,notification)

// 将通知渠道的重要等级设置成了 "高"。关于弹出横幅,实际上在我的手机默认是关闭横幅通知的。
val notification2 = NotificationCompat.Builder(this,"important")
.setContentTitle("This is content title")
.setStyle(NotificationCompat.BigPictureStyle() // 展示大图,在 iQOO Android 10 的手机上,默认折叠。
.bigPicture(BitmapFactory.decodeResource(resources,R.drawable.big_image)))
.setSmallIcon(R.drawable.small_icon)
.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.drawable.large_icon))
.setContentIntent(pi) // 添加点击效果
.setAutoCancel(true) // 点击这个通知时,通知会自动取消。否则通知消息会一直留存在系统状态栏上。
.build()
manager.notify(2,notification2)
}
}

companion object{
fun actionStart(context: Context){
val intent = Intent(context, NotificationActivity::class.java)
context.startActivity(intent)
}
}
}

App 图标

在过去,Android 应用程序的图标都应该放到相应分辨率的 mipmap 目录下,不过从 Android 8.0 系统开始,Google 已经不再建议使用单一的一张图片来作为应用程序的图标,而是应该使用前景和背景分离的图标设计方式。

具体来讲,应用程序的图标应该被分为两层:前景层和背景层。前景层用来展示应用图标的 Logo,背景层用来衬托应用图标的 Logo。

并且要注意,背景层在设计时只允许定义颜色和纹理,不能定义形状。图标的形状由手机厂商来定义,手机厂商会在图标的前景层和背景层之上再盖上一层 mask,这个 mask 可以是圆角矩形、圆形或是方形等,视具体手机厂商而定,这样就可以将手机上所有程序的图标都裁剪成相同的形状,从而统一图标的设计规范。

然后借助 Android Studio 提供的 Asset Studio 工具来制作能够兼容各个 Android 系统版本的应用程序图标。单击导航栏中 File -> New -> Image Asset 打开工具。(左边是操作区域,右边是预览区域)

操作区域:

  • Icon Type:保持默认即可,表示同时创建兼容 8.0 系统以及老版本系统的应用图标。
  • Name:应用图标的名称,保持 ic_launcher 的命名即可,这样可以覆盖掉之前自动生成的应用程序图标。
  • Foreground Layer 页签:用于编辑前景层
    • Path:图片路径
    • Resize:拖动条对图片进行缩放,以保证前景层的所有内容都是在安全区域中。
  • Background Layer 页签:用于编辑背景层
    • Asset Type:选取 Color 模式,并设置颜色值作为背景层的颜色。
  • Legacy 页签:用于编辑老版本系统的图标

预览区域:

它的主要作用就是预览应用图标在应对不同类型的 mask 的最终效果。在预览区域中给出了可能生成的图标形状。并且每个预览图标中都有一个圆圈,它叫做安全区域,必须保证图标的前景层完全处于安全区域中才行,否则可能会出现应用图标的 Logo 被手机厂商的 mask 裁减掉的情况。

  • Circle:圆形

  • Squircle:方圆形

  • Rounded Square:圆角正方形

  • Square:正方形

接下来点击 “Next” 会进入一个确认图标生成路径的界面,然后直接点击界面上的 “Finish” 按钮完成图标的制作。所有图标相关的文件都会被生成到相应分辨率的 mipmap 目录下,其中有个 mipmap-anydpi-v26 目录中放的并不是图片,而是 xml 文件,这是因为 Android 8.0 及以上系统的手机,都会使用这个目录下的文件作为图标。

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
// 适配 Android 8.0 及以上系统应用图标的标准写法
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
// 指定图标的背景层,引用的是之前设置的颜色值。
<background android:drawable="@color/ic_launcher_background"/>
// 指定图标的前景层,引用的是之前准备的 Logo 图片。
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.sunnyweather.android">

<application
android:allowBackup="true"
// 这个属性就是专门用于指定应用程序图标的
// 8.0 及以上系统中,会使用 mipmap-anydpi-v26 目录下的 ic_launcher.xml 文件。
// 7.0 及以下系统会使用 mipmap 相应分辨率目录下的 ic_launcher.png 图片。
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
// 此属性是一个只适用于 Android 7.1 系统的过渡版本,很快就被 8.0 系统的新图标适配方案所替代了,不必关心它。
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
</application>
</manifest>

备注

参考资料:

第一行代码(第3版)