美文网首页
第一行代码 -- 笔记2

第一行代码 -- 笔记2

作者: TomyZhang | 来源:发表于2020-10-31 16:53 被阅读0次

五、广播机制

1.接收系统广播

动态注册监听时间变化
//MainActivity.kt
package com.tomorrow.kotlindemo

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    lateinit var timeChangeReceiver: TimeChangeReceiver

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val intentFilter = IntentFilter()

        intentFilter.addAction(Intent.ACTION_TIME_TICK)
        timeChangeReceiver = TimeChangeReceiver()
        registerReceiver(timeChangeReceiver, intentFilter)
    }

    override fun onDestroy() {
        super.onDestroy()
        unregisterReceiver(timeChangeReceiver)
    }

    inner class TimeChangeReceiver : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            Toast.makeText(context, "Time has changed", Toast.LENGTH_SHORT).show()
        }
    }
}
静态注册实现开机启动

在 Android 8.0 系统之后,所有隐式广播都不允许使用静态注册的方式来接收了。隐式广播指的是那些没有具体指定发送给哪个应用程序的广播。大多数系统广播属于隐式广播,但是少数特殊的系统广播目前仍然允许使用静态注册的方式来接收。

//AndroidManifest.xml
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

<receiver
    android:name=".BootCompleteReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
    </intent-filter>
</receiver >

//BootCompleteReceiver.kt
package com.tomorrow.kotlindemo

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast

class BootCompleteReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        Toast.makeText(context, "Boot Complete", Toast.LENGTH_SHORT).show()
    }
}

2.发送自定义广播

发送标准广播
//AndroidManifest.xml
<receiver
    android:name=".MyBroadcastReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="com.tomorrow.intent.action.MY_BROADCAST"/>
    </intent-filter>
</receiver >

//MyBroadcastReceiver.kt
package com.tomorrow.kotlindemo

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast

class MyBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        Toast.makeText(context, "MyBroadcastReceiver", Toast.LENGTH_SHORT).show()
    }
}

//MainActivity.kt
package com.tomorrow.kotlindemo

import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.left_fragment.*

class MainActivity : AppCompatActivity() {

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

        button.setOnClickListener {
            val intent = Intent("com.tomorrow.intent.action.MY_BROADCAST")
            intent.setPackage(packageName)
            sendBroadcast(intent)
        }
    }
}

Android 8.0 系统之后,静态注册的 Broadcast 是无法接收隐式广播的,而默认情况下我们发出的自定义广播恰恰都是隐式广播,因此这里一定要调用 setPackage() 方法,指定这条广播是发送给哪个应用程序的,从而让它变成一条显式广播,否则静态注册的 BroadcastReceiver 将无法接收到这条广播。

发送有序广播
//AndroidManifest.xml
<receiver
    android:name=".MyBroadcastReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter android:priority="-1000">
        <action android:name="com.tomorrow.intent.action.MY_BROADCAST"/>
    </intent-filter>
</receiver >
<receiver
    android:name=".AnotherBroadcastReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter android:priority="1000">
        <action android:name="com.tomorrow.intent.action.MY_BROADCAST"/>
    </intent-filter>
</receiver >

//AnotherBroadcastReceiver.kt
package com.tomorrow.kotlindemo

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast

class AnotherBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        Toast.makeText(context, "AnotherBroadcastReceiver", Toast.LENGTH_SHORT).show()
        abortBroadcast()
    }
}

//MyBroadcastReceiver.kt
package com.tomorrow.kotlindemo

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast

class MyBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        Toast.makeText(context, "MyBroadcastReceiver", Toast.LENGTH_SHORT).show()
    }
}

//MainActivity.kt
package com.tomorrow.kotlindemo

import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.left_fragment.*

class MainActivity : AppCompatActivity() {

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

        button.setOnClickListener {
            val intent = Intent("com.tomorrow.intent.action.MY_BROADCAST")
            intent.setPackage(packageName)
            sendOrderedBroadcast(intent, null)
        }
    }
}

3.Kotlin:高阶函数

定义高阶函数

高阶函数的定义:

如果一个函数接收另一函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数。

函数类型的语法规则:

(String, Int) -> Unit

高阶函数允许让函数类型的参数来决定函数的执行逻辑。即使是同一个高阶函数,只要传入不同的函数类型参数,那么它的执行逻辑和最终的返回结果就可能是完全不同的。

Kotlin 支持多种方式来调用高阶函数,比如 Lambda 表达式、普通函数、匿名函数、成员引用等。其中,Lambda 表达式是最常见也是最普遍的高阶函数调用方式。

使用普通函数:

fun main() {
    println(num1AndNum2(100, 80, ::plus))
    println(num1AndNum2(100, 80, ::minus))
}

fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int)  = operation(num1, num2)
fun plus(num1: Int, num2: Int) = num1 + num2
fun minus(num1: Int, num2: Int) = num1 - num2

日志打印:
180
20

使用 Lambda 表达式:

fun main() {
    println(num1AndNum2(100, 80) { n1, n2 ->
        n1 + n2
    })
    println(num1AndNum2(100, 80) { n1, n2 ->
        n1 - n2
    })
}

fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int)  = operation(num1, num2)

日志打印:
180
20

高阶函数完整的语法规则:

fun main() {
    val list = listOf("Apple", "Banana", "Pear")
    val result = StringBuilder().build {
        append("Start eating fruits: ")
        for(fruit in list) {
            append(fruit)
            append(" ")
        }
        append("done")
    }
    println(result)
}

fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
    block()
    return this
}

在函数类型的前面加上 ClassName,就表示这个函数类型是定义在哪个类中。好处就是当我们调用 build 函数时传入的 Lambda 表达式将会自动拥有 StringBuilder 的上下文。

内联函数的作用

Kotlin 高阶函数背后的实现原理:

public static int num1AndNum2(int num1, int num2, Function operation) {
    int result = (int) operation.invoke(num1, num2);
    return result;
}

public static void main() {
    int num1 = 100;
    int num2 = 80;
    int result = num1AndNum2(num1, num2, new Function() {
        @Override
        public Integer invoke(Integer n1, Integer n2) {
            return n1 + n2;
        }
    })
}

