通知、多媒體(十)

      本章介紹了通知及使用技巧、調用攝像頭及讀取相冊、播放音視頻。最後我們介紹了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)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章