本章介紹了通知及使用技巧、調用攝像頭及讀取相冊、播放音視頻。最後我們介紹了infix函數這種高級語法糖的用法。
9.1.將程序運行到手機上
沒啥好講的
9.2.使用通知
某app不在前臺運行時卻希望向用戶發出一些提示信息,可以藉助通知來實現。發出通知後,最上方狀態欄會顯示一個通知的圖標,下拉狀態欄可以獲取通知的詳細內容。
//第一步:getSystemService用於獲取系統的那個服務,需要一個NotificationManager對同志進行管理
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
//第二步:創建通知渠道,低於8.0無法創建通知渠道
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
//構建一個通知渠道,創建的話需要知道渠道ID、渠道名稱和重要等級。渠道ID隨便定義,保證全局唯一性
//渠道名稱給用戶看,清楚表明用途,重要等級有四種。IMPORTANT_HIGH、DEFAULT、LOW、MIN。
val channel = NotificationChannel(channelID,channelName,importance)
//完後創建通知渠道
manager.createNotificationChannel(channel)
}
9.2.1.創建通知渠道
每個app都亂髮送通知,用戶煩不勝煩。要麼同意接收所有信息,要麼屏蔽所有信息,這也是Android通知功能的痛點。因此Android8.0引入通知渠道的概念。
通知渠道是每條通知都要屬於一個相應的渠道。每個app可自由創建當前應用擁有哪些通知渠道,但這些通知渠道的控制權是掌握在用戶手上的,用戶可以選擇這些通知渠道重要程度,是否響鈴、震動或者關閉。譬如微博可以創建兩種通知渠道,一個關注、一個推薦。
創建通知渠道的代碼如下:
//第一步:getSystemService用於獲取系統的那個服務,需要一個NotificationManager對同志進行管理
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
//第二步:創建通知渠道,低於8.0無法創建通知渠道
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
//構建一個通知渠道,創建的話需要知道渠道ID、渠道名稱和重要等級。渠道ID隨便定義,保證全局唯一性
//渠道名稱給用戶看,清楚表明用途,重要等級有四種。IMPORTANT_HIGH、DEFAULT、LOW、MIN。
val channel = NotificationChannel(channelID,channelName,importance)
//完後創建通知渠道
manager.createNotificationChannel(channel)
}
9.2.2.通知的基本用法
可以在Service、BroadCastReceiver和Activity中創建,前兩者較多,步驟相同。AndroidX提供的兼容API提供了NotificationCompat類用以創建Notification對象以保證所有系統版本均可運行:
//第一個參數是context,第二個參數是渠道ID,可以連綴任意多的方法來創建一個豐富的Notification對象
val notification = NotificationCompat.Builder(context,channelId).build()
//讓通知顯示出來,第一個參數是ID,每個通知指定的id不同,第二個參數是Notification對象
manager.notify(1,notification)
創建NotificationTest項目:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/send_Notice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Send Notice" />
</LinearLayout>
package com.example.myapplication
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.graphics.BitmapFactory
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.core.app.NotificationCompat
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//步驟一:獲取NotificationManager實例
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
//步驟二:建立通知渠道
if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
val channel = NotificationChannel("normal","Normal",NotificationManager.IMPORTANCE_DEFAULT)
manager.createNotificationChannel(channel)
}
//步驟三:點擊事件裏完成通知的創建工作
send_Notice.setOnClickListener {
//build方法之前連綴任意多的方法創建一個豐富的Notification對象,基本設置包括:
//setContentTitle標題內容;setContentText文本內容;setSmallIcon設置通知的小圖標,純alpha圖層;setLargeIcon大圖標
val notification = NotificationCompat.Builder(this,"normal")
.setContentTitle("This is content title")
.setContentText("This is content text")
.setSmallIcon(R.drawable.small_icon)
.setLargeIcon(BitmapFactory.decodeResource(resources,R.drawable.large_icon))
.build()
//讓通知顯示出來,一個是id,一個是notification對象。
manager.notify(1,notification)
}
}
}
僅僅顯示可不行,點擊通知的效果要有啊,涉及到PendingIntent,延遲執行的Intent。可以通過getActivity、getBroadcase和getService幾個方法來獲取PendingIntent實例。新建NotificationActivity這一Activity。修改xml和點擊事件:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="This is Notification layout"
android:textSize="24sp" />
</RelativeLayout>
send_Notice.setOnClickListener {
val intent = Intent(this, NotificationActivity::class.java)
//第一個參數是Context,第二個參數用不到,第三個參數時Intnet對象,通過這個對象構建出PendingIntent的意圖;第四個參數是PendingIntent的行爲,FLAG_ONE_SHOT等
val pi = PendingIntent.getActivity(this, 0, intent, 0)
//build方法之前連綴任意多的方法創建一個豐富的Notification對象,基本設置包括:
//setContentTitle標題內容;setContentText文本內容;setSmallIcon設置通知的小圖標,純alpha圖層;setLargeIcon大圖標
//setContentIntent設置延遲Intent;setAutoCancel自動取消掉
val notification = NotificationCompat.Builder(this, "normal")
.setContentIntent(pi)
.setAutoCancel(true)
.setContentTitle("This is content title")
.setContentText("This is content text")
.setSmallIcon(R.drawable.small_icon)
.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.large_icon))
.build()
//讓通知顯示出來,一個是id,一個是notification對象。
manager.notify(1, notification)
}
當然可以將setAutoCancel(true)改爲修改NotificationActivity裏面的內容:
class NotificationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_notification)
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.cancel(1)
}
}
9.2.3.通知的進階技巧
可以使用setStyle取代setContentText,因爲後者長文本時不能完全顯示,後面的都會被省略。
val notification = NotificationCompat.Builder(this, "normal")
.setContentIntent(pi)
.setContentTitle("This is content title")
.setStyle(NotificationCompat.BigTextStyle().bigText("豫章故郡,洪都新府。星分翼軫,地接衡廬。襟三江而帶五湖,控蠻荊而引甌越。物華天寶,龍光射牛鬥之墟;人傑地靈,徐孺下陳蕃之榻。雄州霧列,俊採星馳。臺隍枕夷夏之交,賓主盡東南之美。都督閻公之雅望,棨戟遙臨;宇文新州之懿範,襜帷暫駐。十旬休假,勝友如雲;千里逢迎,高朋滿座。騰蛟起鳳,孟學士之詞宗;紫電青霜,王將軍之武庫。家君作宰,路出名區;童子何知,躬逢勝餞。"))
.setSmallIcon(R.drawable.small_icon)
.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.large_icon))
.build()
也可以顯示大圖片。
//BitmapFactory.decodeResource將圖片解析爲Bitmap對象,在傳入BigPicture中
.setStyle(NotificationCompat.BigPictureStyle().bigPicture(BitmapFactory.decodeResource(resources, R.drawable.big_image)))
也可以調整優先級:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel2 =
NotificationChannel("important", "Important", NotificationManager.IMPORTANCE_HIGH)
manager.createNotificationChannel(channel2)
}
...
val notification = NotificationCompat.Builder(this, "important")
9.3.調用攝像頭和相冊
新建CameraAlbumTest項目,修改佈局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/take_photo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Take Photo" />
<ImageView
android:id="@+id/image_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" />
</LinearLayout>
修改MainActivity:
package com.example.myapplication
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.media.ExifInterface
import android.net.Uri
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.provider.MediaStore
import androidx.core.content.FileProvider
import kotlinx.android.synthetic.main.activity_main.*
import java.io.File
class MainActivity : AppCompatActivity() {
val takePhoto = 1
lateinit var imageuri: Uri
lateinit var outputimage: File
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
take_photo.setOnClickListener {
//創建File對象,用於存儲拍照後的照片,存儲位置是SD卡的應用關聯緩存目錄下,6.0後讀寫SD卡是危險權限,使用關聯目錄cache可以跳過這一步。Android10.0後使用作用域存儲。
outputimage = File(externalCacheDir, "output_image.jpg")
//如果File已經存在,刪掉,並調用createNewFile創建新文件
if (outputimage.exists()) {
outputimage.delete()
}
outputimage.createNewFile()
imageuri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//如果系統版本大於7.0,本地真實路徑uri不安全,會拋出異常。 FileProvider.getUriForFile可以將File對象轉換爲一個封裝後的uri對象。
//getUriForFile接收三個參數,一個是context,另一個是任意字符串,第三個是File對象。FileProvider使用類似ContentProvider的機制進行保護,提高程序安全性。
FileProvider.getUriForFile(this, "com.example.camera.fileprovider", outputimage)
} else {
//如果系統版本低於7.0,調用Uri.fromFile將File對象轉換爲Uri對象,這個對象是本地真實路徑
Uri.fromFile(outputimage)
}
//Intent的action進行指定,Intent的putExtra指定圖片的輸出地址,剛剛得到uri對象
val intent = Intent("android.media.action.IMAGE_CAPTURE")
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageuri)
//啓動Activity,隱式的。調用之後返回到onActivityResult方法。
startActivityForResult(intent, takePhoto)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
takePhoto -> {
//將拍攝的照片顯示出來,拍照成功的話使用BitmapFactory.decodeStream將圖片解析爲bitmap對象
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(imageuri))
//最後設置到ImageView當中,再加上照片旋轉的處理。
image_view.setImageBitmap(rotateIfRequired(bitmap))
}
}
}
private fun rotateIfRequired(bitmap: Bitmap): Bitmap {
val exif = ExifInterface(outputimage.path)
val orientation =
exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
return when (orientation) {
ExifInterface.ORIENTATION_ROTATE_90 -> RotateBitmap(bitmap, 90)
ExifInterface.ORIENTATION_ROTATE_180 -> RotateBitmap(bitmap, 180)
ExifInterface.ORIENTATION_ROTATE_270 -> RotateBitmap(bitmap, 270)
else -> bitmap
}
}
private fun RotateBitmap(bitmap: Bitmap, degree: Int): Bitmap {
val matrix = Matrix()
matrix.postRotate(degree.toFloat())
val rotatedBitmap =
Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
bitmap.recycle()//不再需要的bitmap對象回收
return rotatedBitmap
}
}
修改AndroidManifest.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapplication">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- android:name值固定,android:authorities與剛纔第二個參數一致;另外provider標籤的內部使用<meta-data>來指定Uri的共享路徑,並引用了一個@xml/file_paths資源-->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.example.camera.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
新建xml目錄,新建file_paths.xml。
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- external-path用來指定uri共享的,name的值隨便填,path的值表示共享的具體路徑,用單斜線將SD卡進行共享-->
<external-path
name="my_images"
path="/" />
</paths>
9.3.2.從相冊中選擇圖片
佈局就不說了,一個button組件。主要是MainActivity裏的修改。
class MainActivity : AppCompatActivity() {
val takePhoto = 1
val fromAlbum = 2
lateinit var imageuri: Uri
lateinit var outputimage: File
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
....
from_album_btn.setOnClickListener {
//1.打開文件選擇器,Intent的action指定爲打開系統文件選擇器。
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
//2.指定只顯示圖片,增加過濾條件,只允許打開圖片文件顯示出來
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "image/*"
//3.選擇完圖片後進入onActivityResult方法
startActivityForResult(intent, fromAlbum)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
...
fromAlbum -> {
//如果data不等於null
if (resultCode == Activity.RESULT_OK && data != null) {
//調用Intnet的getData方法獲取圖片的uri。在調用getBitmapFromUri將uri轉換爲Bitmap對象,最後顯示出來。
data.data?.let { uri ->
val bitmap = getBitmapFromUri(uri)
image_view.setImageBitmap(bitmap)
}
}
}
}
}
private fun getBitmapFromUri(uri: Uri) = contentResolver.openFileDescriptor(uri, "r")?.use {
BitmapFactory.decodeFileDescriptor(it.fileDescriptor)
}
.....
}
9.4.播放多媒體文件
9.4.1.播放音頻
音頻文件MediaPlayer類中的方法:
新建PlayAudioTest項目,新建assets目錄用於存儲音樂文件,修改佈局文件activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/play_music_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Play"/>
<Button
android:id="@+id/pause_music_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Pause"/>
<Button
android:id="@+id/stop_music_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Stop"/>
</LinearLayout>
修改MainActivity裏的代碼:
package com.example.myapplication
import android.media.MediaPlayer
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
//類初始化創建一個MediaPlayer的實例
private val mediaPlayer = MediaPlayer()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//初始化
initMediaPlayer()
play_music_btn.setOnClickListener {
//對於細節的處理堪稱完美,來了先判斷,用完之後初始化、銷燬等等
if (!mediaPlayer.isPlaying){
mediaPlayer.start()
}
}
pause_music_btn.setOnClickListener {
if (mediaPlayer.isPlaying){
mediaPlayer.pause()
}
}
stop_music_btn.setOnClickListener {
if (mediaPlayer.isPlaying){
//重置爲剛纔的狀態並重現調用initMediaPlayer方法
mediaPlayer.reset()
initMediaPlayer()
}
}
}
private fun initMediaPlayer(){
//得到一個assetManager實例,assetManager可讀取assets目錄下的所有資源
val assetManager = assets
//調用openFd將音頻文件句柄打開,做一次調用setDataSource設置要播放文件的位置、prepare完成初始化
val fd = assetManager.openFd("music.mp3")
mediaPlayer.setDataSource(fd.fileDescriptor,fd.startOffset,fd.length)
mediaPlayer.prepare()
}
override fun onDestroy() {
super.onDestroy()
mediaPlayer.stop()
mediaPlayer.release()
}
}
9.4.2.播放視頻
藉助VideoView類來實現,VideoView並不是萬能工具類,其支持的格式不多、效率低。視頻放在新建的raw目錄下,方法如下:
新建PlayVideoTest項目,修改activity_main.xml。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/play_video_btn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Play" />
<Button
android:id="@+id/pause_video_btn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Pause" />
<Button
android:id="@+id/stop_video_btn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Replay" />
</LinearLayout>
<VideoView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
修改MainActivity.java:
package com.example.myapplication
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//將raw目錄下的video.MP4解析爲一個uri對象
val uri = Uri.parse("android.resource://$packageName/${R.raw.video}")
//調用setVideoURI將解析出來的uri對象傳入,這樣完成了初始化
video_view.setVideoURI(uri)
play_video_btn.setOnClickListener {
if (!video_view.isPlaying){
video_view.start()
}
}
pause_video_btn.setOnClickListener {
if (video_view.isPlaying){
video_view.pause()
}
}
stop_video_btn.setOnClickListener {
if (video_view.isPlaying){
video_view.resume()//重新播放
}
}
}
override fun onDestroy() {
super.onDestroy()
video_view.suspend()
}
}
9.5.Kotlin課堂:使用infix函數構建更可讀的用法
val map = mapOf("Apple" to 1, "Banana" to 2, "Pear" to 3)
for ((fruit, number) in map) {
println("fruit is " + fruit + ",number is " + number)
}
to並不是Kotlin關鍵字,藉助了高級語法糖:infix函數,infix只是調整了寫法,他是將A .to( B)改爲A to B。Infix可提高代碼的可讀性。舉例來講:
//判斷字符串是否以某個參數開頭
if ("Hello Kitty".startsWith("Hello")) {
//處理邏輯
}
//將其改寫爲infix函數形式:
//藉助infix函數,使用更可讀的寫法
if ("Hello Kitty" beginsWith "hello"){
}
//String類的擴展函數,添加一個beginsWith,內部實現基於startsWith方法。
// 加上infix後beginsWith變爲infix函數,除了傳統的調用方式,還有特殊語法糖格式
infix fun String.beginsWith(prefix: String) = startsWith(prefix)
Infix函數需要滿足兩個條件:1.不能定義爲頂層函數,必須爲類成員函數或者擴展函數;2.只能接受一個參數,參數類型沒有限制。舉例來說:
val list2 = listOf("Apple", "Banana", "Orange", "Pear")
if (list2.contains("Banana")) {
//處理具體邏輯
}
//使用infix寫法
if (list2.has("Banana")) {
//處理具體邏輯
}
//給所有Collection接口添加一個擴展函數,這是因爲Collection是所有Java和Kotlin集合的總接口,因此給它添加一個has函數所有集合子類都能用了
infix fun <T> Collection<T>.has(element: T) = contains(element)
研究A to B,發現是使用了Pair函數,自己仿寫整一個:
val map = mapOf("Apple" with 1, "Banana" with 2, "Pear" with 3)
infix fun <A, B> A.with(that: B): Pair<A, B> = Pair(this, that)