num1AndNum2() 函数的第三个参数变成了一个 Function 接口,这是一种 Kotlin 内置的接口,里面有一个待实现的 invoke() 函数。而 num1AndNum2() 函数其实就是调用了 Function 接口的 invoke() 函数,并把 num1 和 num2 参数传了进去。

在调用 num1AndNum2() 函数的时候,之前的 Lambda 表达式在这里变成了 Function 接口的匿名类实现,然后在 invoke() 函数中实现了 n1 + n2 的逻辑,并将结果返回。这就表明,我们每调用一次 Lambda 表达式,都会创建一个新的匿名类实例,当然也会造成额外的内存和性能开销。

为了解决这个问题,Kotlin 提供了内联函数的功能,它可以将使用 Lambda 表达式带来的运行时开销完全消除。内联函数的工作原理就是:Kotlin 编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方,这样也就不存在运行时的开销了:

inline fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int)  = operation(num1, num2)
noinline 与 crossinline

一个高阶函数中如果接收了两个或者更多函数类型的参数,这时我们给函数加上了 inline 关键字,那么 Kotlin 编译器会自动将所有引用的 Lambda 表达式全部进行内联。但是如果我们只想内联其中的一个 Lambda 表达式的话,就可以使用 noinline 关键字了:

inline fun inlineTest(block1: () -> Unit, noinline block2: () -> Unit) {
}

Kotlin 提供一个 noinline 关键字来排除内联功能的原因:内联的函数类型参数在编译的时候会被进行代码替换,因此它没有真正的参数属性。非内联的函数类型参数可以自由地传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局限性。

内联函数与非内联函数还有一个重要的区别,那就是内联函数所引用的 Lambda 表达式中是可以使用 return 关键字来进行函数返回的,而非内联函数只能进行局部返回。

非内联函数例子:

fun main() {
    println("main start")
    val str = ""
    printString(str) {
        println("Lambda start")
        if(it.isEmpty()) return@printString
        println(it)
        println("Lambda end")
    }
    println("main end")
}

fun printString(str: String, block: (String) -> Unit) {
    println("printString begin")
    block(str)
    println("printString end")
}

日志打印:
main start
printString begin
Lambda start
printString end
main end

注意,Lambda 表达式中是不允许直接使用 return 关键字的,这里使用了 return@printString 的写法,表示进行局部返回,并且不再执行 Lambda 表达式的剩余部分代码。

内联函数例子:

fun main() {
    println("main start")
    val str = ""
    printString(str) {
        println("Lambda start")
        if(it.isEmpty()) return
        println(it)
        println("Lambda end")
    }
    println("main end")
}

inline fun printString(str: String, block: (String) -> Unit) {
    println("printString begin")
    block(str)
    println("printString end")
}

日志打印:
main start
printString begin
Lambda start

现在 printString() 函数变成了内联函数,我们就可以在 Lambda 表达式中使用 return 关键字了。此时的 return 代表的是返回外层的调用函数,也就是 main() 函数。

将高阶函数声明成内联函数是一种良好的编程习惯,事实上,绝大多数高阶函数是可以直接声明成内联函数的,但是也有少部分例外的情况:

inline fun runRunnable(block: () -> Unit) {
    val runnable = Runnable {
        block() //编译错误
    }
    runnable.run()
}

在 runRunnable() 函数中,我们创建了一个 Runnable 对象,并在 Runnable 的 Lambda 表达式中调用了传入的函数类型参数。而 Lambda 表达式在编译的时候会被转换成匿名类的实现方式,也就是,上述代码实际上是在匿名类中调用了传入的函数类型参数。

而内联函数所引用的 Lambda 表达式允许使用 return 关键字进行函数返回,但是由于我们是在匿名类中调用的函数类型参数,此时是不可能进行外层调用函数返回的,最多只能对匿名类中的函数调用进行返回,因此这里就提示了上述错误。也就是说,如果我们在高阶函数中创建了另外的 Lambda 或者匿名类的实现,并且在这些实现中调用函数类型参数,此时再将高阶函数声明成内联函数,就一定会提示错误。

此时我们可以借助 crossinline 关键字来解决这个问题:

inline fun runRunnable(crossinline block: () -> Unit) {
    val runnable = Runnable {
        block()
    }
    runnable.run()
}

内联函数的 Lambda 表达式中允许使用 return 关键字,和高阶函数的匿名类实现中不允许使用 return 关键字之间造成了冲突。而 crossinline 关键字就像一个契约,它用于保证在内联函数的 Lambda 表达式中一定不会使用 return 关键字,这样冲突就不存在了。声明了 crossinline 之后,我们就无法在调用 returnRunnable() 函数时的 Lambda 表达式中使用 return 关键字进行函数返回了,但是仍然可以使用 return@runRunnable 的写法进行局部返回。总体来说,除了在 return 关键字的使用上有所区别之外,crossinline 保留了内联函数的其他所有特性。

六、数据存储

1.文件存储

将数据存储到文件中
fun save(inputText: String) {
    try {
        val output = openFileOutput("data", Context.MODE_PRIVATE)
        val writer = BufferedWriter(OutputStreamWriter(output))
        writer.use {
            it.write(inputText)
        }
    } catch (e: IOException) {
        e.printStackTrace()
    }
}

use 函数是 Kotlin 提供的一个内置扩展函数,它会保证在 Lambda 表达式中的代码全部执行完之后自动将外层的流关闭,这样就不需要我们再编写一个 finally 语句,手动去关闭流了,是一个非常好用的扩展函数。

从文件中读取数据
fun load() : String {
    val content = StringBuilder()
    try {
        val input = openFileInput("data")
        val reader = BufferedReader(InputStreamReader(input))
        reader.use {
            reader.forEachLine {
                content.append(it)
            }
        }
    } catch (e: IOException) {
        e.printStackTrace()
    }
    return content.toString()
}

2.SharedPreferences 存储

将数据存储到 SharedPreferences 中
fun save() {
    val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
    editor.putString("name", "Tomy")
    editor.putInt("age", 30)
    editor.apply()
}
从 SharedPreferences 中读取数据
fun load() {
    val prefs = getSharedPreferences("data", Context.MODE_PRIVATE)
    val name = prefs.getString("name", "")
    val age = prefs.getInt("age", 0)
    Log.d("TAG", "zwm, $name $age")
}

3.SQLite 数据库存储

基本使用
//MySQLiteOpenHelper.kt
package com.tomorrow.kotlindemo

import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper

class MySQLiteOpenHelper(val context: Context, name: String, version: Int) : SQLiteOpenHelper(context, name, null, version) {

    private val createBook = "create table Book (" +
            "id integer primary key autoincrement," +
            "author text," +
            "price real," +
            "pages integer," +
            "name text)"

    private val createCategory = "create table Category (" +
            "id integer primary key autoincrement," +
            "category_name text," +
            "category_code integer)"

    override fun onCreate(db: SQLiteDatabase?) {
        db?.execSQL(createBook)
        db?.execSQL(createCategory)
     }

    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
        db?.execSQL("drop table if exists Book")
        db?.execSQL("drop table if exists Category")
        onCreate(db)
     }
}

//MainActivity.kt
package com.tomorrow.kotlindemo

import android.content.ContentValues
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

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

        val dbHelper = MySQLiteOpenHelper(this, "BookStore.db", 2)
        val db = dbHelper.writableDatabase

        //增
        val values1 = ContentValues().apply {
            put("name", "Android 开发艺术探索")
            put("author", "任玉刚")
            put("pages", 300)
            put("price", 88)
        }
        db.insert("Book", null, values1)
        val values2 = ContentValues().apply {
            put("name", "第一行代码")
            put("author", "郭霖")
            put("pages", 600)
            put("price", 99)
        }
        db.insert("Book", null, values2)

        //改
        val values3 = ContentValues()
        values3.put("price", 99)
        db.update("Book", values3, "name = ?", arrayOf("Android 开发艺术探索"))

        //删
        db.delete("Book", "pages > ?", arrayOf("500"))

        //查
        val cursor = db.query("Book", null, null, null, null, null, null)
        if(cursor.moveToFirst()) {
            do {
                val name = cursor.getString(cursor.getColumnIndex("name"))
                val author = cursor.getString(cursor.getColumnIndex("author"))
                val pages = cursor.getInt(cursor.getColumnIndex("pages"))
                val price = cursor.getDouble(cursor.getColumnIndex("price"))
                Log.d("TAG", "zwm, $name $author $pages $price")
            } while (cursor.moveToNext())
        }
        cursor.close()
    }
}

SQLiteOpenHelper 中有两个非常重要的实例方法:getReadableDatabase() 和 getWritableDatabase()。这两个方法都可以创建或打开一个现有的数据库(如果数据库已存在则直接打开,否则要创建一个新的数据库),并返回一个可对数据库进行读写操作的对象。不同的是,当数据库不可写入的时候(如磁盘已满),getReadableDatabase() 方法返回的对象将以只读的方式打开数据库,而 getWritableDatabase() 方法则将出现异常。

SQLite 数据库的数据类型很简单:integer 表示整型,real 表示浮点型,text 表示文本类型,blob 表示二进制类型。

query() 方法参数的详细解释:

query() 方法参数 对应 SQL 部分 描述
table from table_name 指定查询的表名
columns select column1, column2 指定查询的列名
selection where column = value 指定 where 的约束条件
selectionArgs - 为 where 中的占位符提供具体的值
groupBy group by column 指定需要 group by 的列
having having column = value 对 group by 后的结果进一步约束
orderBy order by column1, column2 指定查询结果的排列方式
使用 SQL 操作数据库
//MainActivity.kt
package com.tomorrow.kotlindemo

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

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

        val dbHelper = MySQLiteOpenHelper(this, "BookStore.db", 2)
        val db = dbHelper.writableDatabase

        //增
        db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)", arrayOf("Book1", "Author1", "666", "50"))
        db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)", arrayOf("Book2", "Author2", "777", "60"))

        //改
        db.execSQL("update Book set price = ? where name = ?", arrayOf("55", "Book1"))

        //删
        db.execSQL("delete from Book where pages > ?", arrayOf("700"))

        //查
        val cursor = db.rawQuery("select * from Book", null)
        if(cursor.moveToFirst()) {
            do {
                val name = cursor.getString(cursor.getColumnIndex("name"))
                val author = cursor.getString(cursor.getColumnIndex("author"))
                val pages = cursor.getInt(cursor.getColumnIndex("pages"))
                val price = cursor.getDouble(cursor.getColumnIndex("price"))
                Log.d("TAG", "zwm, $name $author $pages $price")
            } while (cursor.moveToNext())
        }
        cursor.close()
    }
}

除了查询数据的时候调用的是 SQLiteDatabase 的 rawQuery() 方法,其他操作都是调用的 exceSQL() 方法。

使用事务

SQLite 数据库是支持事务的,事务的特性可以保证让一系列的操作要么全部完成,要么一个都不会完成。

//MainActivity,kt
package com.tomorrow.kotlindemo

import android.content.ContentValues
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

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

        val dbHelper = MySQLiteOpenHelper(this, "BookStore.db", 2)
        val db = dbHelper.writableDatabase

        db.beginTransaction()
        try {
            db.delete("Book", null, null)
            if(true) {
                throw NullPointerException()
            }
            val values = ContentValues().apply {
                put("name", "Android")
                put("author", "Google")
                put("pages", 999)
                put("price", 99)
            }
            db.insert("Book", null, values)
            db.setTransactionSuccessful()
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            db.endTransaction()
        }
    }
}
升级数据库
//MySQLiteOpenHelper.kt
package com.tomorrow.kotlindemo

import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper

class MySQLiteOpenHelper(val context: Context, name: String, version: Int) : SQLiteOpenHelper(context, name, null, version) {

    private val createBook = "create table Book (" +
            "id integer primary key autoincrement," +
            "author text," +
            "price real," +
            "pages integer," +
            "name text," +
            "category_id integer)"

    private val createCategory = "create table Category (" +
            "id integer primary key autoincrement," +
            "category_name text," +
            "category_code integer)"

    override fun onCreate(db: SQLiteDatabase?) {
        db?.execSQL(createBook)
        db?.execSQL(createCategory)
     }

    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
        if(oldVersion <= 1) {
            db?.execSQL(createCategory)
        }
        if(oldVersion <= 2) {
            db?.execSQL("alter table Book add column category_id integer")
        }
     }
}

4.Kotlin:高阶函数的应用

简化 SharedPreferences 的用法
fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit) {
    val editor = edit()
    editor.block()
    editor.apply()
}

getSharedPreferences("data", Context.MODE_PRIVATE).open {
    putString("name", "Tomy")
    putInt("age", 30)
}

其实 Google 提供的 KTX 扩展库中已经包含了上述 SharedPreferences 的简化用法,这个扩展库会在 Android Studio 创建项目的时候自动引入 build.gradle 的 dependencies 中:

implementation 'androidx.core:core-ktx:1.0.2'

因此,我们实际上可以直接在项目中使用如下写法来向 SharedPreferences 存储数据:

getSharedPreferences("data", Context.MODE_PRIVATE).edit {
    putString("name", "Tomy")
    putInt("age", 30)
}
简化 ContentValues 的用法
fun cvOf(vararg pairs: Pair<String, Any?>) = ContentValues().apply {
    for(pair in pairs) {
        val key = pair.first
        val value = pair.second
        when (value) {
            is Int -> put(key, value)
            is Long -> put(key, value)
            is Short -> put(key, value)
            is Float -> put(key, value)
            is Double -> put(key, value)
            is Boolean -> put(key, value)
            is String -> put(key, value)
            is Byte -> put(key, value)
            is ByteArray -> put(key, value)
            null -> putNull(key)
        }
    }
}

val values = cvOf("name" to "Android", "author" to "Google", "pages" to 999, "price" to 99)

当然,KTX 库中也提供了一个具有同样功能的 ContentValuesOf() 方法,平时我们在编写代码的时候,直接使用 KTX 提供的方法就可以了:

val values = contentValuesOf("name" to "Android", "author" to "Google", "pages" to 999, "price" to 99)

七、ContentProvider

ContentProvider 主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。目前,使用 ContentProvider 是 Android 实现跨进程共享数据的标准方式。

1.运行时权限

Android 权限机制详解

Android 现在将常用的权限大致归成了两类:一类是普通权限,一类是危险权限。普通权限指的是那些不会直接威胁到用户的安全和隐私的权限,对于这部分权限的申请,系统会自动帮我们进行授权,不需要用户手动操作。危险权限则表示那些可能会触及用户隐私或者对设备安全性造成影响的权限,如获取设备联系人信息、定位设备的地理位置等,对于这部分权限申请,必须由用户手动授权才可以,否则程序就无法使用相应的功能。

到 Android 10 系统为止所有的危险权限:

权限组名 权限名
CALENDAR READ_CALENDAR、WRITE_CALENDAR
CALL_LOG READ_CALL_LOG、WRITE_CALL_LOG、PROCESS_OUTGOING_CALLS
CAMERA CAMERA
CONTACTS READ_CONTACTS、WRITE_CONTACTS、GET_ACCOUNTS
LOCATION ACCESS_FINE_LOCATION、ACCESS_COARSE_LOCATION、ACCESS_BACKGROUND_LOCATION
MICROPHONE RECORD_AUDIO
PHONE READ_PHONE_STATE、READ_PHONE_NUMBERS、CALL_PHONE、ANSWER_PHONE_CALLS、ADD_VOICEMAIL、USE_SIP、ACCEPT_HANDOVER
SENSORS BODY_SENSORS
ACTIVITY_RECOGNITION ACTIVITY_RECOGNITION
SMS SEND_SMS、RECEIVER_SMS、READ_SMS、RECEIVE_WAP_PUSH、RECEIVE_MMS
SOTRAGE READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE、ACCESS_MEDIA_LOCATION

如果是这张表中的权限,就需要进行运行时权限处理,否则,只需要在 AndroidManifest.xml 文件中添加一下权限声明就可以了。

在程序运行时申请权限
//AndroidManifest.xml
<uses-permission android:name="android.permission.CALL_PHONE"/>

//MainActivity.kt
package com.tomorrow.kotlindemo

import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat

class MainActivity : AppCompatActivity() {

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

        if(ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CALL_PHONE), 1)
        } else {
            call()
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode) {
            1 -> {
                if(grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    call()
                } else {
                    Toast.makeText(this, "You denied the premission", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    private fun call() {
        try {
            val intent = Intent(Intent.ACTION_CALL)
            intent.data = Uri.parse("tel:10086")
            startActivity(intent)
        } catch (e: SecurityException) {
            e.printStackTrace()
        }
    }
}

2.读取系统联系人

对于每一个应用程序来说,如果想要访问 ContentProvider 中共享的数据,就一定要借助 ContentResolver 类,可以通过 Context 中的 getContentResolver() 方法获取该类的实例。

//AndroidManifest.xml
<uses-permission android:name="android.permission.READ_CONTACTS"/>

//MainActivity.kt
package com.tomorrow.kotlindemo

import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.provider.ContactsContract
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat

class MainActivity : AppCompatActivity() {

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

        if(ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_CONTACTS), 1)
        } else {
            readContacts()
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode) {
            1 -> {
                if(grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    readContacts()
                } else {
                    Toast.makeText(this, "You denied the premission", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    private fun readContacts() {
        contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null)?.apply {
            while(moveToNext()) {
                val displayName = getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
                val number = getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
                Log.d("TAG", "zwm, $displayName $number")
            }
            close()
        }
    }
}

3.创建自己的 ContentProvider

一个标准的内容 URI 写法是:

content://com.example.app.provider/table1

这就表示调用方期望访问的是 com.example.app.provider 这个应用的 table1 表中的数据。还可以在这个内容 URI 的后面加上一个 id:

content://com.example.app.provider/table1/1

这就表示调用方期望访问的是 com.example.app.provider 这个应用的 table1 表中 id 为 1 的数据。

内容 URI 的格式主要就只有以上两种,以路径结尾表示期望访问该表中所有的数据,以 id 结尾表示期望访问该表中拥有相应 id 的数据。我们可以使用通配符分别匹配这两种格式的内容 URI:

  • 匹配任意表的内容 URI 格式:
content://com.example.app.provider/*
  • 匹配 table1 表中任意一行数据的内容 URI 格式:
content://com.example.app.provider/table1/#

getType() 方法是所有的 ContentProvider 都必须提供的一个方法,用于获取 Uri 对象所对应的 MIME 类型。一个内容 URI 所对应的 MIME 字符串主要由 3 部分组成,Android 对这 3 个部分做了如下格式规定:

  • 必须以 vnd 开头。
  • 如果内容 URI 以路径结尾,则后接 android.cursor.dir/;如果内容 URI 以 id 结尾,则后接 android.cursor.item/。
  • 最后接上 vnd.<authority>.<path>。
vnd.android.cursor.dir/vnd.com.example.app.provider.table1
vnd.android.cursor.item/vnd.com.example.app.provider.table1

实现跨程序数据共享:

//AndroidManifest.xml
<provider
    android:name=".MyProvider"
    android:authorities="com.tomorrow.kotlindemo"
    android:exported="true"
    android:enabled="true" />
    
//MySQLiteOpenHelper.kt
package com.tomorrow.kotlindemo

import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper

class MySQLiteOpenHelper(val context: Context, name: String, version: Int) : SQLiteOpenHelper(context, name, null, version) {

    private val createBook = "create table Book (" +
            "id integer primary key autoincrement," +
            "author text," +
            "price real," +
            "pages integer," +
            "name text," +
            "category_id integer)"

    private val createCategory = "create table Category (" +
            "id integer primary key autoincrement," +
            "category_name text," +
            "category_code integer)"

    override fun onCreate(db: SQLiteDatabase?) {
        db?.execSQL(createBook)
        db?.execSQL(createCategory)
     }

    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
        if(oldVersion <= 1) {
            db?.execSQL(createCategory)
        }
        if(oldVersion <= 2) {
            db?.execSQL("alter table Book add column category_id integer")
        }
     }
}

//MyProvider.kt
package com.tomorrow.kotlindemo

import android.content.ContentProvider
import android.content.ContentValues
import android.content.UriMatcher
import android.net.Uri

class MyProvider : ContentProvider() {

    private val bookDir = 0
    private val bookItem = 1
    private val categoryDir = 2
    private val categoryItem = 3
    private val authority = "com.tomorrow.kotlindemo"
    private var dbHelper: MySQLiteOpenHelper? = null

    private val uriMatcher by lazy {
        val matcher = UriMatcher(UriMatcher.NO_MATCH)
        matcher.addURI(authority, "book", bookDir)
        matcher.addURI(authority, "book/#", bookItem)
        matcher.addURI(authority, "category", categoryDir)
        matcher.addURI(authority, "category/#", categoryItem)
        matcher
    }

    override fun onCreate() = context?.let {
        dbHelper = MySQLiteOpenHelper(it, "BookStore.db", 2)
        true
    } ?: false

    override fun query(uri: Uri?, projection: Array<out String>?, selection: String?,
                       selectionArgs: Array<out String>?, sortOrder: String?) = dbHelper?.let {
        val db = it.readableDatabase
        val cursor = when (uriMatcher.match(uri)) {
            bookDir -> db.query("Book", projection, selection, selectionArgs, null, null, sortOrder)
            bookItem -> {
                val bookId = uri!!.pathSegments[1]
                db.query("Book", projection, "id = ?", arrayOf(bookId), null, null, sortOrder)
            }
            categoryDir -> db.query("Category", projection, selection, selectionArgs, null, null, sortOrder)
            categoryItem -> {
                val categoryId = uri!!.pathSegments[1]
                db.query("Category", projection, "id = ?", arrayOf(categoryId), null, null, sortOrder)
            }
            else -> null
        }
        cursor
    }

    override fun insert(uri: Uri?, values: ContentValues?) = dbHelper?.let {
        val db = it.writableDatabase
        val uriReturn = when (uriMatcher.match(uri)) {
            bookDir, bookItem -> {
                val newBookId = db.insert("Book", null, values)
                Uri.parse("content://$authority/book/$newBookId")
            }
            categoryDir, categoryItem -> {
                val newCategoryId = db.insert("Category", null, values)
                Uri.parse("content://$authority/category/$newCategoryId")
            }
            else -> null
        }
        uriReturn
    }

    override fun update(uri: Uri?, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?) = dbHelper?.let {
        val db = it.writableDatabase
        val updateRows = when (uriMatcher.match(uri)) {
            bookDir -> db.update("Book", values, selection, selectionArgs)
            bookItem -> {
                val bookId = uri!!.pathSegments[1]
                db.update("Book", values, "id = ?", arrayOf(bookId))
            }
            categoryDir -> db.update("Category", values, selection, selectionArgs)
            categoryItem -> {
                val categoryId = uri!!.pathSegments[1]
                db.update("Category", values, "id = ?", arrayOf(categoryId))
            }
            else -> 0
        }
        updateRows
    } ?: 0

    override fun delete(uri: Uri?, selection: String?, selectionArgs: Array<out String>?) = dbHelper?.let {
        val db = it.writableDatabase
        val deletedRows = when (uriMatcher.match(uri)) {
            bookDir -> db.delete("Book", selection, selectionArgs)
            bookItem -> {
                val bookId = uri!!.pathSegments[1]
                db.delete("Book", "id = ?", arrayOf(bookId))
            }
            categoryDir -> db.delete("Category", selection, selectionArgs)
            categoryItem -> {
                val categoryId = uri!!.pathSegments[1]
                db.delete("Category", "id = ?", arrayOf(categoryId))
            }
            else -> 0
        }
        deletedRows
    } ?: 0

    override fun getType(uri: Uri?) = when (uriMatcher.match(uri)) {
        bookDir -> "vnd.android.cursor.dir/vnd.com.tomorrow.kotlindemo.book"
        bookItem -> "vnd.android.cursor.item/vnd.com.tomorrow.kotlindemo.book"
        categoryDir -> "vnd.android.cursor.dir/vnd.com.tomorrow.kotlindemo.category"
        categoryItem -> "vnd.android.cursor.item/vnd.com.tomorrow.kotlindemo.category"
        else -> null
    }
}

//MainActivity.kt
package com.tomorrow.kotlindemo

import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.contentValuesOf

class MainActivity : AppCompatActivity() {

    companion object {
        const val TAG = "MainActivity"
    }
    var bookId: String? = null

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

        //增
        var uri = Uri.parse("content://com.tomorrow.kotlindemo/book")
        var values = contentValuesOf("name" to "Android", "author" to "Google", "pages" to 999, "price" to 99)
        val newUri = contentResolver.insert(uri, values)
        Log.d(TAG, "zwm, newUri: $newUri")
        bookId = newUri?.pathSegments?.get(1)

        //改
        bookId?.let {
            uri = Uri.parse("content://com.tomorrow.kotlindemo/book/$it")
            values = contentValuesOf("price" to 9.9)
            contentResolver.update(uri, values, null, null)
        }

        //查
        uri = Uri.parse("content://com.tomorrow.kotlindemo/book")
        contentResolver.query(uri, null, null, null, null)?.apply {
            while (moveToNext()) {
                val name = getString(getColumnIndex("name"))
                val author = getString(getColumnIndex("author"))
                val pages = getInt(getColumnIndex("pages"))
                val price = getDouble(getColumnIndex("price"))
                Log.d(TAG, "zwm, $name $author $pages $price")
            }
            close()
        }

        //删
        bookId?.let {
            uri = Uri.parse("content://com.tomorrow.kotlindemo/book/$it")
            contentResolver.delete(uri,null, null)
        }
    }
}

by lazy 代码块是 Kotlin 提供的一种懒加载技术,代码块中的代码一开始并不会执行,只有当 uriMatcher 变量首次被调用的时候才会执行,并且会将代码块中最后一行代码的返回值赋给 uriMatcher。

4.Kotlin:泛型和委托

泛型的基本用法

在一般的编程模式下,我们需要给任何一个变量指定一个具体的类型,而泛型允许我们在不指定具体类型的情况下进行编程,这样编写出来的代码将会拥有更好的扩展性。泛型主要有两种定义方式:一种是定义泛型类,另一种是定义泛型方法,使用的语法结构都是 <T>。当然括号内的 T 并不是固定要求的,事实上你使用任何英文字母或单词都可以,但是通常情况下,T 是一种约定俗成的泛型写法。

定义泛型类:

class MyClasss<T> {
    
    fun method(param: T): T {
        return param
    }
}

val myClass = MyClass<Int>()
val result = myClass.method(123)

不定义泛型类,只定义泛型方法:

class MyClass {
    fun <T> method(param: T): T {
        return param
    }
}

val myClass = MyClass()
val result = myClass.method<Int>(123)

由于 Kotlin 具有非常出色的类型推导机制,也可以这么调用:
val myClass = MyClass()
val result = myClass.method(123)

对泛型设置上界:

class MyClass {

    fun <T : Number> method(param: T): T {
        return param
    }
}

在默认情况下,所有的泛型都是可以指定成可空类型的,这是因为在不手动指定上界的时候,泛型的上界默认是 Any?。而如果想要让泛型的类型不可为空,只需要将泛型的上界手动指定成 Any 就可以了。

举个例子(类似于 apply 函数):

fun <T> T.build(block: T.() -> Unit): T {
    block()
    return this
}

contentResolver.query(uri, null, null, null, null)?.build {
    while (moveToNext()) {
        ...
    }
    close()
}
类委托和委托属性

委托是一种设计模式,它的基本理念是:操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。Kotlin 也是支持委托功能的,并且将委托功能分为了两种:类委托和委托属性。

类委托的核心思想在于将一个类的具体实现委托给另一个类去完成。Kotlin 中委托使用的关键字是 by,我们只需要在接口声明的后面使用 by 关键字,再接上受委托的辅助对象,就可以免去之前所写的一大堆模板式代码了:

class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet {

    fun helloworld() = println("Hello World") //新增的方法

    override fun isEmpty() = false //重写的方法
}

类委托的核心思想是将一个类的具体实现委托给另一个类去完成,而委托属性的核心思想是将一个属性(字段)的具体实现委托给另一个类去完成。委托属性的语法结构:

class MyClass {
    var p by Delegate()
}

将 p 属性的具体实现委托给了 Delegate 类去完成,当调用 p 属性的时候会自动调用 Delegate 类的 getValue() 方法,当给 p 属性赋值的时候会自动调用 Delegate 类的 setValue() 方法。因此,我们还得对 Delegate 类进行具体的实现才行:

class Delegate {

    var propValue: Any? = null

    operator fun getValue(myClass: MyClass, prop: KProperty<*>): Any? {
        return propValue
    }

    operator fun setValue(myClass: MyClass, prop: KProperty<*>, value: Any?) {
        propValue = value
    }
}

这是一种标准的代码实现模板,在 Delegate 类中我们必须实现 getValue() 和 setValue() 这两个方法,并且都要使用 operator 关键字进行声明。getValue() 方法要接收两个参数:第一个参数 MyClass 用于声明该 Delegate 类的委托功能可以在什么类中使用;第二个参数 KProperty<*> 是 Kotlin 中的一个属性操作类,可用于获取各种属性相关的值,在当前场景下用不着,但是必须在方法参数上进行声明。至于返回值可以声明成任何类型,根据具体的实现逻辑去写就行了。setValue() 方法也是相似的,第三个参数表示具体要赋值给委托属性的值,这个参数的类型必须和 getValue() 方法返回值的类型保持一致。

整个委托属性的工作流程就是这样实现的,现在当我们给 MyClass 类的 p 属性赋值时,就会调用 Delegate 类的 setValue() 方法,当获取 MyClass 类的 p 属性的值时,就会调用 Delegate 类的 getValue() 方法。不过,如果 MyClass 类的 p 属性是用 val 关键字声明的话,就意味着 p 属性是无法在初始化之后被重新赋值的,因此也就没有必要实现 setValue() 方法,只需要实现 getValue() 方法就可以了。

实现一个自己的 lazy 函数

懒加载技术 by lazy 的基本语法结构:

val p by lazy { ... }

实际上,by lazy 并不是连在一起的关键字,只有 by 才是 Kotlin 中的关键字,lazy 在这里只是一个高阶函数而已。在 lazy 函数中会创建并返回一个 Delegate 对象,当我们调用 p 属性的时候,其实调用的是 Delegate 对象的 getValue() 方法,然后 getValue() 方法中又会调用 lazy 函数传入的 Lambda 表达式,这样表达式中的代码就可以得到执行了,并且调用 p 属性后得到的值就是 Lambda 表达式中最后一行代码的返回值。

实现一个自己的 lazy 函数:

//Later.kt
package com.tomorrow.kotlindemo

import kotlin.reflect.KProperty

class Later<T>(val block: ()->T) {

    var value: Any? = null

    operator fun getValue(any: Any?, prop: KProperty<*>): T {
        if(value == null) {
            value = block()
        }
        return value as T
    }
}

fun <T> later(block: () -> T) = Later(block)

//MainActivity.kt
package com.tomorrow.kotlindemo

import android.os.Bundle
import android.os.Handler
import android.util.Log
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    companion object {
        const val TAG = "MainActivity"
    }

    private val value by later {
        Log.d(TAG, "zwm, lazy init")
        "good job"
    }

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

        Handler().postDelayed({
            Log.d(TAG, "zwm, value: $value")
        }, 3000)
    }
}

日志打印:
2020-10-21 14:04:08.051 16892-16892/com.tomorrow.kotlindemo D/MainActivity: zwm, lazy init
2020-10-21 14:04:08.052 16892-16892/com.tomorrow.kotlindemo D/MainActivity: zwm, value: good job

由于懒加载技术是不会对属性进行赋值的,因此这里就不用实现 setValue() 方法了。这里只是大致还原了 lazy 函数的基本实现原理,在正式项目中,使用 Kotlin 内置的 lazy 函数才是最佳的选择。

八、多媒体

1.使用通知

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

通知的基本用法
//MainActivity.kt
package com.tomorrow.kotlindemo

import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationCompat

class MainActivity : AppCompatActivity() {

    companion object {
        const val TAG = "MainActivity"
    }

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

       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)
        }
        val intent = Intent(this, SecondActivity::class.java)
        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.ic_launcher)
            .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.ic_launcher_round))
            .setContentIntent(pi)
            .setAutoCancel(true)
            .build()
        manager.notify(1, notification)
    }

    override fun onDestroy() {
        super.onDestroy()
        val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        manager.cancel(1)
    }
}
通知的进阶技巧

在通知当中显示一段长文字:

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)
}
val intent = Intent(this, SecondActivity::class.java)
val pi = PendingIntent.getActivity(this, 0, intent, 0)
val notification = NotificationCompat.Builder(this, "normal")
    .setContentTitle("This is content title")
    .setSmallIcon(R.drawable.ic_launcher)
    .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.ic_launcher_round))
    .setContentIntent(pi)
    .setAutoCancel(true)
    .setStyle(NotificationCompat.BigTextStyle().bigText("This is content text aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"))
    .build()
manager.notify(1, notification)

在通知当中显示一张大图片:

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)
}
val intent = Intent(this, SecondActivity::class.java)
val pi = PendingIntent.getActivity(this, 0, intent, 0)
val notification = NotificationCompat.Builder(this, "normal")
    .setContentTitle("This is content title")
    .setSmallIcon(R.drawable.ic_launcher)
    .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.ic_launcher_round))
    .setContentIntent(pi)
    .setAutoCancel(true)
    .setStyle(NotificationCompat.BigPictureStyle().bigPicture(BitmapFactory.decodeResource(resources, R.drawable.test)))
    .build()
manager.notify(1, notification)

2.调用摄像头和相册

调用摄像头拍照
//AndroidManifest.xml
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="com.tomorrow.kotlindemo.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths"/>
</provider>

//res/xml/file_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-cache-path name="my_images" path="/" />
</paths>

//MainActivity.kt
package com.tomorrow.kotlindemo

import android.app.Activity
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 android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import kotlinx.android.synthetic.main.activity_main.*
import java.io.File

class MainActivity : AppCompatActivity() {

    companion object {
        const val TAG = "MainActivity"
    }

    lateinit var imageUri: Uri
    lateinit var outputImage: File

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

        outputImage = File(externalCacheDir, "output_image.jpg")
        if(outputImage.exists()) {
            outputImage.delete()
        }
        outputImage.createNewFile()
        Log.d(TAG, "zwm, outputImage: $outputImage")
        imageUri = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            FileProvider.getUriForFile(this, "com.tomorrow.kotlindemo.fileprovider", outputImage)
        } else {
            Uri.fromFile(outputImage)
        }
        Log.d(TAG, "zwm, imageUri: $imageUri")
        val intent = Intent("android.media.action.IMAGE_CAPTURE")
        intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri)
        startActivityForResult(intent, 1)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        when (requestCode) {
            1 -> {
                if(resultCode == Activity.RESULT_OK) {
                    val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(imageUri))
                    imageView.setImageBitmap(rotateIfRequired(bitmap))
                }
            }
        }
    }

    private fun rotateIfRequired(bitmap: Bitmap): Bitmap {
        val exif = ExifInterface(outputImage.path)
        val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
        Log.d(TAG, "zwm, rotateIfRequired, orientation: $orientation")
        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 rotateBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
        bitmap.recycle() //将不需要的 Bitmap 对象回收
        return rotateBitmap
    }
}

日志打印:
2020-10-22 10:27:55.615 8560-8560/com.tomorrow.kotlindemo D/MainActivity: zwm, outputImage: /storage/emulated/0/Android/data/com.tomorrow.kotlindemo/cache/output_image.jpg
2020-10-22 10:27:55.616 8560-8560/com.tomorrow.kotlindemo D/MainActivity: zwm, imageUri: content://com.tomorrow.kotlindemo.fileprovider/my_images/output_image.jpg
2020-10-22 10:28:05.215 8560-8560/com.tomorrow.kotlindemo D/MainActivity: zwm, rotateIfRequired, orientation: 6

从 Android 6.0 系统开始,读写 SD 卡被列为了危险权限,如果将图片存放在 SD 卡的任何其他目录,都要进行运行时权限处理才行,而使用应用关联目录则可以跳过这一步。另外,从 Android 10 系统开始,公有的 SD 卡目录已经不再允许被应用程序直接访问了,而是要使用作用域存储才行。

如运行设备的系统版本低于 Android 7.0,就调用 Uri 的 fromFile() 方法将 File 对象转换成 Uri 对象,这个 Uri 对象标识着 output_image.jpg 这张图的本地真实路径。否则,就调用 FileProvider 的 getUriForFile() 方法将 File 对象转换成一个封装过的 Uri 对象。因为从 Android 7.0 系统开始,直接使用本地真实路径的 Uri 被认为是不安全的,会抛出一个 FileUriExposedException 异常。而 FileProvider 则是一种特殊的 ContentProvider,它使用了和 ContentProvider 类似的机制来对数据进行保护,可以选择性地将封装过的 Uri 共享给外部,从而提高了应用的安全性。

需要注意的是,调用照相机程序去拍照有可能会在一些手机上发生照片旋转的情况。这是因为这些手机认为打开摄像头进行拍摄时就应该是横屏的,因此回到竖屏的情况下就会发生 90 度的旋转。为此,需要添加判断图片方向的代码,如果发现图片需要进行旋转,则先将图片旋转相应的角度。

从相册中选择图片
//MainActivity.kt
package com.tomorrow.kotlindemo

import android.app.Activity
import android.content.Intent
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    companion object {
        const val TAG = "MainActivity"
    }

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

        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
        intent.addCategory(Intent.CATEGORY_OPENABLE)
        intent.type = "image/*"
        startActivityForResult(intent, 1)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        when (requestCode) {
            1 -> {
                if(resultCode == Activity.RESULT_OK && data != null) {
                    data.data?.let {
                        Log.d(TAG, "zwm, uri: $it")
                        val bitmap = getBitmapFromUri(it)
                        imageView.setImageBitmap(bitmap)
                    }
                }
            }
        }
    }

    private fun getBitmapFromUri(uri: Uri) = contentResolver.openFileDescriptor(uri, "r")?.use {
        BitmapFactory.decodeFileDescriptor(it.fileDescriptor)
    }
}

日志打印:
2020-10-22 10:43:12.381 13597-13597/com.tomorrow.kotlindemo D/MainActivity: zwm, uri: content://com.android.providers.media.documents/document/image%3A650

3.播放多媒体文件

播放音频

在 Android 中播放音频文件一般是使用 MediaPlayer 类实现的,它对多种格式的音频文件提供了非常全面的控制方法,从而使播放音乐的工作变得十分简单。

MediaPlayer 类中常用的控制方法:

方法名 功能描述
setDataSource() 设置要播放的音频文件的位置
prepare() 在开始播放之前调用,以完成准备工作
start() 开始或继续播放音频
pause() 暂停播放音频
reset() 将 MediaPlayer 对象重置到刚刚创建的状态
seekTo() 从指定的位置开始播放音频
stop() 停止播放音频。调用后的 MediaPlayer 对象无法再播放音频
release() 释放与 MediaPlayer 对象相关的资源
isPlaying() 判断当前 MediaPlayer 是否正在播放音频
getDuration() 获取载入的音频文件的时长
//MainActivity.kt
package com.tomorrow.kotlindemo

import android.media.MediaPlayer
import android.os.Bundle
import android.os.Handler
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    companion object {
        const val TAG = "MainActivity"
    }

    private val mediaPlayer = MediaPlayer()

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

        initMediaPlayer()
        mediaPlayer.start()
        Handler().postDelayed({
            mediaPlayer.pause()
        }, 5000)
    }

    override fun onDestroy() {
        super.onDestroy()
        mediaPlayer.stop()
        mediaPlayer.release()
    }

    private fun initMediaPlayer() {
        val assetManager = assets
        val fd = assetManager.openFd("Over_the_Horizon.mp3")
        mediaPlayer.setDataSource(fd.fileDescriptor, fd.startOffset, fd.length)
        mediaPlayer.prepare()
    }
}

Android Studio 允许我们在项目工程中创建一个 assets 目录(在 main 目录下,和 java、res 这两个目录是平级的),并在这个目录下存放任意文件和子目录,这些文件和子目录在项目打包时会一并被打包到安装文件中,然后我们在程序中就可以借助 AssetManager 这个类提供的接口对 assets 目录下的文件进行读取。

播放视频

播放视频文件主要是使用 VideoView 类来实现的。其实 VideoView 只是帮我们做了一个很好的封装而已,它的背后仍然是使用 MediaPlayer 对视频文件进行控制的。另外需要注意,VideoView 并不是一个万能的视频播放工具类,它在视频格式的支持以及播放效率方面都存在着较大的不足。

VideoView 的常用方法:

方法名 功能描述
setVideoPath() 设置要播放的视频文件的位置
start() 开始或继续播放视频
pause() 暂停播放视频
resume() 将视频从头开始播放
seekTo() 从指定的位置开始播放视频
isPlaying() 判断当前是否正在播放视频
getDuration() 获取载入的视频文件的时长
//MainActivity.kt
package com.tomorrow.kotlindemo

import android.net.Uri
import android.os.Bundle
import android.os.Handler
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    companion object {
        const val TAG = "MainActivity"
    }

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

        val uri = Uri.parse("android.resource://$packageName/${R.raw.test}")
        videoView.setVideoURI(uri)
        videoView.start()
        Handler().postDelayed({
            videoView.pause()
        }, 5000)
    }

    override fun onDestroy() {
        super.onDestroy()
        videoView.suspend()
    }
}

VideoView 不支持直接播放 assets 目录下的视频资源,我们在 res 目录下创建一个 raw 目录,像诸如 音频、视频之类的资源文件也可以放在这里,并且 VideoView 是可以直接播放这个目录下的视频资源的。

4.Kotlin:infix 函数

infix 函数只是把编程语言函数调用的语法规则调整了一下而已,比如 A to B 这样的写法,实际上等价于 A.to(B) 的写法。

举个例子:

infix fun String.beginsWith(prefix: String) = startsWith(prefix)

fun main() {
    if("Hello Kotlin" beginsWith "Hello") {
        //处理具体的逻辑
    }
}

infix 函数由于其语法糖格式的特殊性,有两个比较严格的限制:首先,infix 函数是不能定义成顶层函数的,它必须是某个类的成员函数,可以使用扩展函数的方式将它定义到某个类当中;其次,infix 函数必须接收且只能接收一个参数,至于参数类型是没有限制的。只有同时满足这两点,infix 函数的语法糖才具备使用的条件。

to 函数的源码:

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

A to B 这样的语法结构实际上得到的是一个包含 A、B 数据的 Pair 对象,而 mapOf() 函数实际上接收的正是一个 Pair 类型的可变参数列表。

相关文章

网友评论

      本文标题:第一行代码 -- 笔记2

      本文链接:https://www.haomeiwen.com/subject/zqnsvktx.html