添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
有胆有识的鼠标垫  ·  PHP: ...·  昨天    · 
瘦瘦的斑马  ·  戴铭的博客·  昨天    · 
失落的勺子  ·  app | Electron 中文网·  昨天    · 
风度翩翩的柿子  ·  Scope functions | ...·  6 小时前    · 
善良的筷子  ·  OVERFLOW | ...·  2 月前    · 
咆哮的地瓜  ·  CompilationGuide/Ubunt ...·  2 月前    · 

官方手册: Android 的 Kotlin 优先方法 | Android 开发者 | Android Developers (google.cn)

中文手册 : Kotlin/Native - Kotlin 语言中文站 (kotlincn.net)

参考文档 - Kotlin Programming Language (liying-cn.net)

Android精华教程_郭霖的专栏-CSDN博客

```kotlin // 基本类型有:数字、字符、布尔值、数组与字符串 // 变量默认为 publish // 变量声明时必须初始化,而且不能为 null val str: String = null // error val str: String? = null // 指定一个可 null 的值 val len = str?.length // 自动兼容 null val len = str!!.length // 强制关闭 null 检测
val str: String // 变量声明后不可修改 var str: String // 变量声明后要修改 lateinit var str: String // 声明一个变量可以在以后初始化 ,不需要先赋值, 使用 ::str.isInitialized 判断有没有初始化 val name: Int by lazy { 1 } // 延迟初始化

fun main(): Unit {} // 表示返回 void
fun main() {} // 可省略
// 函数中的函数
fun login(user: String, password: String, illegalStr: String) {
    fun validate(value: String) {
      if (value.isEmpty()) {
          throw IllegalArgumentException(illegalStr)
    validate(user, illegalStr)
    validate(password, illegalStr)

可见性修饰符

public:公开,可见性最大,哪里都可以引用。
private:私有,可见性最小,根据声明位置不同可分为类中可见和文件中可见。
protected:保护,相当于 private + 子类可见。
internal:内部,仅对 module 内可见。
// 类默认为 public
// 类默认为 final 不可继承,需要手工指定 open, 但是子类默认也是 final
// 重载函数 override, 如果不想子类再重载,在函数前+ final
// abstract 虚拟类,与 c# 类似 
// 实例化不需要使用 now
var activity: Activity = NewActivity()
// is 类型判断 
if (activity is NewActivity) {        
    activity.action() // 的强转由于类型推断被省略了
// as 类型转换
(activity as NewActivity).action() // 如果转换失败,会抛出异常
(activity as? NewActivity)?.action() // 使用 as? , 转型失败返回 null,不调用,不会有异常
// class Person (private val name: String) 可如下简写, 需要注明 val 或是 var ,次构造函数不能指定 val/var ,并且必须使用 this 调用  
// 主构造函数可以设置为 private , class Person private constructor(private val name: String)
// 主构造函数参数默认在类中创建对应的属性
class Person constructor(private val name: String) : Eater { 
    var age: Int = 0
    init {
        // 主构造函数
    constructor(name: String, age: Int) : this(name) {
        this.age = age
// 或是指定类为 文件名Kt, 默认为 public
class Program {
    companion object {
        // 入口函数, 静态函数
        @JvmStatic
        fun main(args: Array<String>) {
            Rect.ShowDemo()
package Gnlab.X // 定义一个包名
// 自动生成一个 只读属性 Height , 一个私有的读写属性 width
class Rect(val Height: Int, private var width: Int) {
    companion object {
        // 演示类属性, 相当于静态函数
        fun ShowDemo() {
            val rect = Rect(220, 220)
            println("$rect")
            println("${rect.name}")
            println(rect.Height)
            rect.isChanged = false
            println("IsChanged: ${rect.isChanged} ")
            println("IsSquare: ${rect.isSquare}")
            println("demo")

字段、属性

open class Person {
    val lastName = "mike" // 字段,其实后台会编译为属性(加入 get set)
    // 一个只读属性, 后台会编译为属性(加入 get set)
    val name: String = "rect name"
    // 一个读写属性
    var isChanged: Boolean = true
    // 自定义一个访问器
    val isSquare: Boolean
        get() {
            return Height == width
    var age = "demo" // 属性,并可进行赋值或计算返回
        get() {
            return field + " nb"
        set(value) {
            field = "Cute " + value
    val name = "Mike" // 只读属性,也可以进行计算返回 
        get() {
            return field + " nb"
    // 简写,带有参数默认值
    fun area(width: Int, height: Int = 32): Int = width * height

静态函数、属性、静态类

// object 关键词,直接创建一个对象 , 就是单例对象, 而且线程安全 
//////////////////////////////////////////////////////////////////////////////////////////
// 相当于静态类
object Oper {
    fun GetName(): String {
        return "test"
// 调用 
Oper.GetName()
//////////////////////////////////////////////////////////////////////////////////////////
// 匿名对象实现接口
interface IOper {
    fun getName(): String
val oper = object : IOper {
    override fun getName():String {
        return "test"
val name = oper.getName()
Log.i("info", name)
//////////////////////////////////////////////////////////////////////////////////////////
// 静态属性、函数
class A {
    // 相当于实例化一个对象,所有静态属性或函数都放在这里。 B 可以省略。
    // 一个类中只允许一个内部的伴生对象,所以只能定义一个 companion object 
    companion object B {
        var c: Int = 12
        fun getName(): String {
            return "name"
        // 定义一个静态对象,应该可以被 java 调用 
         @JvmStatic fun doAction(){
            println("test")
Log.i("test", "${A.B.c}")
Log.i("test", "${A.c}") // B 可以省略
Log.i("test", "${A.B.getName()}")
Log.i("test", "${A.getName()}") // B 可以省略
// 使用 data class 自动完成 equals hashCode toString 这些重载函数
// 以简化常用类
data class Cellphone(val brand: String, val price: Double )
sealed class Result
class Success(val msg: String) : Result
class Failure(val error: Exception):Result
// 密封类的继承必须在同一个 kt 文件中,when 可以不用写 else, 因为继承类得到控制
val strs: Array<String> = arrayOf("a", "b", "c") // 数组 相当于 c# Array / []
val intArray = intArrayOf(1, 2, 3) // 特定类型的 array
val strList = listOf("a", "b", "c") // 相当于 list, mutableListOf 可变 或 toMutableList
val strSet = setOf("a", "b", "c") // mutableSetOf 可变 或 toMutableSet
val map = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key4" to 3) // 使用 to 将 key 与 value 创建连接, mutableMapOf 可变 可 toMutableMap
sequenceOf("a", "b", "c") // 不确定为什么多一个这个类型

常用操作 (lambda)

intArray.forEach { i -> print(i + " ") } // 遍历每一个元素
val newList: List = intArray.filter { i -> i != 1 } // 过滤掉数组中等于 1 的元素
val newList: List = intArray.map { i -> i + 1 } // 每个元素加 1, 得到一个新数组 
intArray.flatMap { i -> listOf("${i + 1}", "a") } // 每个元素生成新集合,并合并到一个新的数组中, linq 中的 manayselect
intArray.maxBy {  it.length }  // 取出最大值 
intArray.any { } // 某一项为 true
intArray.all { } // 所有为 true

Range

val range: IntRange = 0..1000 // [1, 1000]
val range: IntRange = 0 until 1000 // [0, 1000) 
var range = 0..1000 step 2 //
var range = 1000 downTo 1 // [1000, 1]                               

Sequence

相当于 c# linq

        // 类似于 linq 惰性计算,先编译计算公式,调用 first 之类才进行计算,并且计算出指定的值后即停止。下面的例子只计算 3 次
        val it1 = (1..1000).asSequence()
            .map { it -> Log.i("info", "map $it"); it * 2 }
            .filter { it -> Log.i("info", "filter $it"); it % 3 == 0 }
            .first()
        // 类比于 sequence map/filter 调用一次,所有的值都会计算一次
        val it2 = (1..1000).asIterable()
            .map { it -> Log.i("info", "map $it"); it * 2 }
            .filter { it -> Log.i("info", "filter $it"); it % 3 == 0 }
            .first()
// 拼接字符串
String.format("Hi %s", name)
"Hi $name"
"Hi ${name.length}"
// 原生字符串,相当于 C# @""
val text = """
      Hi $name!
    My name is $myName.\n
// 逐行格式化, trimMargin, 默认去掉每一行前的 |及空格 
val text = """
      |Hi world!
    |My name is kotlin.
""".trimMargin()

lambda 函数简写

// 传入一个接口实例,相当于匿名接口实现类, 比如 Runnable 接口
Thread(object: Runnable { override fun run(){ Log.i('test') } }).start() // 使用 object 标志此为一个实例 
// 简写I, 因为该接口只要实现一个实现,所以自动推断实现函数
Thread( Runnable {Log.i('test')}).start() // 增掉函数声明部分
// 因为是传入接口,会自动推断为何接口,简写II
Thread( { Log.i('test') }).start() // 只要实现部分
// 如果函数参数就是一个 lambda ,则函数括号可以省略,简写III
Thread { Log.i('test') }.start() // 去掉括号
button.setOnClickListener { 鼠标事件对应的函数体 }

if/else

// 类似于三元计算 val it = if (a < b) 12 else 15

枚举/when

替代 switch

enum class Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
////////////////////////////////////////////////
// 也可以带有属性的类作为枚举 
enum class Color(val r: Int, val g: Int, val b: Int) {
    // 初始化枚举, ',' 进行分隔
    RED(255, 0, 0), ORANGE(0, 255, 0), BLUE(0, 0, 255);
    fun rgb() = ( r * 256 + g ) * 256 + b
    companion object{
        fun getName(color: Color) : String {
            return when(color){
                RED -> "RED"
                ORANGE -> "ORANGE"
                BLUE -> "BLUE"
                else -> throw Exception("unknown error")
        when (x) {
            in 1..10 -> print("x 在区间 1..10 中")
            in listOf(1, 2) -> print("x 在集合中")
            !in 10..20 -> print("x 不在区间 10..20 中")
            is String -> print("x 是不是 string")
            ((x * 3) == 32) -> print("进行计算")
            else -> print("不在任何区间上")

for-in

for (i in 0..10) {
    println(i)

try-catch

try {
catch (e: Exception) {
finally {
// 直接返回值
val a: Int? = try { parseInt(input) } catch (e: NumberFormatException) { null }

?. 和 ?:

val str: String? = "Hello" // 可为空对象S=
var length: Int = str?.length // error , 因为此为 Int?
val length: Int = str?.length ?: -1 // 正确, 相当于 c# ??
// let, 相当于做一次  if ( val != null) , 但是 if 线程不安全,let 线程安全
val str: String? = "test"
str?.let {
    it.substring(0, 2)
    val len = it.length

== 和 ===

== :可以对基本数据类型以及 String 等类型进行内容比较
=== :对引用的内存地址进行比较
val str1 = "123"
val str2 = "123"
println(str1 == str2) // 👈 内容相等,输出:true
val str1= "字符串"
val str2 = str1
val str3 = str1
print(str2 === str3) // 👈 引用地址相等,输出:true
val bufferedWriter = BufferedWriter(streamWriter)
// use 调用完成后,自动调用关闭函数
bufferedWriter.use{
    it.write("测试内容")
// let, 相当于做一次  if ( val != null) {  }, 但是 if 线程不安全,let 线程安全
val str: String? = "test"
str?.let {
    it.substring(0, 2)
    val len = it.length
// with , 传入一个对象,with 中进行该对象的上下文,可以精简一些写法 
val range: IntRange = 0..1000 // [1, 1000]
        val result = with(StringBuilder()) { // 传入 with 的对象
            append("title") // 进入 with 对象的上下文
            range.forEach { append(it) } // 进入 with 对象的上下文
            append("end") // 进入 with 对象的上下文
            toString() // 作为 with 的返回值
// 与 with 一样,写法不同。对一个对象调用 run ,进入该对象的上下文进行处理。最后一行返回结果
        val range: IntRange = 0..1000 // [1, 1000]
        StringBuilder().run { 
            append("title")  // 进入 with 对象的上下文
            range.forEach { append(it) }  // 进入 with 对象的上下文
            append("end")  // 进入 with 对象的上下文
            toString() // 作为 with 的返回值

apply

// 与 run 一样,只是没有返回值,进行对象的上下文进行处理
        val range: IntRange = 0..1000 // [1, 1000]
        var result = StringBuilder().apply { // 但是没有返回值,返回值即对象本身
            append("title") // 进入 with 对象的上下文
            range.forEach { append(it) } // 进入 with 对象的上下文
            append("end") // 进入 with 对象的上下文
        Log.i("log", result.toString())
// 1 最好作为顶层函数
// 2 类名.函数名, 函数中使用 this 来访问该对象
fun String.lettersCount(): Int {
    var count = 0
    for (char in this) {
        if (char.isLetter()) {
            count++
    return count
// 调用 
"abc".lettersCount()

运算符重载

// + plus; - minus; * times; / div; % rem; ++ inc; -- dec; ! not; == equals; >|<|>=|<=| compareTo; a..b rangeTo; [] get|set;  in contains
class Obj {
    operator fun plus(obj: Obj) { /* 处理加法 */
        this.count += obj.count

参数或是返回值为函数的函数为高阶函数

# (函数参数类型, ) -> 返回值类型,如果无返回值,为 Unit
(String, Int) -> Unit 
fun num1AndNum2(num1: Int, num2: Int, oper: (Int, Int) -> Int) :Int{
    return oper(num1, num2)
# 调用, ::plush 为一个顶层函数
num1AndNum2(1, 2, ::plus)
# lambda 写法
num1AndNum2(1, 3) { num1, num2 -> num1 + num2 }
# 辅助构造函数, StringBuilder.() 表示在哪个上下文中,类似于 apply
# 1. build 前的 StringBuilder 表示,这是 StringBuilder 的扩展函数, 
# 2. block 后的 StringBuilder 表示 block 的调用上下文是必须为 StringBuilder, 如果不加,则 block 为普通函数
# 3. 第三个 StringBuilder 表示,返回值类型
fun StringBuilder.build(block:StringBuilder.() -> Unit): StringBuilder {
    ... 进行一些自定义的初始化操作, 
    this.block()
    return this
val result = StringBuild().build { /* 自定义初始化内容 */  }

高阶函数的原理是使用 java 的一个 Function 接口,在函数参数中传入该接口的匿名类实现,但是每调用一次就会实例化一次接口实例效率会比较差

而内联就是将对函数的调用,在调用的地方直接展开,这样可以减少开稍

############################################################################
### 那么每个调用的地方就会将函数展开
inline fun num1AndNum2(num1: Int, num2: Int, oper: (Int, Int) -> Int) :Int{
    return oper(num1, num2)
############################################################################
### noinline 指定传入函数不进行展开
inline fun num1AndNum2(
    num1: Int,
    num2: Int,
    oper1: (Int, Int) -> Int,
    noinline oper2: (Int, Int) -> Int // 该函数不进行展开
): Int {
    return oper1(num1, num2) + oper2(num1, num2)
############################################################################
### lambda 中的 return , lambda 中不允许使用 return ,需要使用 return@函数名 的形式进行局部返回
### 但是如果函数进行了展开,则可以使用 return ,因为 return 会被在调用函数中展开的,但是这样就是将上层函数 return 了
############################################################################
### crossinline 
### 像下面这样调用会发生错误,
### 1. inline 函数中 block 中是可以使用 return 的。
### 2. 匿名函数实现 Runnable 中使用了 lambda 调用,是不允许使用 return 的。
### 3. 1、2 造成冲突。所以需要定义 block 为 crossinline , 表示 block 中确保不会调用 return。像这样 inline fun runRunnable( crossinline block: () -> Unit )  { ... }
inline fun runRunnable( block: () -> Unit ) :Runnable {
    // 内部使用了匿名接口实现,实现 Runnable 接口
    return  Runnable{
        block()

高阶函数应用

/************************************************************************************/ /** 1. SharedPreferences 的 open 扩展函数, this 为 SharedPreferences */ /** 2. block 为 SharedPreferences.Editor 上下文的回调函数, 回调函数中为 Editor */ fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit ){ val editor = this.edit() /** todo: 进行一些通用化操作 */ editor.block() editor.apply() this.getSharedPreferences("tmp", MODE_PRIVATE).open{} /************************************************************************************/ /** 一个创建数据库操作字典的快捷函数 */ /** vararg 指定这是一个可变函数 */ /** Pair<> 使用 a to b 的语法创建键值对 */ fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues { val values = ContentValues() // 枚举出所有参数 for (it in pairs){ val key = it.first // 因为 values 没有泛型的 put, 所以下面使用转型后调用 put when(val value = it.second){ is Int -> values.put(key, value) is Long -> values.put(key, value) is Short -> values.put(key, value) is Float -> values.put(key, value) // ... 剩下其它类型的赋值 return values cvOf("name" to 2, "author" to "x", "price" to 32.4 ) // 定义一个函数, 前面加上 infix 并指定一个参数,则为 xxxx 函数名 参数 infix fun String.beginsWith(prefix:String) = startsWith(prefix) if( "this is test " beginsWith "test" ){ } // 调用事例 // 构建 map 的 to 语法, 定义如下 infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

泛型与委托

使用上与 C# 很像

// 泛型类
class MyClass<T> {
    fun method(param: T): T {
        return param
// 泛型函数
fun <T> func(param: T) :T{ return param }
// 泛型类型限制
fun <T:Number> func(param: T):T { return param }

比如原来的 build 函数,使用了指定类型,这里可以转为泛型

// 这里指定了类型必须为 StringBuilder fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder { // todo: 进行一些通用初始化 block() return this // 这里使用了泛型,使用范围可能更广 fun <T> T.build(block: T.() -> Unit):T{ // todo: 进行一些通用初始化 this.block() return this

java/kotlin 的泛型只有编译时有效。运行时没有泛型的概念(称为类型擦除机制)。kotlin 更进一步,引用内联机制进行泛型的实化。这样可以使用 T::class.java 类型

# 1. 使用 inline 声明内联 # 2. 泛型使用 reified inline fun <reified T> getGenericType() = T::class.java val strType = getGenericType<String>() val intType = getGenericType<Int>() # 例子, startActivity 的辅助函数 inline fun <reified T> startActivity(context: Context, block: Intent.() -> Unit) { val intent = Intent(context, T::class.java) # 1. 生成 Intent intent.block() # 2. 调用 block 进行初始化 context.startActivity(intent) # 3. startActivity 启动 startActivity<MyService>(this){ this.putExtra("item1", 1) this.putExtra("item2", 2)

泛型的协变与逆变

# 1. 现象
    泛型列表对象不能传至父类参数的函数中, 比如存在 B:A, list<B> 不能传到 fun(list<A>) 中
# 2. 原因
    因为如果存在 C:A, 在函数 fun(list<A>) 中可以调用 list<A>.add(C), 但是传入的实际上是 list<B>, 这是不允许的 
# 3. 解决
    声明泛型数据在类中只能通过函数返回,不能设置 (相当于只读),这叫做协变,就能安全的传入函数中
    使用 out 声明
    class SimpleData<out T>(val data: T?) // val data 或是 private var data 这样不能外部设置
        fun get(): T? = data
        fun set(data: T?){ } # 不能这样用,因为会有设置
# 4. 特例
    class SimpleData<out T>(val data: T) {
        fun set(data: @UnsafeVariance T?){ } # 使用 @UnsafeVariance 声明不会破坏内部数据,比如传入的 data 仅用来进行数据比较之类的, 编译器对放行
    比如 list<T> 是只读的,它被声明为协变的。但是 contains 会传入 T ,所以加了 @UnsafeVariance
# 1. 现象
    泛型对象不能传至子类参数的函数中, 比如存在 B:A SimpleData<A> 不能会传入 fun(SimpleData<B>) 中
# 2. 原因
    因为如果存在 C:A 在函数 fun(SimpleData<B>) 中可能调用 SimpleData<B>[0] 返回一个 C     
    class SimpleData<A>() {
        fun get() : A{ 
            return C() 
    func test(data: SimpleData<B>){
        data.get() # 因为参数声明为 B, get 应该返回 B, 但是可能会传入  SimpleData<A> ,这时返回 C ,在运行时会报错
# 3. 解决
     声明泛型类只能传入参数不能传出参数, 使用 in 进行声明 
    class SimpleData<in A>() {
        fun get() : A{ 
            return C() 
# 4. 特例
    其实我有点晕
    class SimpleData<in A>() {
        fun get() : @UnsafeVariance A { 
            return C() 

当我们实现一个接口的时候,如果接口比较多,有一种方式,可以内置一个实现该接口的对象,然后将所有接口实现都调用该对象对应的接口。但是如果接口太多的话,代码量太大了一些。

kotlin 可以在语法层面实现该功能,使用 by 语法,可以自动指定接口函数由某个对象实现

** 我的想法,直接继承内置类就不就可以了吗?

// 1. 自定义一个实现 Set 接口的类 MySet, 并为一个泛型类,
// 2. 内置一个 helperSet 字段,
// 3. 使用 by 指定自定义类的 Set 接口实现都由 helperSet 实现
class MySet<T>(private val helperSet: HashSet<T>) : Set<T> by helperSet {
    // 重载某个函数
    override fun isEmpty(): Boolean {
        // 进行一些自定义操作
        return this.helperSet.isEmpty()
    // 实现一个自定义函数
    fun hello(): String {
        return "hello"

将一个属性(字段)的实现交给另一个类实现

class MyClass { // 调用 Delegate 的 getValue 与 setValue, // 读取 p 时调用 Delegate.getValue // 设置 p 时调用 Delegate.setValue // 如果是 val,则 Delegate 可以不用实现 Delegate.setValue var p by Delegate() class Delegate { var propValue: Any? = null // myClass: MyClass 表示可以在哪个类中使用 // prop: KProperty<*>:应该相当于 C# 中的 Type operator fun getValue(myClass: MyClass, prop: KProperty<*>): Any? { return this.propValue operator fun setValue(myClass: MyClass, prop: KProperty<*>, value: Any?) { this.propValue = value
委托属性应用

lazy 就使用了 by 关键字,下面自行实现一个

// 1. 初始化类时传入 T 的构建函数 class Later<T>(val block: () -> T) { var value: Any? = null operator fun getValue(any: Any?, prop: KProperty<*>): T { // 2. 被调用时如果第一次被调用,则 if (value == null) { value = block() // 返回生成的对象 return value as T // 1.1 直接调用的方式 class MyClass { val p: StringBuilder by Later<StringBuilder> { val builder = StringBuilder() builder.appendLine("test") builder // 2. 将委托属性的类包装为一个函数 fun <T> later(block: () -> T) = Later<T>(block) // 2.1 调用方式, 感觉与直接生成类对象(1.1)比较,没有啥区别 class MyXlass { val P: StringBuilder by later<StringBuilder> { val builder = StringBuilder() builder.appendLine("test") builder

kotlin 高级应用

vararg

不定数量参数

    /* 返回不定长度数值的最大值 */
    fun max(vararg nums: Int) : Int{
        var current = nums.first()
        for (num in nums){
            current = max(current, num)            
        return current
    /* 返回不定长度数值的最大值, 不指定类型, 因为 java 数值实现接口  Comparable*/
    fun <T: Comparable<T>> max(vararg nums: Int) : Int{
        var current = nums.first()
        for (num in nums){
            if(num > current ){
                current = num
        return current

使用扩展函数,将一些调用函数简写

    /** 字符串扩展,提示提示 */
    fun String.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT) {
        Toast.makeText(context, this, duration).show()
    /** 资源文件扩展,提示提示 */
    fun Int.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT) {
        Toast.makeText(context, this, duration).show()
    /** 显示提示窗口 */
    fun View.showSnackbar(text:String, duration: Int = Snackbar.LENGTH_SHORT, actionText: String, block: (() -> Unit) ? = null){
        val snackbar = Snackbar.make(this, text, duration)
        if(!actionText.isNullOrEmpty()){
            snackbar.setAction(actionText){
                block?.invoke()
        snackbar.show()

DSL 领域特定语言(Domain Specific Language)。可以脱离原有的语法结构,构建出一套专有的语法结构。kotlin 有这种能力生成 DSL

// 实现一个如下语法 
dependency {
    implementation("x.com.lib.test1")
    implementation("x.com.lib.test2")
1. 实现一个类,作为基础。可以输入一些字符串作为类库保存于一个列表中
class Dependency {
    val libaries = ArrayList<String>()
    fun implementation(lib: String){
        libaries.add(lib)
2. 实现语法 
    2.1 实现一个顶层函数 dependency
    2.2 传入一个 lambda 函数,该函数是 Dependency 的扩展函数
    2.3 函数内部生成 Dependency 对象,并调用该对象的扩展 labmbda 函数
    2.4 进行返回,有没有返回,或是返回什么无所无所谓
fun dependency(block: Dependency.() -> Unit):List<String>{
    val dependencies = Dependency()
    dependencies.block()
    return dependencies.libaries

构建 html DSL

# 调用函数如下,生成一个 table html 
val html = table {        
                td { "Apple" } 
                td { "Grape" } 
                td { "Orange" } 
// 1. td 层
class Td {
    var content = ""
    fun html() = "\n\t\t<td>$content</td>"
// 2. tr 层
class Tr {
    private val children = ArrayList<Td>()
    fun td(block: Td.() -> String) {
        val td = Td()
        td.content = td.block()
        children.add(td)
    fun html(): String {
        val builder = StringBuilder()
        builder.append("\n\t<tr>")
        for (it in children) {
            builder.append(it.html())
        builder.append("\n\t</tr>")
        return builder.toString()
// 3. table 层
class Table {
    private val children = ArrayList<Tr>()
    fun tr(block: Tr.() -> Unit) {
        val tr = Tr()
        tr.block()
        children.add(tr)
    fun html(): String {
        val builder = StringBuilder()
        builder.append("<table>")
        for (it in children) {
            builder.append(it.html())
        builder.append("</table>")
        return builder.toString()
// 4. table 顶层函数
fun table(block: Table.() -> Unit): String {
    val table = Table()
    table.block()
    return table.html()
---- 说明 ------
        /** 调用 table 顶层函数,并传入 table 的扩展 lambda 函数 */
        val html = table {
            /** 当前 this 为 table, 调用 table.tr 函数传入 lambda 函数 */
                /** 当前 this 为 tr 对象, 调用 tr.td 函数传入 lambda 函数 */
                td { "Apple" } /** 调用 Tr.td */
                td { "Grape" } /** 调用 Tr.td */
                td { "Orange" } /** 调用 Tr.td */

android

Activity

AndroidManifest.xml 中添加 Activity 定义

        <activity
            android:name=".MainActivityX"
            android:label="@string/title_activity_xmain">
            <intent-filter>
                <!-- 定义启动 activity -->
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" /> 
            </intent-filter>
            <intent-filter>
                <!-- 定义一个自定义名称,可以使用该名称启动 -->
                <action android:name="xsoft.demo.xapp.action_start" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
            <intent-filter tools:ignore="AppLinkUrlError">
                <!-- 定义可以响应 view 请求。符合 https 的就支持, 也可以指定 host,port,path,mineType -->
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:scheme="https" />
            </intent-filter>
        </activity>
// 显式调用,指定类定义
val intent = Intent(this, MainActivity_BBS::class.java) // 初始化时,指定启动目标
intent.setClass(this, MainActivity_BBS::class.java) // 指定启动目标, 据说内部调用 setComponent
intent.component = ComponentName(this, MainActivity_BBS::class.java); // setComponent, new 出 ComponentName 进行设置 
intent.setClassName(this, "xsoft.demo.myapplication.MainActivity_BBS") // 使用 string ,但是我没有设置成功
// 隐式调用,指定名称,在 AndroidManifest.xml 中的 intent-filter | action 中定义
val intent = Intent("xsoft.demo.xapp.action_start")
// 打开一个 url 
val intent = Intent(Intent.ACTION_VIEW)
intent.data = android.net.Uri.parse(url) // 动作对应的数据
intent.putExtra() // 传递附加数据
intent.putxxxx() // 传递附加数据
// 进行拨号 
val intent = Intent(Intent.ACTION_DIAL)
intent.data = Uri.parse("tel:{$phone}")
// 开始调用 
this.startActivity(intent)

新启动方式

// 原来的方式看起来过期了,
val result  = ActivityResultContracts.StartActivityForResult()
var resultLauncher = registerForActivityResult(result) { it ->
if (it.resultCode == Activity.RESULT_OK) {
    // There are no request codes
    val data: Intent? = it.data
val intent = Intent(this, AppBar::class.java)
resultLauncher.launch(intent)
this.finish() // 关闭前调用 this.setResult  
this.finishActivity()  // 给 startActivityForResult 返回一个结果 
// 使用 intent.putxxxx 附加参数 
val intent = Intent(this, MainActivityX2::class.java)
intent.putExtra("data", "1234567890") 
// 使用 intent.putExtras 批量附加参数 
val bundle = Bundle()
bundle.putString("key1", "test")
bundle.putInt("key2", 32)
intent.putExtras(bundle)
// 传递序列化对象
/// 存入
val bundle = Bundle()
bundle.putSerializable("person_json", Person())
intent.putExtras(bundle)
/// 取出
intent.getSerializableExtra("person_json") as Person
this.startActivity(intent) // 不需要等待返回值
this.startActivityForResult(intent, 123) // 需要等待返回值
// 等待打开窗口的回调返回值
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?){
    val data = data?.getStringExtra("data_return")
    Log.i("activity 返回", "回调 Id: {$requestCode}, activityId: {$resultCode}, data: {$data}")
// 打开的窗口中获取传入值
val value = this.intent.getStringExtra("data")
// 关闭打开的窗口,并返回值, 前一个窗口的 onActivityResult 会被回调
val intent = Intent()
intent.putExtra("data_return", "0987654321")
this.setResult(RESULT_OK, intent) // 定义要向前一页返回数据
this.finish()
<!-- res.menu 目录中定义 xml文件 -->
<menu ...>
    <item android:id="@+id/action_Add" android:title="Add" />
    <item android:id="@+id/action_Delete" android:title="Delete" />
    <item android:id="@+id/action_GotoBaidu" android:title="baidu" />
    <item android:id="@+id/action_Dial" android:title="dial" />
    <item android:id="@+id/action_Close" android:title="Close" />
</menu>
    // 初始化按钮 
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        this.menuInflater.inflate(R.menu.menu_main, menu)
        return true
    // 定义菜单处理事件 
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            R.id.action_Add -> this.showMessage("add message")
            R.id.action_GotoBaidu -> this.gotoUrl("https://www.baidu.com")
            R.id.action_Dial -> this.dial("12345678901")
        return true

上下文菜单

    override fun onResume() {
        super.onResume()
         // 在控件上注册菜单 
        this.registerForContextMenu(this.phoneText)
    override fun onPause() {
        super.onPause()
         // 在控件上注销菜单 
        this.unregisterForContextMenu(this.phoneText) 
    // 选择上下文菜单时
    override fun onContextItemSelected(item: MenuItem): Boolean {
        return super.onContextItemSelected(item)
        return true
stateDiagram-v2
[*] --> onCreate
onCreate --> onStart
onStart --> onResume
onResume --> onPause
onRestart --> onStart
onPause --> onStop
onPause --> onRestart
onStop --> onDestroy
onDestroy --> onCreate
onDestroy --> [*]
    // 回调保存一些数据
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putString("dataKey", "value")
    // 启动时读回数据,并设置入界面中
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val value = savedInstanceState?.getString("dataKey")

AndroidManifest.xml 的使用 android:launchMode 指定

standard: 默认,每启动一次就创建一个实例,并保存至栈顶

singleTop: 如果栈顶有实例,则不创建,否则创建实例,并保存至栈顶

singleTask: 如果栈中有实例,直接将其顶部 activity 出栈,使用现有实例

singleInstance:标记为此的 activity 会单独使用一个调用栈。此 activity 一般与其它应用共用,所以要单独使用一个栈

常用启动及关闭方式

// 写一个单例,收集所有的 activity ,并进行管理 object ActivityCollector{ private val activities = ArrayList<Activity>() fun addActivity(activity: Activity){ this.activities.add(activity) fun removeActivity(activity:Activity){ this.activities.remove(activity) fun finishAll(){ this.activities.filter { !it.isFinishing }.forEach { it.finish() } android.os.Process.killProcess(android.os.Process.myPid()) // 写一个 activity 父类,在启动时记录自身 class BaseActivity : AppCompatActivity(){ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.i("log", javaClass.simpleName) ActivityCollector.addActivity(this) override fun onDestroy() { super.onDestroy() ActivityCollector.removeActivity(this) // 关闭所有 activity , fun Close(){ ActivityCollector.finishAll() companion object{ // 一种常用的启动方式, 约定一些参数,以方式直接调用 fun actionStart(context: Context, param1: String, param2: String){ val intent = Intent(context, BaseActivity::class.java) intent.putExtra("param1", param1) intent.putExtra("param2", param2) context.startActivity(intent)

Application

全局唯一实例。在 AndroidManifest.xml 中 application 节 name 属性 可以指定启动 application

<application android:name=".XApplication" />
// 根据需要设置一个全局对象供其它类中访问
class XApplication : Application() {
    // 应用启动时
    override fun onCreate(){
        super.onCreate()
    // 低内存时
    override fun onLowMemory() {
        super.onLowMemory()
    // 配置变更时,比如竖屏、横屏
    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
// 访问,使用下面两个方法进行访问。是同一个对象,但在不同的场景下。比如 Activity、Service、BroadcastReceiver 中使用不同方式获取
val application = this.application
val applicationContext = this.applicationContext
LinearLayout

最常用布局之一

<LinearLayout android:orientation="vertical|horizontal"> </LinearLayout> android:gravity="center" # 控件内的文字排版 android:layout_gravity="center_horizontal" # 该控件在父控件中的排版 android:layout_weight="3" # 该控件在父控件中分配长(宽)的权重。 一般设置 android:layout_width/layout_height="0dp"
RelativeLayout
<!-- 相对布局 -->
<RelativeLayout
       android:layout_width="match_parent"
       android:layout_height="match_parent" >
       <TextView
           android:id="@+id/CenterButton"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_centerInParent="true"  // 相对于父控件
           android:text="TextView" />
       <TextView
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_above="@+id/CenterButton" // 相对于指定控件的位置
           android:layout_toLeftOf="@+id/CenterButton" // 相对于指定控件的位置
           android:text="TextView" />
   </RelativeLayout> 
FrameLayout

很简单的布局,仅使用 android:layout_gravity 指定位置 。如果不是居中的话,可以指定一下 layout_marginXXX 来定义一下边距

   <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center" // 定义布局
            android:text="文本" />
    </FrameLayout>
ConstraintLayout

现在主推布局

Android新特性介绍,ConstraintLayout完全解析_郭霖的专栏-CSDN博客_constraintlayout

ConstraintLayout学起来! - 简书 (jianshu.com)

Android之约束布局ConstraintLayout详解 - 华为云 (huaweicloud.com)

使用 ConstraintLayout 构建自适应界面 | Android 开发者 | Android Developers (google.cn)

// 指定靠近约束方向: layout_constraintA_toBOf 必须指定约束,不然其它属性不起作用
app:layout_constraintTop_toTopOf // 指定 top 对应于另外一个控件的 top, 比如对应于父框架,则使用 parent
// 居中: 如果指定 top_toxx 与 bottom_toxx 则为居中
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
// 充满: 如果设置高度或是宽度为 0dp,
// 官方不推荐在ConstraintLayout中使用match_parent,可以设置 0dp (MATCH_CONSTRAINT) 配合约束代替match_parent,举个例子:
// 高充满至 top_toxxx 与 bottom_toxxx 之间的距离。宽充满至 left_toxxx 与 right_toxxx 之间的距离
比如 layout_height=0dp, 则宽度充满 top_toxx 与 bottom_toxx
// 宽高按比例显示, 将高或是宽设置为**父容器**高或宽的百分比, 注意是父容器
app:layout_constraintWidth_percent="0.1"
app:layout_constraintHeight_percent="0.5"
// 边距: layout_marginxxx
需要设置 对应方向 constraintxxx 则对应的 layout_marginxxx 才生效
比如 layout_constraintTop_toxxx layout_marginTop 生效
// layout_goneMarginX
控件不显示也会计算边距
// 左右边距比例
layout_constraintVertical_bias="0.2" 上下边距的比例
layout_constraintHorizontal_bias="0.8" 左右边距的比例
// 宽高比例
app:layout_constraintDimensionRatio="3:4" // 宽高比较, 宽或是高有一个必须为 0dp
// 位置位置均分,第一个控件设置 xxx_chainStyle, 后面的所有 xx_toxx 控件按该值进行分配位置
app:layout_constraintHorizontal_chainStyle="spread_inside" // 平均分配横向或是纵向空间,两侧控件贴边
app:layout_constraintVertical_chainStyle="spread" // 平均分配横向或是纵向空间,两侧控件不贴边 
app:layout_constraintVertical_chainStyle="packed" // 所有控件配横向或是纵向空间居中
// Guideline: 辅助线,不显示,但是其它控件可以设置与该值的相对关系
// 按文字对齐 
app:layout_constraintBaseline_toBaselineOf="@id/textView3"
// 根据角度值进行计算位置 
app:layout_constraintCircle="@+id/textView3" // 相对于哪个控件进行计算
app:layout_constraintCircleRadius="100dp" // 与相应控件的距离
app:layout_constraintCircleAngle="165"  // 与相应控件对应的角度值
// Guideline 辅助线,不显示,使用锚定元素
<androidx.constraintlayout.widget.Guideline
    android:id="@+id/guideline2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical|horizontal" // 设置方向 
    app:layout_constraintGuide_percent="0.3" // 设置左右分隔比例以定位 Guideline
    app:layout_constraintGuide_begin="100dp" //与parent左边界或上边界(根据GuideLine的方向)的距离
    app:layout_constraintGuide_end="100dp" ////与parent右边界或下边界(根据GuideLine的方向)的距离
// Barriers 辅助定位
// 与 Guideline 相似,但不是一根线,而是针对一组控件的最突出的位置指定一条线。
// 比如 Setting 中 label 部分有长有短,而且不同语言下 label 也长短不统一。
// 将 label 都加入到 Barriers 中,并指定方向为 end ,这样 label 最长的那一头为Barriers的基准线
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2017/1017/8601.html
<androidx.constraintlayout.widget.Barrier
        android:id="@+id/barrier7"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:barrierDirection="right" // 以子控件的最右侧为基准线
        app:constraint_referenced_ids="labelText1,labelText2" // 对应的控件 
// 点位符 PlaceHolder
<android.support.constraint.Placeholder
    app:content="@id/btn1" // 指定由哪个控件占用该位置 
    android:id="@+id/pl" 
    android:layout_width="50dp" // 与普通控件一样设置宽
    android:layout_height="50dp" // 与普通控件一样设置高
    app:layout_constraintStart_toStartOf="parent" // 与普通控件一样设置位置
    app:layout_constraintTop_toTopOf="parent" // 与普通控件一样设置位置
// 代码设置
placeHolder.setContentId(R.id.btn1)
// 分组控件, 将一批控件设置一组,统一设置可见性
<androidx.constraintlayout.widget.Group
              android:id="@+id/group"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:visibility="visible"
              app:constraint_referenced_ids="button1,button2" />
// 使用ConstrainsSet 运行动态修改布局
https://www.jianshu.com/p/a95daeaa02d7

自定义布局

classDiagram
View <|-- TextView
View <|-- ViewGroup
View <|-- ImageView
TextView <|-- EditText
TextView <|-- Button
ViewGroup <|-- LinearLayout
ViewGroup <|-- RelativeLayout
ViewGroup <|-- 其它布局
创建布局控件
<!-- res.layout 目录中中创建布局文件类型为 LinearLayout -->
<!-- 引用, 如果要自定义事件,请看下一节 -->
<include layout="@layout/title" />
需要自定义处理函数
// 定义继承对象, 
class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
    init {
        // 与布局文件绑定
        LayoutInflater.from(context).inflate(R.layout.title, this)
        // 定义事件
        this.findViewById<Button>(R.id.title_back_button)?.setOnClickListener {
            val activity = context as Activity
            activity?.finish()
// 引用布局文件,需要使用完整路径 
<xsoft.demo.xapp.TitleLayout android:layout_width="match_parent" android:layout_height="wrap_content" />

列表基类为 AdapterView 继承于 ViewGroup, 下面扩展出了 ListView, Spinner 之类的, 为视图提示渲染。

Adapter 为 AdapterView 提供了数据及数据项呈现方式 。一般常用基类为 BaseAdapter

​ ArrayAdapter: 最常用的 List 集合, 参数中指定数组,布局文件,布局文件中的控件编号,自动将数组中的每一项创建布局并填充对应的控件内容,只有填充布局中的一个控件

​ SimpleAdapter: 可以将多组 List 组合成多个列表项,参数中指定n 个等长的数组及布局文件,数组对应内容及内容对应的控件编号,相比于 ArrayAdapter 可以自动填充布局中的多个控件

​ SimpleCursorAdapter: 与 SimpleAdapter 类似,应该是提供了数据库操作相关的功能

​ BaseAdapter: 用来扩展定制,从中他创建布局并填充布局,新版本中扩展出带 ViewHolder 的类以方便使用

简单 ListView

使用字符串数组及ArrayAdapter

        // 生成一些模拟数据
        val names = (0..30).map { faker.animal.unique.name() } // 生成唯一值,但是可能会因为为了生成唯一值重复次数太多而失败
        // 使用 ArrayAdapter, 指定每一项定义 layout 文件, layout 文件中填写文本的控件编号 , 指定每一项的布局及对应数组
        val arrayAdapter = ArrayAdapter(this, R.layout.listview_item, R.id.TextView, names)
        // 设置 listview 的 adapter
        val listView = this.findViewById<ListView>(R.id.ListView)
        listView.adapter = arrayAdapter
简单 ListView 另一个例子
1. 定义界面 
    <ListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/ListView" />
2. 生成一批模拟数据
    private val mockData: Array<String> by lazy { CreateMockData() }
    fun CreateMockData(): Array<String> {
        val faker = Faker()
        val ls = mutableListOf<String>()
        (1..100).asSequence().forEach { _ -> ls.add(faker.food.fruits()) }
        return ls.toTypedArray()
3. listview 绑定数据的 ArrayAdapter
// android.R.layout.simple_list_item_1 填充方式 
        val adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, this.mockData)
        val view = this.findViewById<ListView>(R.id.ListView)
        view?.adapter = adapter
自定义 ListView 项
1.0 创建每一项的页面资源文件, 比如 fruit_item.xml
1.1 在 activity 中引用  ListView 
2. 创建每一项对应的数据在 
class Fruit(val name: String, val title: String)
3. 创建 listview 绑定适配器 
class FruitAdapter(private val activity: Activity, private val resourceId: Int, data: List<Fruit>) :
    ArrayAdapter<Fruit>(activity, resourceId, data) {
    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        // 返回对应数据项
        val item = this.getItem(position)
        // 返回每一项对应的 view ,并设置其中的控件, convertView 为每一项的视图。在上下滚动的时候,可以重用
        // 可以缓存 findViewById 的结果至 view.tag 中
        val view = convertView ?: LayoutInflater.from(this.activity).inflate(resourceId, parent, false)
        val nameTextView = view.findViewById<TextView>(R.id.Fruit_Item_Name)
        nameTextView?.text = item?.name
        val titleTextView = view.findViewById<TextView>(R.id.Fruit_Item_Title)
        titleTextView?.text = item?.title
        return view
4. 将 listview 页面资源文件与适配器进行绑定 
val adapter = FruitAdapter(this, R.layout.fruit_item, this.mockData) // 将每一项的资源与适配器进行绑定 
val view = this.findViewById<ListView>(R.id.ListView) // 返回 listview, 并进行绑定 
view?.adapter = adapter // 绑定
view?.setOnItemClickListener {  parent, view, position, id -> Log.d("--> listviewclick", "我被点击了 ${position}") } // 点击
SimpleAdapter

使用 list 准备一个复杂数据进行填充

        val maxLen = 100 // 准备生成多少数据
        val nameArray = this.getNameArray(maxLen) // 生成一个名称数组 Array<String>
        val imgIdArray = this.getImgIdArray(maxLen) // 生成一个图像id 数组 Array<Int> resources.getIdentifier
        val listItems = (1..maxLen).map { it -> // 生成一个 HashMap 的数组
            val dict = HashMap<String, Any>()
            dict["name"] = nameArray[it]
            dict["img"] = imgIdArray[it]
        // 生成一个 SimpleAdapter, 指定数据数组,数组必须为 HashMap 格式,每个 item 对应一条数据
        // 指定数据显示的布局
        // 指定数据数组中 hashmap 中需要显示的 key 数组
        // 与 key 顺序相同的顺序指定布局中的控件的编号
        // 程序会自动将每个数组中的值填写入控件
        val adapter = SimpleAdapter(
            this,
            listItems,
            R.layout.simple_adapter_item,
            arrayOf("name", "img"),
            intArrayOf(R.id.TextView, R.id.ImageView)
        // 取出 list 视图,并设置入  adapter
        val listView = this.findViewById<ListView>(R.id.ListView)
        listView.adapter = adapter
BaseAdapter

可以从这里继承实现接口,然后设置为 list , 可以见 下面 RecyclerView 的例子,RecyclerView.Adapter 应该实现了 BaseAdapter 而且还加入了 ViewHolder 缓存以实现更高级内容

RecyclerView

应该为 GridView 的升级版本

1.0 准备每一项的资源页面文件 ,比如 fruit_item.xml
1.1 在 activity 中引用 android.support.v7.widget.RecyclerView
2. 准备每一项的数据类
class Fruit(val name: String, val title: String)
3. 创建一个适配器项
class FruitAdapter(val fruitList: List<Fruit>) : RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
    // 每一项的 view 缓存项的定义 
    inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val nameTextView: TextView? = view.findViewById<TextView>(R.id.Fruit_Item_Name)
        val titleTextView: TextView? = view.findViewById<TextView>(R.id.Fruit_Item_Title)
    // 创建每一项的缓存
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
        val holder = ViewHolder(view)
        holder.nameTextView?.setOnClickListener { /* 点击事件处理 */ }
        return holder
    // 填充每一项的页面
    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val fruit = fruitList[position]
        holder.nameTextView?.text = fruit.name
        holder.titleTextView?.text = fruit.title
    override fun getItemCount(): Int {
        return this.fruitList.size
4. 创建并设置 RecyclerView 的布局对象及数据适配器 
val view = this.findViewById<RecyclerView>(R.id.Fruit_ListView)
view.adapter = FruitAdapter(this.mockData) // 绑定数据项
view.layoutManager = LinearLayoutManager(this) // 布局管理器,根据需要可以换成不同的对象
5. 可以换用其它布局管理器
// 横向滚动
val manager = LinearLayoutManager(this)
manager.orientation = LinearLayoutManager.HORIZONTAL
view.layoutManager = manager
// 瀑布流滚动
view.layoutManager = StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL)
// 创建 adapter, 并设置视图的 adapter
val adapter = this.createExpandableListViewAdapter()
val view = this.findViewById<ExpandableListView>(R.id.ExpandableListView)
view.setAdapter(adapter)
    // 父项
    class Item(val name: String, val items: Array<SubItem>)
    // 子项
    class SubItem(val name: String, val imgId: Int)
    /* 创建收缩控件 ExpandableListAdapter*/
    private fun createExpandableListViewAdapter(): ExpandableListAdapter {
        // 保存数据项
        val items: Array<Item> = this.LoadItems()
        // 从 BaseExpandableListAdapter 直接继承
        val adapter = object : BaseExpandableListAdapter() {
            // 返回项目总数
            override fun getGroupCount(): Int {
                return items.size
            // 指定父类的项目总数
            override fun getChildrenCount(groupPosition: Int): Int {
                return getGroup(groupPosition).items.size
            // 返回父项
            override fun getGroup(groupPosition: Int): Item {
                return items[groupPosition]
            // 返回父项中指定的子项
            override fun getChild(groupPosition: Int, childPosition: Int): SubItem {
                return getGroup(groupPosition).items[childPosition]
            // 返回父编号
            override fun getGroupId(groupPosition: Int): Long {
                return groupPosition.toLong()
            // 返回子项编号
            override fun getChildId(groupPosition: Int, childPosition: Int): Long {
                return childPosition.toLong()
            // 不清楚
            override fun hasStableIds(): Boolean {
                return true
            // 返回父项对应的视图
            override fun getGroupView(
                groupPosition: Int,
                isExpanded: Boolean,
                convertView: View?,
                parent: ViewGroup?
            ): View? {
                var itemView = convertView
                if (itemView == null) {
                    // 创建出视图
                    itemView = LayoutInflater.from(parent?.context)
                        .inflate(R.layout.activity_main_expandable_list_item_view, parent, false)
                // 编辑视图, 显示文字
                val item = this.getGroup(groupPosition)
                val textView = itemView?.findViewById<TextView>(R.id.TextView)
                textView?.textSize = 24f
                textView?.text = item.name
                // 这里显示 +- 符,收缩展开
                val flag = itemView?.findViewById<TextView>(R.id.TextView_Flag)
                flag?.text = if (isExpanded) "+" else "-"
                flag?.visibility = View.VISIBLE
                return itemView
            // 返回子项对应的视图
            override fun getChildView(
                groupPosition: Int,
                childPosition: Int,
                isLastChild: Boolean,
                convertView: View?,
                parent: ViewGroup?
            ): View? {
                var itemView = convertView
                if (itemView == null) {
                    itemView = LayoutInflater.from(parent?.context)
                        .inflate(R.layout.activity_main_expandable_list_item_view, parent, false)
                val item = this.getChild(groupPosition, childPosition)
                val textView = itemView?.findViewById<TextView>(R.id.TextView)
                textView?.text = item.name
                return itemView
            override fun isChildSelectable(p0: Int, p1: Int): Boolean {
                return true
        return adapter
AdapterViewFlipper

图片轮换显示

    <AdapterViewFlipper
        android:id="@+id/ViewFlipper"
        android:animateFirstView="true"
        android:loopViews="true"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </AdapterViewFlipper>
        // 設置 flipper
        val flipper = this.findViewById<AdapterViewFlipper>(R.id.ViewFlipper)
        val adapter = this.createAdapter()
        flipper.adapter = adapter // 显示
    /** 顯示下一張圖片 */
    fun showNextImage(){
        val flipper = this.findViewById<AdapterViewFlipper>(R.id.ViewFlipper)
        flipper.showNext()
    private fun createAdapter(): BaseAdapter {
        return object : BaseAdapter() {
            // 载入图片资源列表
            val imgIds: Array<Int> = this@MainActivity_Flipper.loadImageIds()
            override fun getCount(): Int = this.imgIds.size
            override fun getItem(idx: Int): Int  = this.imgIds[idx]
            override fun getItemId(idx: Int): Long = idx.toLong()
            override fun getView(idx: Int, view: View?, parent: ViewGroup?): View {
                // 如果 view 已经存在了,直接重用,设置一下内容即可
                var imgView = view as? ImageView
                if(imgView == null ){
                    imgView = ImageView(this@MainActivity_Flipper)
                    imgView.scaleType = ImageView.ScaleType.CENTER_INSIDE
                    imgView.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
                    // 点击图片时显示下一张图片
                    imgView.setOnClickListener { this@MainActivity_Flipper.showNextImage() }
                // 设置图片内容
                imgView.setImageResource(getItem(idx))
                return imgView
StackView

图片列表堆叠显示,与 AdapterViewFlipper 一样使用

微调框 (spinner)

其实就是下拉框

<Spinner android:id="@+id/spinner_cal" />
val adapter = null; // 生成一个适配器
val spinner = this.findViewById<Spinner>(R.id.spinner_cal)
spinner.prompt = "请选择" // 文字提示
spinner.adapter = adapter // 设置入适配器
spinner.setSelection(0) // 设置默认选择项
/** 设置选择后的动作 */
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
    override fun onNothingSelected(parent: AdapterView<*>?) { }
    override fun onItemSelected( parent: AdapterView<*>?, view: View?, position: Int, id: Long ) {
                showNote("当前选择第 $position 项")

纯文字格式的微调框

// 数据源。可以使用资源文件或数据库:https://developer.android.com/guide/topics/ui/controls/spinner
val strs = arrayOf("水星", "金星", "地球", "火星", "木星", "土星")
// 这里使用字符串 adapter
val adapter = android.widget.ArrayAdapter(this, R.layout.support_simple_spinner_dropdown_item, strs)
        adapter.setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item)
return adapter

自定义视图, 假设定义一个带 imageview 与 textview 的视图名为 spinner_item.xml

1. 定义一个 ArrayAdapter
class IconArrayAdapter(
        private val activity: Activity,
        private val resourceId: Int,
        objects: Array<IconItem>
        android.widget.ArrayAdapter<IconItem>(activity, resourceId, objects) {
        // 定义下拉时的视图, 与默认视图一样处理
        override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
            return getView(position, convertView, parent);
        // 定义默认视图
        override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
            val view = // 可以尝试缓存 View
                convertView ?: LayoutInflater.from(this.activity).inflate(resourceId, parent, false)
            val item = this.getItem(position) // 返回对应数据项
            item?.let {
                val imgView = view.findViewById<ImageView>(R.id.spinner_item_image_view)
                imgView?.setImageResource(item.icon) // 设置图标
                val txtView = view.findViewById<TextView>(R.id.spinner_item_Text_view)
                txtView?.text = item.text // 设置文字 
            return view
2. 定义 IconArrayAdapter 并设置 spinner
    private fun setupIconArrayAdapterSpinner(spinner: Spinner): SpinnerAdapter {
        val strs = arrayOf(
            IconItem("星球1", R.drawable.star_1_24px),
            IconItem("星球2", R.drawable.star_2_24px),
            IconItem("星球3", R.drawable.star_3_24px),
            IconItem("星球4", R.drawable.star_4_24px),
            IconItem("星球5", R.drawable.star_5_24px),
            IconItem("星球6", R.drawable.star_5_24px),
            IconItem("星球7", R.drawable.star_7_24px)
        // 这里使用字符串 adapter
        val adapter = IconArrayAdapter(this, R.layout.spinner_item, strs)
        return adapter
ProgressBar
    <ProgressBar
        android:id="@+id/ProgressBar"
        android:layout_width="match_parent"
        android:layout_height="25dp"
        android:progress="50"
        android:max="100"
        style="@android:style/Widget.ProgressBar.Large"
                android:indeterminate="true"/>
SeekBar
<SeekBar
    android:layout_width="match_parent"
    android:layout_height="42dp"></SeekBar>
RatingBar
<RatingBar
    android:layout_width="match_parent"
    android:layout_height="wrap_content"></RatingBar>

ViewSwitcher

多个 view 进行切换,可以设置切换动画

  • 调用 ViewSwitcher的 setFactory 返回内嵌的 view
  • 翻页前设置 ViewSwitcher.nextView ( setFactory 中返回 ) 的相关参数 , 比如这里的 view 是 GridView, 那么可以设置 adapter
  • ViewSwitcher.showPrevious 或 ViewSwitcher.showNext 进行翻页
  • 创建一个类似于 launcher 的程序,设置一批应用图标,分配在多个 GridView 中并支持翻页

        /** 表示一项应用,一页中有多项  */
        class CellItem(val imgId: Int, val text: String)
        // 一些翻页数据
        private var pageIdx: Int = -1 // 当前对应的页码
        lateinit var items: Array<CellItem> = this.createItems() // 用来模拟的项数据, 分配至多页的 viewSwitcher 中
        // 设置 ViewSwitcher 的 setFactory 以返回多页中使用的 view, view 可以动态 new 出并设置,也可以从布局文件中载入
        val switcher = this.findViewById<ViewSwitcher>(R.id.ViewSwitcher)
        switcher.setFactory { //  ViewSwitcher 有多页,所以要使用 setFactory 返回多页布局
                // 这里 new 一个 GridView, 并设置为 3 列
                with(GridView(this)){
                    numColumns = 3
                * 也可以从布局文件载入一个视图
                val inflater = LayoutInflater.from(this@MainActivity_ViewSwitcer)
                inflater.inflate(R.layout.activity_main_bbs, null) */
            // 设置切换下一页或是下一页
            this.findViewById<Button>(R.id.nextPageButton).setOnClickListener { this.next(switcher) } // 切换下一页
            this.findViewById<Button>(R.id.prevPageButton).setOnClickListener { this.prev(switcher) } // 切换上一页
            this.next(switcher) // 进入第一页
        /** 下一页, 为了方便,统一在 gotoPageView 中调用核心代码  */
        private fun next(switcher: ViewSwitcher) =this.gotoPageView(pageIdx + 1, switcher) { it.showNext() }
        /** 上一页, 为了方便,统一在 gotoPageView 中调用核心代码 */
        private fun prev(switcher: ViewSwitcher) =this.gotoPageView(pageIdx - 1, switcher) { it.showPrevious() }
        /** 跳转至指定的页码, next 为下一部操作 */
        private fun gotoPageView(page: Int,switcher: ViewSwitcher,next: (switcher: ViewSwitcher) -> Unit ) {
             val numEachPage: Int = 18 // 每一页几项
            // 调整下一页正确的页码
            pageIdx = if (page * numEachPage <= items.size) page else page - 1
            // 设置单页 view 中的相关参数,比如设置 adapter
            val gridView = switcher.nextView as GridView
            gridView.adapter = createAdapter(pageIdx, numEachPage, items) // 这里创建一个 BaseAdapter, 传入当前的页码及每页几项  
            next(switcher) // 通过回调调用  showNext 或 showPrevious
        /** 为每一页 GridView 对应的 adapter, 并指定该页页码, 其它与标准操作一样 */
        private fun createAdapter(baseIdx: Int, numEachPage: Int, items: Array<CellItem>): BaseAdapter {
            return object : BaseAdapter() {
                override fun getCount(): Int {
                    val remain = items.size - baseIdx * numEachPage
                    return if (remain > numEachPage) numEachPage else remain
                override fun getItem(idx: Int): CellItem = items[baseIdx * numEachPage + idx]
                override fun getItemId(idx: Int): Long = idx.toLong()
                override fun getView(idx: Int, view: View?, group: ViewGroup?): View {
                    val item = this.getItem(idx)
                    val outView = view ?: LayoutInflater.from(this@MainActivity_ViewSwitcer)
                        .inflate(R.layout.activity_main_view_switcer_item, group, false)
                    outView.findViewById<TextView>(R.id.TextView).text = item.text // "我在第 $baseIdx 页"
                    outView.findViewById<ImageView>(R.id.ImageView).setImageResource(item.imgId)
                    return outView
    
    ImageSwitcher

    图片切换,继承自 ViewSwitcher, setFactory 必须返回 ImageView

    val switcher = this.findViewById<ImageSwitcher>(R.id.ImageSwitcher)
    switcher.setFactory { ImageView(this) } // 设置 setFactory 返回 ImageView
    var imgIdx = 0
    val imgIds: Array<Int> = this.loadImageIds() // 载入要显示的图片资源 
    this.findViewById<Button>(R.id.nextPageButton)
        .setOnClickListener { switcher.setImageResource(this.loadImageId(imgIds, imgIdx++)) } // 通过 loadImageId 载入想要显示的图片
    /** 设置需要显示的图片 */
    private fun loadImageId(imgIds: Array<Int>, imgIdx: Int): Int {
            var idx = abs(imgIdx % imgIds.size)
            return imgIds[idx]
    // 设置图片切换时的动画效果
    android:inAnimation="@android:anim/fade_in"
    android:outAnimation="@android:anim/fade_out"
    
    TextSwitcher

    文件切换显示,与 ImageSwitcher 一样处理

    ViewFlipper

    相当于 tab ,加入一批 view, 在 ViewFlipper 中调用 showNext 或是 showPrevious 进行切换,或是 startFlipping 进行自动切换

      <ViewFlipper
            android:outAnimation="@android:anim/slide_out_right"
            android:inAnimation="@android:anim/slide_in_left">
            <ImageView />
            <TextView />
            <ImageView />
            <TextView />
            <ImageView />
        </ViewFlipper>
    
    val flipper = this.findViewById<ViewFlipper>(R.id.ViewFlipper)
    flipper.setOnClickListener { flipper.showNext() } // 进行切换 
    

    EditText

     /* 在 AndroidManifest.xml 中设置, stateVisible 为显示,默认为隐藏
                * <activity android:windowSoftInputMode="stateVisible"/>  */
                // 使用输入法管理器
                val manager = this.getSystemService(Context.INPUT_METHOD_SERVICE)
                if(manager is InputMethodManager){
                    if(manager.isActive(editText)){ // isActive 一直是 true, 有人用控件高度变化来判断
                        // 关闭输入法
                        manager.hideSoftInputFromWindow(editText.windowToken, 0)
                        val r = Runnable {
                            // 打开输入法 
                            editText.requestFocus()
                            manager.showSoftInput(editText, 0)
                        // 设置一个延时执行方法
                        Handler(Looper.getMainLooper()).postDelayed(r, 3000)
    

    AutoCompleteTextView

    自动提示, 还有 multiAutoCompleteTextView 可以进行多个项提示

    <AutoCompleteTextView
        android:completionHint="请输入" // 输入提示
        android:completionThreshold="2" // 输入多少个字母后进行提示
    val textEdit = this.findViewById<AutoCompleteTextView>(R.id.Demo_AutoCompleteTextView)
    val arrayAdapter =
    ArrayAdapter(this, android.R.layout.simple_list_item_1, names)
    textEdit.setAdapter(arrayAdapter)
            val items = arrayOf("数据 1", "数据 2", "数据 3", "数据 4", "数据 5")
            val arrayAdapter = ArrayAdapter(this, R.layout.activity_simple_item, R.id.TextView, items)
            val autoCompleteTextView = this.findViewById<AutoCompleteTextView>(R.id.AutoCompleteTextView)
            autoCompleteTextView.setAdapter(arrayAdapter)
            // 可以进行多项输入进提示,每一项字符使用 setTokenizer 进行设置
            val multiAutoCompleteTextView = this.findViewById<MultiAutoCompleteTextView>(R.id.MultiAutoCompleteTextView)
            multiAutoCompleteTextView.setAdapter(arrayAdapter)
            multiAutoCompleteTextView.setTokenizer(MultiAutoCompleteTextView.CommaTokenizer())
    

    SearchView

    NumberPicker

    数值选择控件

    ScrollView/HorizontalScrollView

    为控件增加滚动功能

    日期时间控件

    <!-- 选择日历 -->
    <CalendarView />
    <!-- 选择日期 -->
    <DatePicker />
    <!-- 选择时间 -->
    <TimePicker />
    
    DatePicker

    显示一个日期列表进行选择

    // 直接显示在界面中的控件
    val datePicker = this.findViewById<DatePicker>(R.id.Test_DatePicker)
    datePicker.setOnClickListener { // 点击后进行触发 
        this.showLog("当前选择: ${datePicker.year} - ${datePicker.month} - ${datePicker.dayOfMonth}")
    
    // 显示一个窗口进行日期选择
    // 获取当前时间 /*val time = LocalDateTime.now() // 新版本中才有*/
            val calendar = Calendar.getInstance() // current time
            val year = calendar.get(Calendar.YEAR)
            val month = calendar.get(Calendar.MONTH)
            val day = calendar.get(Calendar.DAY_OF_MONTH)
            // 设置选择时间后的回调接口
            val setDater = object: DatePickerDialog.OnDateSetListener  {
                override fun onDateSet(picker: DatePicker?, year: Int, month: Int, day: Int) {
                    this@MainActivity_TimeCtrl.showDate(year, month, day)
                    this@MainActivity_TimeCtrl.showLog("当前选择: $year - $month - $day ")
            // 显示日期对话框, 设置回调接口,并指定日期中显示的时间
            val datePickerDialog = DatePickerDialog(this, setDater, year, month, day)
            datePickerDialog.show()
    

    动态读取资源

    val name = "face$idx" // 资源文件名, 不带后缀 
    val deftype = "drawable" // 资源的类型
    val resourceId = this.resources.getIdentifier(name, deftype, this.packageName);
    
    工具栏菜单
    // 在工具栏菜单中显示 
        // 1. 重载 onCreateOptionsMenu 设置工具栏菜单内容 
       override fun onCreateOptionsMenu(menu: Menu?): Boolean {
           // 1.1. 这里载入菜单,看公共代码部分
            this.LoadMenuXml(menu)
            return super.onCreateOptionsMenu(menu)
        // 2. 选中菜单后重载 onOptionsItemSelected 处理
        override fun onOptionsItemSelected(item: MenuItem): Boolean {
            // 2.1 处理秆的菜单,这部分看公共公代码
            this.ShowSelectedMenu(item)
            return super.onOptionsItemSelected(item)
    
    上下文菜单
    // 常按某个菜单后弹出菜单 
    // 1. 调用函数 registerForContextMenu 设置该控件支持上下文菜单 
        val text = this.findViewById<View>(R.id.ContextMenuTextView)
        this.registerForContextMenu(text)
    // 2. 重载 onCreateContextMenu 设置上下文菜单 
        override fun onCreateContextMenu(
            menu: ContextMenu?,
            view: View?,
            menuInfo: ContextMenu.ContextMenuInfo?
            // 这里载入菜单,看公共代码部分
            this.LoadMenuXml(menu)
            super.onCreateContextMenu(menu, view, menuInfo)
    // 3. 重载 onContextItemSelected 处理选中菜单事件
        override fun onContextItemSelected(item: MenuItem): Boolean {
            // 处理选中菜单 ,这部分看公共公代码
            this.ShowSelectedMenu(item)
            return super.onContextItemSelected(item)
    
    // 代码动态弹出菜单(比如在点击按钮之类的时候)
    // 1. 设置某个动作,比如点击按钮时
    val button = this.findViewById<View>(R.id.PopupMenuButton)
            button.setOnClickListener {
                // 2. 创建弹出菜单对象
                val popupMenu = PopupMenu(this, button)
                popupMenu.let {
                    // 3. 弹出菜单载入菜单内容,这里看公共代码
                    LoadMenuXml(popupMenu.menu)
                    // 4. 设置点击菜单后如何动作
                    popupMenu.setOnMenuItemClickListener { it ->
                      // 5. 处理动作,这里看公共代码 
                      this.ShowSelectedMenu(it)
                    // 6. 弹出菜单 
                    popupMenu.show()
    
    一些公共代码
    // 载入菜单     
    private fun LoadMenuXml(menu: Menu?) {
            // 1. 使用 MenuInflater 载入菜单资源
            MenuInflater(this).inflate(R.menu.menu_main, menu)
            // 2. 代码动态载入菜单 
            val fontMenu = menu?.addSubMenu("菜单") // 2.1 动态插入子菜单 
            // 2.2 这里设置子菜单 
            fontMenu?.let {
                fontMenu.setIcon(R.drawable.lowercase_2_30px)
                fontMenu.setHeaderIcon(R.drawable.lowercase_2_30px)
                fontMenu.setHeaderTitle("选择字体大小")
                    // 动态添加子菜单的子菜单项, 分组id, 菜单id, 排序编号, 菜单名称
                    fontMenu.add(0, 0, 0, "子菜单1")
                    fontMenu.add(0, 1, 0, "子菜单2")
                    fontMenu.add(1, 2, 0, "子菜单3")
                    fontMenu.add(1, 3, 0, "子菜单4")
    

    AppBar

    AppBar 按钮

    在工具栏上显示可变图标

    <!-- 工具栏上显示公用控件 --> android:id="@+id/action_bar_click" app:showAsAction="always" app:actionViewClass="android.widget.SearchView" <!-- 加入公共控件, 这里是一个搜索按钮,点击会自动执行一些动作 --> android:showAsAction="always" android:actionViewClass="android.widget.SearchView" android:title="classView"></item> <!-- 工具栏上显示自定义控件 --> android:id="@+id/action_bar_botton" app:actionLayout="@layout/activity_main_menu" app:showAsAction="always" android:actionLayout="@layout/activity_main_menu" <!-- 使用指定布局,没有深入研究,以后再研究 --> android:showAsAction="always" android:title="classView"></item>
    AppBar Tab 页

    现在不推荐使用,但也挺方便的

    // 1. 启用 tab 页   
    this.supportActionBar?.let{ it ->
                it.navigationMode = ActionBar.NAVIGATION_MODE_TABS // 设置启动 tab 页
                it.addTab(it.newTab().setText("第一页").setTabListener(this)) // 添加 tab 页,并设置处理函数
                it.addTab(it.newTab().setText("第二页").setTabListener(this)) // 添加 tab 页,并设置处理函数
    // 要实现 ActionBar.TabListener ,响应 tab 页选择等动作
        override fun onTabSelected(tab: ActionBar.Tab?, ft: FragmentTransaction?) {
            showMessage("${tab?.text} 被选择")
        override fun onTabUnselected(tab: ActionBar.Tab?, ft: FragmentTransaction?) {
            showMessage("${tab?.text} 取消选择")
        override fun onTabReselected(tab: ActionBar.Tab?, ft: FragmentTransaction?) {
            showMessage("${tab?.text} 再次选择")
    
    CardView

    我把它当 wpf 的 board 来用

     <androidx.cardview.widget.CardView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="24dp"
            android:minWidth="240dp"
            app:cardCornerRadius="10dp"
            app:contentPaddingBottom="12dp"
            app:contentPaddingLeft="8dp"
            app:contentPaddingRight="8dp"
            app:contentPaddingTop="12dp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent">
            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:orientation="vertical">
                <androidx.cardview.widget.CardView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_margin="8dp"
                    app:cardCornerRadius="5dp" <!-- 圆角 -->
                    app:cardElevation="5dp" <!-- z 阴影 -->
                    <ImageView
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:src="@drawable/aratar_59"></ImageView>
                </androidx.cardview.widget.CardView>
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginLeft="12dp"
                    android:layout_marginTop="8dp"
                    android:text="我是一段测试"
                    android:textSize="18sp" />
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginLeft="12dp"
                    android:padding="5dp"
                    android:text="这里是小的任务" />
            </LinearLayout>
        </androidx.cardview.widget.CardView>
    
    // 从 view 继承 实现一些事件
    // 使用
            val root = this.findViewById<ViewGroup>(R.id.Root)
            val drawView = XDrawView(this)
            drawView.minimumWidth = root.width;
            drawView.minimumHeight = root.height;
            root.addView(drawView) // 将自定义控件加入到视图中
    // 按一下显示一个点
    class XDrawView(context: Context?) : View(context) {
        private val lastPoint = PointF()
        override fun onTouchEvent(event: MotionEvent?): Boolean {
            if (event != null) {
                lastPoint.x = event.x
                lastPoint.y = event.y
                this.invalidate()
            return super.onTouchEvent(event)
        override fun onFinishInflate() {
            this.paint.color = Color.RED
            super.onFinishInflate()
        private val paint = Paint()
        @SuppressLint("DrawAllocation")
        override fun onDraw(canvas: Canvas) {
            super.onDraw(canvas)
            canvas.drawCircle(this.lastPoint.x, this.lastPoint.y, 15F, this.paint)
    

    常用对话框

    Kotlin入门(20)几种常见的对话框 - aqi00 - 博客园 (cnblogs.com)

    Toast

    显示一小段提示,看起来不推荐了,推荐使用 Snackbars 或是 通知系统

    // 生成一些测试数据
                val items = arrayOf("RED", "GREEN", "YELLOW", "BLACK", "MAGENTA", "PINK")
                val checkeds = items.map { false }.toBooleanArray()
                val alertDialog = AlertDialog.Builder(this).run {
                    setTitle("我是标题")
                    setIcon(R.drawable.firealarm)
                    setPositiveButton("确定") { _, id -> showNote("setPositiveButton: $id") }
                    setNegativeButton("取消") { _, id -> showNote("setNegativeButton: $id") }
                    setNeutralButton("查看更多") { _, id -> showNote("setNeutralButton: $id") }
                    // 使用 view 设置标题
                    setCustomTitle()
                    // 设置文本内容
                    setMessage("我是消息内容")
                    // 显示列表进行选择
                    setItems(array) { _, idx -> showNote("setItems: $idx") }
                    // 设置当单选
                    setSingleChoiceItems(array, 0) { _, idx -> showNote("setSingleChoiceItems: $idx") }
                    // 进行多选, 每选择一次就回调一次
                    setMultiChoiceItems(
                        items,
                        checkeds // 设置初始时,哪些是选择的
                    ) { _, which, isChecked -> showNote("setMultiChoiceItems: $which -> $isChecked") }
                    // 使用 Adapter 初始化,这里使用 ArrayAdapter 为例子
                    setAdapter(
                        ArrayAdapter(
                            this@MainActivity_SearchView,
                            android.R.layout.simple_list_item_1,
                            items
                    ) { _, idx -> showNote("setAdapter: $idx ") }
                    // 使用一个布局填充内容,这里没有设置怎么取内容,以后再说
                    val view = layoutInflater.inflate(R.layout.activity_login, null)
                    setView(view)
                    create()
                alertDialog.show()
    

    见 spinner

    因为是模式窗口,不推荐使用. 现在使用 ProgressBar

                val dialog = ProgressDialog(this)
                dialog.setTitle("请稍候")
                dialog.setMessage("正在努力加载页面")
                dialog.max = 100
                dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL) // ProgressDialog.STYLE_SPINNER
                dialog.setOnShowListener {
                    thread {
                        (0..100).forEach {
                            Thread.sleep(250)
                            runOnUiThread { dialog.progress = it /* 设置进度 */}
                        runOnUiThread { dialog.cancel() /* 完成后关闭容器 */ }
                dialog.show()
    

    一个通用界面结构

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout 
        xmlns:android="http://schemas.android.com/apk/res/android" 
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools" >
        <!-- DrawerLayout 滑出菜单 -->
        <!-- 里面 2 个组件,第一个为主界面,第二个为滑出内容 -->
        <androidx.drawerlayout.widget.DrawerLayout >
            <!-- 主界面区, -->
            <!-- CoordinatorLayout 会监听子控制自动调整,包含 Snackbar -->
            <androidx.coordinatorlayout.widget.CoordinatorLayout >
                <!-- AppBarLayout 可以与 RecyclerView 与 toolbar 配合, 实现上滚隐藏, 下滚显示的效果 -->
                <!-- app:layout_scrollFlags="enterAlways|scroll|snap" 向上滚动隐藏,向下滚动显示, -->
                <!-- app:layout_behavior="@string/appbar_scrolling_view_behavior" 滚动时通知 appbarlayout -->
                <com.google.android.material.appbar.AppBarLayout 
                    android:layout_height="wrap_content">
                    <!-- CollapsingToolbarLayout 自动收缩子控件, 可以设置 2 个控件, 设置 layout_collapseMode 属性 -->
                    <com.google.android.material.appbar.CollapsingToolbarLayout 
                        app:layout_scrollFlags="enterAlways|scroll|snap" >
                        <!-- 横幅, CollapsingToolbarLayout - parallax 自动隐藏 -->
                        <ImageView 
                            android:layout_height="180dp" 
                            app:layout_collapseMode="parallax" />
                        <!-- 工具栏 CollapsingToolbarLayout - pin 一直显示 -->
                        <!-- 1. background 背景色 -->
                        <!-- 2. theme 因为默认主题为亮色,所以文字是显色,在 attr/colorPrimary 中显示不出现。所以将标题栏设置为 Dark 主题 -->
                        <!-- 3. popupTheme 因为 2 的原因,工具栏使用黑色主题,所以弹出菜单也是黑色。会比较难看,这里再调整为亮色   -->
                        <androidx.appcompat.widget.Toolbar 
                            android:layout_height="wrap_content"
                            android:background="?attr/colorPrimary"
                            android:minHeight="?attr/actionBarSize"
                            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
                            app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
                            app:layout_collapseMode="pin" />
                    </com.google.android.material.appbar.CollapsingToolbarLayout>
                </com.google.android.material.appbar.AppBarLayout>
                <!-- SwipeRefreshLayout 子组件带有自动刷新功能, appbar_scrolling_view_behavior 与 AppBarLayout 配合 -->
                <androidx.swiperefreshlayout.widget.SwipeRefreshLayout 
                    app:layout_behavior="@string/appbar_scrolling_view_behavior">
                    <!-- 显示列表例子 -->
                    <androidx.recyclerview.widget.RecyclerView 
                        android:layout_width="match_parent" android:layout_height="match_parent" />
                </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
                <!-- 一个浮动按钮, 使用 layout_anchor 与 layout_anchorGravity 将位置定位至其它控件上 -->
                <com.google.android.material.floatingactionbutton.FloatingActionButton 
                    android:layout_width="wrap_content" 
                    android:layout_height="wrap_content" 
                    android:layout_margin="16dp" 
                    app:layout_anchor="@id/toolbar"                 
                    app:layout_anchorGravity="bottom|center" 
                    android:src="@drawable/ali_months" />
            </androidx.coordinatorlayout.widget.CoordinatorLayout>
            <!-- 引用滑出区 -->
            <!-- 必须加入 layout_gravity 指定滑动方向, 不然会报错 -->
            <!-- headerLayout 指定滑出区的头部 -->
            <!-- menu 指定滑出区的菜单 -->
            <com.google.android.material.navigation.NavigationView 
                android:background="#FFF"
                app:headerLayout="@layout/mainactivity_x_nav_header"
                app:menu="@menu/mainactivity_x_nav_menu"        
                android:layout_gravity="start">
                <!-- 在滑出区中设置一个文本 -->
                <TextView            
                android:layout_gravity="bottom|right"
                android:layout_marginRight="12dp"
                android:layout_marginBottom="12dp"
                android:text="关于: xxxx"></TextView>
            </com.google.android.material.navigation.NavigationView>
        </androidx.drawerlayout.widget.DrawerLayout>
    </androidx.constraintlayout.widget.ConstraintLayout>
    
    工具栏及滑出区
      /** 初始化 toolbar */ 
        private fun initToolbar() {
            val toolbar = this.findViewById<Toolbar>(R.id.toolbar)
            this.setSupportActionBar(toolbar)
            supportActionBar?.let {
                it.setDisplayHomeAsUpEnabled(true) // 显示右上角图标, 默认为一个回退键 
                it.setHomeAsUpIndicator(R.drawable.menu_24px)
            /** 滑动区滑动收起设置,
             * 如果不用 ActionBarDrawerToggle 需要
             * 1. 监听 DrawerLayout 的展开、收起事件,然后设置图标,而且它还带动画
             * 2. 点击 android.R.id.home 时做 DrawerLayout 的展开、收起动作 */
            val navigationDrawer = this.findViewById<DrawerLayout>(R.id.drawer_layout)
            val drawerToggle = ActionBarDrawerToggle(
                this,
                navigationDrawer,
                toolbar,
                R.string.drawer_open, // <-- ""
                R.string.drawer_close // <-- ""
            navigationDrawer.addDrawerListener(drawerToggle)
            navigationDrawer.isClickable = true
            drawerToggle.syncState()
    
    滑出区中的通用菜单
        private fun initNavigationView() {
            val view = this.findViewById<NavigationView>(R.id.navigationView)
            view.setCheckedItem(R.id.nav_menu_call) // 设置第一个为被选择项
            view.itemIconTintList = null // 菜单图标设置为彩色
            view.setNavigationItemSelectedListener {
                val layout = this.findViewById<DrawerLayout>(R.id.drawer_layout)
                layout.close() // 选择选择后关闭滑出菜单
    
       private fun initSwipeRefreshLayout() {
            val view = this.findViewById<SwipeRefreshLayout>(R.id.swipeRefreshLayout)
            view.setOnRefreshListener {
                thread {
                    Thread.sleep(3000)
                    runOnUiThread {
                        view.isRefreshing = false // 关闭刷新图标 
    

    jetpack

    LifecycleObserver

    接收 View, Fragment, Application 的事件

    // 现在好像是建议使用 DefaultLifecycleObserver, 然后 override 需要的函数 class MainLifecycleObserver() : DefaultLifecycleObserver { override fun onStart(owner: LifecycleOwner) { super.onStart(owner) override fun onStop(owner: LifecycleOwner) { super.onStop(owner) // 监听, 在 View, Fragment, Application 中 this.lifecycle.addObserver(MainLifecycleObserver())

    ViewModel

    http://yifeiyuan.me/blog/52b29a03.html

    ViewModel 对象,里面一般会有 LiveData 值, LiveData 对象中的值变更时发出通知 , 还没有试过,在不同的类中创建是否是同一个对象

    它能感知生命周期

    class MainViewModel(initVal: Int) : ViewModel() {
        val ageValue = MutableLiveData(initVal)
    // 在 view 中调用, 多次调用会返回对应于 View 的唯一实例 
    val viewModel = ViewModelProvider(this)[MainViewModel::class.java]
    // 如果 ViewModel 带有参数,则需要使用 factory 创建
    class MainViewModelFactory(private val initVal: Int) : ViewModelProvider.Factory {
        override fun <T : ViewModel> create(modelClass: Class<T>): T {
            return MainViewModel(initVal) as T
    // 使用工厂模式创建, 使用 ViewModelProvider 时传入参数
    val viewModel = ViewModelProvider(this, MainViewModelFactory(100))[MainViewModel::class.java]
    

    LiveData

    http://yifeiyuan.me/blog/9d326805.html

    值变更时发出通知, 它与LifecycleOwner (view之类) 对象进行绑定,在 view 在生命周期中才进行通知

    class MainViewModel(initVal: Int) : ViewModel() {
        private val ageValue = MutableLiveData(initVal) // 初始化值内容
        val age: LiveData<Int>
            get() = ageValue // 如果不想直接对外暴露可读写的 ageValue,这里使用只读 LiveData 进行包装   
        fun plusOne() { // 对 livedata 数据进行变更,会调用 observe 进行通知
            this.ageValue.value = (this.ageValue.value ?: 0) + 1
    // 监控变量的变更
    viewModel.age.observe(this, Observer {
        val textView = this.findViewById<TextView>(R.id.InfoTextView)
        textView.text = "$it"
    
    Transformations.map 暴露对象中的某个值
    data class UserData(var age: Int) {} // 使用 ViewModelProvider 获取唯一实例 class MainViewModel(initVal: Int) : ViewModel(), DefaultLifecycleObserver { // 初始化变量 private val userData = MutableLiveData(UserData(initVal)) // 包装对象中的值 val age = Transformations.map(this.userData) { data -> "${data.age} value" } fun plusOne() { // 变更值内容,必须直接变量 value 内容,如果变更属性则不会通知 this.userData.value = UserData((this.userData.value?.age ?: 0) + 1) // 监控变量的变更 viewModel.age.observe(this, Observer { val textView = this.findViewById<TextView>(R.id.InfoTextView) textView.text = "$it"
    Transformations.switchMap 更新 LiveData

    在某些场景中ViewModel 需要更新 LiveData 对象,会导致监听的 LiveData 对象变量,监听失败

    使用 switchMap 的底层更新 LiveData 中的值(可能,没看过源代码)

    class MainViewModel(initVal: Int) : ViewModel() {
        // 用来触发 switchMap 更新的参数值,
        // 如果没有参数值,则使用 private val refreshKeyData = MutableLiveData<Any?>(), 触发 this.refreshKeyData.value = this.refreshKeyData.value    
        private val userAge = MutableLiveData(initVal)
        // switchMap 监听某个 LiveData 作为调用参数进行同步更新 
        val userData: LiveData<UserData> =
            Transformations.switchMap(userAge) { it -> createUserData(it) }
        /** 模拟对象生成函数, 也许是一个生成器调用数据库或是 webapi */
        private fun createUserData(it: Int?): LiveData<UserData> {
            val data = UserData(it ?: 0)
            return MutableLiveData(data)
        fun plusOne() { // 更新原始值
            this.userAge.value = (this.userAge.value ?: 0) + 1
    // 监控变量的变更, 这里监听的是 LIveData,所以如果直接变更 userData 会导致监听的是旧对象引用 
    viewModel.userData.observe(this, Observer {
                val textView = this.findViewById<TextView>(R.id.InfoTextView)
                textView.text = "${it.age}"
    

    Fragment

    自定义控件继承至 view , Fragment 更轻量级。一些动作交给父级 View 来处理

    Fragment 也有一些子类,比如 DialogFragment , ListFragment , PreferenceFragment , WebViewFragment 可以直接使用

    创建 Fragment

    1. 创建资源 xml 文件,与普通 activity 文件一样 2. 新建类 class LeftFragment : Fragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.fragment_left, container, false) // 创建视图

    ```xml

     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:orientation="horizontal">
         <fragment
             android:layout_width="140dp"
             android:layout_height="match_parent"
             android:name="xsoft.demo.xapp.LeftFragment" <!-- 直接在 xml 资源文件中引用,并且要指明类路径  --> 
             android:id="@+id/left_fragment"/>
     </LinearLayout>
    
       <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal">
            <!-- 资源文件中定义一个占位符 -->
          <FrameLayout
              android:id="@+id/framelayout_right"
              android:layout_width="wrap_content"
              android:layout_height="match_parent"
              android:layout_weight="1" />
        </LinearLayout>
        // 动态载入  fragment
        private fun loadFragment() {
            // 返回 fragment 管理器  
            val manager = this.fragmentManager
            manager?.let {
                val arguments = Bundle() // 准备一些参数传入
                arguments.putInt('test', 123) 
                val fragment = RightFragment()
                fragment.arguments = arguments // 传递一些参数 
                val transaction = manager.beginTransaction()
                transaction.replace(R.id.framelayout_right, fragment)
                transaction.addToBackStack(null) // 将当前 fragment 加入到栈中,可以进行后退操作
                transaction.commit()
        // 在 onCreateView 在查找按钮之类的操作,因为在 onCreate 中视图应该还没有创建
        override fun onCreateView(
            inflater: LayoutInflater, container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View? {
            // 创建视图
            val view = inflater.inflate(R.layout.fragment_left, container, false)
            // 返回 view 并查找控件, 也可以使用
            val button = view.findViewById<Button>(R.id.Load_Fragment_Button)
            button?.setOnClickListener {
                this.loadFragment()
            // 返回当前的 activity ,然后读取其它 fragment 并进行一些操作
            val fragment =
                activity?.supportFragmentManager?.findFragmentById(R.id.left_fragment)
            return view
    

    适配不同屏幕

    设置不同的 activity.xml, 比如放在 layout 目录中与放在 large_layout 中的 activity.xml 组织不同布局的 Fragment 。 不同的分辨率设置见手册

    Android 屏幕适配:最全面的解决方案 - 简书 (jianshu.com)

    比如做一个新闻阅读,一个列表,一个显示内容
    在手机端显示列表,点击一个条目后显示一个 activity, 如果在平板端,左右显示列表与内容
    1. 一个 Fragment1 显示列表,一个 Fragment2 显示内容
    2. 设置入口 activity (生成2个),一个在 layout 中 (嵌入列表 Fragment1), 一个在 layout-sw600dp 中 (嵌入列表 Fragment1 与内容 Fragment2)
    3. 通过查找入口 activity 中带不带内容 Fragment2 看是哪个 activity
    4. 点击列表中的某一项时如果是手机端,跳转到一个包括内容 Fragment2 的 activity 。 如果是平板中,则直接显示在当前的内容 Fragemnt2 中
    

    一些可用的小知识

    this.activity as? MainActivity // Fragment 中找到当前绑定的 activity
    this.view // 返回当前 root view
    val activity = viewGroup.context as? AppCompatActivity (或是 activity, 根据实际情况) // RecyclerView.ViewHolder 中返回 activity
    val manager  = activity?.supportFragmentManager // 在 activity 中返回 fragment 管理器,并进一步进行管理 
    val  fragment = manager?.findFragmentById(R.id.NewsContentFragment)
    // 如果要访问控件请在 onViewCreated 中进行,因为 onCreate 中视图还没有创建, 如果要在 onCreateView 在访问,请 inflater.inflate 出 View 后,使用访问 View 处理
    

    Intent

    启动 Activity , Service , BroadcastReceiver 的参数

    显式指定启动对象

    // 显式指定
    val intent = Intent()
    // 指定启动对象包及类
    intent.component = ComponentName(activity, MainActivity_BBS::class.java)
    intent.setClass(activity, MainActivity::class::class.java) // 内部应该调用 setComponent 
    intent.setClassName(activity, "${MainActivity_BBS::class.java}") // 内部应该是new 了 setComponent 
    this.startActivity(intent)
    

    使用 action 隐式指定启动对象

    // AndroidManifest.xml 中为 Activity , Service , BroadcastReceiver 指定 intent-filter, // intent-filter 中指定一个或是多个 action 及 category // 启动定义 Intent 的 action 及 category , 系统会自动查找匹配的 action/category 进行启用 // 如果有多个 Activity(Service\BroadcastReceiver) 符合条件,系统弹出对话框提示用户选择哪一个进行启动 // 隐式指定 val intent = Intent() // 指定启动对象定义的 action, 只能指定一个 intent.action = "android.intent.action.MAIN" // 指定启动对定义的 category, 可多次调用 addCategory 指定多个对象,如果不指定,则为 android.intent.category.DEFAULT intent.addCategory("android.intent.category.DEFAULT") this.startActivity(intent)
    隐匿调用系统动作
    // 返回首页
    val intent = Intent()
    intent.action = Intent.ACTION_MAIN
    intent.addCategory(Intent.CATEGORY_HOME)
    this.startActivity(intent)
    

    Data 与 Type

    // Intent 通过 Data 传入启动参数
    // Data 格式为 Uri
    // 但是有一个 Data 不足以定类型,比较指定一个文件路径,可能是图片也可以是音乐,那么可以指定 type
    // type 为 MIME 格式
    // data 与 type 单独指定会相互覆盖,请调用  setDataAndType 同时指定 
    intent.data = Uri.parse("content://com.anroid.contacts/contacts/1")
    intent.type = "png/image"
    intent.setDataAndType(Uri.parse("content://com.anroid.contacts/contacts/1"), "png/image")
    // AndroidManifest.xml 中设置 intent-filter 时可以指定 data 节点,其中指定mimeType, scheme, host, port path, pathPrefix, pathPattern 等参数来匹配传入的 data 参数 
    

    启动时可以调用 setFlag 指定一个控制标志,比如将启动时不使用启动动画,置前操作之类的

    两种发送机制,一种是异步发送,所有接收器同时收到。一种是排序式,一个处理完后处理下一个。可以中断广播。

    动态注册接收器

       lateinit var timeBroadcastReceiver: TimeBroadcastReceiver
        override fun onCreate(savedInstanceState: Bundle?) {
            // 3. 定义接收事件类型,这是一分钟一次的时间事件 
            val intentFilter = IntentFilter()
            intentFilter.addAction("android.intent.action.TIME_TICK")
            // 4. 进行动态注册接收器
            timeBroadcastReceiver = TimeBroadcastReceiver()
            registerReceiver(this.timeBroadcastReceiver, intentFilter)
        override fun onDestroy(){
            // 5. 动态注销接收器
            unregisterReceiver(this.timeBroadcastReceiver)
        // 1. 继承一个 BroadcastReceiver
        inner class TimeBroadcastReceiver : BroadcastReceiver(){
            // 2. 处理接收事件
            override fun onReceive(context: Context?, intent: Intent?) { ... }
    

    静态注册接收器

    静态注册可以在程序未启动时也能接收到事件。不过大多数没有指明接收者的事件已经不允许静态注册接收器了。但是还保留了一些,比如: android.intent.action.BOOT_COMPLETED

    <!-- 1. 继承一个 BroadcastReceiver -->
    class BootCompletedReceiver : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) { ... }
    <!-- 2. 编辑 AndroidManifest.xml  -->
            <receiver
                android:name=".BootCompletedReceiver"
                android:enabled="true" <!-- 3. 是否启用 --> 
                android:exported="true"> <!-- 4. 是否允许接收程序外的广播 -->
                <intent-filter>
                    <!-- 5. 定义接收事件  -->
                    <action android:name="android.intent.action.BOOT_COMPLETED" />
                </intent-filter>
            </receiver>
    <!-- 6. 不过现在都是权限申请问题。以后再说 -->
    
    <!-- 1. 定义接收器 -->
    <receiver android:name=".DemoBroadcastReceiver" android:enabled="true" android:exported="true">
        <intent-filter android:priority="100"> <!-- 如果是顺序广播,这里定义权重 -->
            <action android:name="xsoft.demo.xapp.DEMO_MESSAGE" />
        </intent-filter>
    </receiver>
    <!-- 2. 继承一个 BroadcastReceiver -->
    class DemoBroadcastReceiver : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            <!-- 3. 处理接收事件, 如果是顺序广播,可以停止继续广播 -->
            abortBroadcast()
    <!-- 4. 发送广播 -->
    val intent = Intent("xsoft.demo.xapp.DEMO_MESSAGE")
    intent.setPackage(packageName) <!-- 5. 指定消息的接收者所在包,目前不指定无法接收到消息 -->
    sendBroadcast(intent) <!-- 6.1 发送广播 -->
    sendOrderedBroadcast(intent, null) <!-- 6.2 发送顺序广播 -->
    

    文本文件读写

        private fun loadFile(fileName: String) {
            val input = openFileInput(fileName)
            val streamReader = InputStreamReader(input)
            val bufferedReader = BufferedReader(streamReader)
            bufferedReader.use {
                bufferedReader.forEachLine { it -> Log.d("Debug", it) }
        private fun saveFile(fileName: String) {
            val output = openFileOutput(fileName, Context.MODE_APPEND /* 追加模式 */ ) // Context.MODE_PRIVATE /* 覆盖模式 */
            val streamWriter = OutputStreamWriter(output)
            val bufferedWriter = BufferedWriter(streamWriter)
            bufferedWriter.use{
                it.write("测试内容")
    
    // 以 xml 格式进行文件存储 
    // 作为应用设备不错
        private fun saveFile(fileName: String) {
            // 或是 getPreferences,以类名为文件名
            val preferences =
                this.getSharedPreferences(fileName, Context.MODE_PRIVATE /* 目前只有这一种模式可用 */)
            val edit = preferences.edit()
            edit.putString("key1", "我是测试")
            edit.putBoolean("key2", true)
            edit.clear() // 清空所有数据
            edit.apply() // 进行保存
        private fun loadFile(fileName: String) {
            val preferences =
                this.getSharedPreferences(fileName, Context.MODE_PRIVATE /* 目前只有这一种模式可用 */)
            val value1 = preferences.getString("key1", "1111")
            val value2 = preferences.getBoolean("key2", false)
    

    SQLite

    /** 数据数读写, 要继承 SQLiteOpenHelper, 实现 onCreate 与 onUpgrade */ class SQLiteDemo(context: Context, name: String, version: Int) : SQLiteOpenHelper(context, name, null, version) { companion object { /** 进行数据库演示调用 */ fun test(context: Context, fileName: String) { val sqLiteDemo = SQLiteDemo(context, fileName, 1) sqLiteDemo.test() /* 数据库演示 */ private fun test() { val tableName = "Books" val dbWriter = this.writableDatabase /** 插入数据 */ /****************************************************************************************/ val values = ContentValues().apply { put("author", "x") put("price", 12.5) var rowId = dbWriter.insertWithOnConflict(tableName, null, values, SQLiteDatabase.CONFLICT_NONE) Log.d("db", "插入数据,编号: $rowId") /** 更新数据 */ /****************************************************************************************/ values.clear() values.put("price", 20.1) var count = dbWriter.update(tableName, values, "id = ?", arrayOf("$rowId")) Log.d("db", "更新 $count 条数据") /** 删除数据 */ /****************************************************************************************/ count = dbWriter.delete(tableName, "id = ?", arrayOf("$rowId")) Log.d("db", "删除 $count 条数据") /** 查询数据 */ /****************************************************************************************/ val dbReader = this.readableDatabase /** 传入表名,列名,条件,条件参数,是否分组,排序等参数 */ var rows = dbReader.query(tableName, null, "id >= ?", arrayOf("0"), null, null, null, null) if (rows.moveToFirst()) { val author = rows.getString(rows.getColumnIndex("author")) val price = rows.getDouble(rows.getColumnIndex("price")) Log.d("db", "查询 $author $price ") } while (rows.moveToNext()) /** 直接运行 sql */ /****************************************************************************************/ if(true) /** DatabaseUtils, 数据库辅助函数 https://www.apiref.com/android-zh/android/database/DatabaseUtils.html */ val count = DatabaseUtils.longForQuery(dbWriter, "select count(*) from books; ", null) Log.d("db", "插入数据 $count 条 ") /** 直接运行 sql */ /****************************************************************************************/ // dbWriter.execSQL() dbReader.rawQuery("select * from Books;", null).use { if (it.moveToFirst()) { Log.d("db", "共返回 ${it.columnCount} 列 ") for (idx in 0 until it.columnCount){ Log.d("db", "第 $idx 列名 ${ it.getColumnName(idx) }") /** 事务 */ /****************************************************************************************/ dbWriter.beginTransaction() // 开始事务 try { dbWriter.setTransactionSuccessful() // 设置操作成功 finally{ dbWriter.endTransaction() // 结束事务 /** 创建语句 */ private val createSql: String = CREATE TABLE "Books" ( id INTEGER PRIMARY KEY AUTOINCREMENT, author TEXT, price REAL """.trimIndent() /** 进行数据为创建, 自动调用 */ override fun onCreate(db: SQLiteDatabase?) { this.onUpgrade(db, 0, 100000) // 调用链式升级函数, 实际可以直接生成最后的数据表, /** 进行数据为升级, 自动调用 */ override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { // 我想到的升级链调用 this.upgradeToVer3(db, oldVersion) // 假设最新版本是 3 private fun upgradeToVer3(db: SQLiteDatabase?, oldVersion: Int) { if(oldVersion >= 3 ){ return } // 不需要升级 upgradeToVer2(db, oldVersion) // 先进行升 2 级操作 TODO("进行 2 升级至 3 动作") private fun upgradeToVer2(db: SQLiteDatabase?,oldVersion: Int) { if(oldVersion >= 2 ){ return } // 不需要升级 upgradeToVer1(db, oldVersion) // 先进行升 2 级操作 TODO("进行 1 升级至 2 动作") private fun upgradeToVer1(db: SQLiteDatabase?,oldVersion: Int) { db?.execSQL(createSql) // 创建数据库

    因为空间有限,程序安装在内部的存储器中,数据一般推荐放在 SD 卡中 (目前手机都已经固话至手机中)。

    Environment.get***Directory() 获取内部存储器相关目录

    Environment.getExternalStoragePublicDirectory(***) 获取 SD 卡中的共有目录,一般因为权限问题,很多目录不能直接读写了。

    Context.getExternalFilesDir(***) 获取 SD 卡中应用专属目录,没有权限问题

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMSt" />
    val rootDirectory = Environment.getRootDirectory()
    this.showLog("系统根目录 rootDirectory: $rootDirectory")
    val dataDirectory = Environment.getDataDirectory()
    this.showLog("系统数据目录 dataDirectory: $dataDirectory")
    val downloadCacheDirectory = Environment.getDownloadCacheDirectory()
    this.showLog("下载缓存目录 downloadCacheDirectory: $downloadCacheDirectory")
    val externalStorageDirectory = Environment.getExternalStorageDirectory()
    this.showLog("外部 SD 卡 externalStorageDirectory: $externalStorageDirectory")
    val exStorageState = Environment.getExternalStorageState()
    this.showLog("外部 SD 卡状态 exStorageState: $exStorageState")
    // 公共数据目录, 现在一般不给权限  
    val dcim = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
    this.showLog("DIRECTORY_DCIM $dcim")
    // 公共数据目录, 现在一般不给权限  
    val downloads = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
    this.showLog("DIRECTORY_DOWNLOADS: $downloads")
    // 应用专属目录 
    val mydcim = this.getExternalFilesDir(Environment.DIRECTORY_DCIM)
    this.showLog("mydcim: $mydcim")
    // 应用专属目录 
    val myPictures = this.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
    this.showLog("myPictures: $myPictures")
    // 应用专属目录 
    val mydownloads = this.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
    this.showLog("mydownloads: $mydownloads")
    系统根目录 rootDirectory: /system
    系统数据目录 dataDirectory: /data
    下载缓存目录 downloadCacheDirectory: /data/cache
    外部 SD 卡 externalStorageDirectory: /storage/emulated/0
    外部 SD 卡状态 exStorageState: mounted
    DIRECTORY_DCIM /storage/emulated/0/DCIM
    DIRECTORY_DOWNLOADS: /storage/emulated/0/Download
    mydcim: /storage/emulated/0/Android/data/xsoft.demo.myapplication/files/DCIM
    myPictures: /storage/emulated/0/Android/data/xsoft.demo.myapplication/files/Pictures
    mydownloads: /storage/emulated/0/Android/data/xsoft.demo.myapplication/files/Download
    

    跨程序共享数据

    获取一个程序与外部程序传递数据的通道

    ContentProvider 由程序实现,借外部访问程序提供的数据。实现后提供一些增、查、改、删的功能,并要在 AndroidManifest.xml 中注册。见后面的例子。一般用的少,因为程序一般不提供外部访问

    ContentResolver 程序使用该接口访问其它程序提供的数据(比如 ContentProvider )。一般读写系统数据(电话本)等比较多一些

    ContentObserver 监听 ContentResolver 查询到的数据变量 , 提供方调用 notifyChange()

    动态权限申请

    1. 在 AndroidManifest.xml 中加入需要的权限 
    <uses-permission android:name="android.permission.CALL_PHONE" />
    2. 调用函数中申请权限
    private fun autoCall(phone: String) {
        2.1  将调用设置为回调函数,如果有权限,直接调用,如果没有权限放到一个字典中在权限申请通过后进行调用 
        val callback: () -> Unit = { this.callPhone(phone) }
        2.2 调用我写的通用函数,进行权限查询并进行调用 
        QueryAndInvoke(callback, Manifest.permission.CALL_PHONE)
    /** 线程安全的计数器, 为了生成一个唯一 id,用为回调函数标记  */
    private val nextActiveCounter: AtomicInteger = AtomicInteger()
    /** 缓存需要处理的回调函数, 使用唯一编号进行标记 */
    private val nextActiveMap = mutableMapOf<Int, () -> Unit>()
    3. 我写的通用处理函数,查询一个权限,如果有权限直接调用,如果没有权限请求权限后再次调用 
    private fun QueryAndInvoke(callback: () -> Unit, premission: String) {
        3.1 检测权限        
        if (ContextCompat.checkSelfPermission(this, premission) != PackageManager.PERMISSION_GRANTED) {
            3.2 没有权限,将调用缓存起来
            val idx = nextActiveCounter.getAndIncrement() // 这里用了一个线程安全的计数器生成调用序号 private val nextActiveCounter : AtomicInteger = AtomicInteger()
            nextActiveMap[idx] = callback // 这里将回调的调用序号存储进字典
            3.3 申请权限, 传入要求的权限及调用序号
            ActivityCompat.requestPermissions(this, arrayOf(premission), idx)
        } else {
            3.4 有权限,直接调用
            callback.invoke()
    4. 根据授权情况进行下一步操作
    override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        4.1 取出待处理的回调
        val callback = nextActiveMap.remove(requestCode)
        4.2 如果授权成功,进行回调
        if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                callback?.invoke()
    5. 真实调用
    private fun callPhone(phone: String) {
        // todo: 捕获到异常并进行提示
        check(!phone.isNullOrBlank()) { "电话号码未设置" }
        val intent = Intent(Intent.ACTION_CALL)
        intent.data = Uri.parse("tel:${phone.trim()}")
        startActivity(intent)
    
    一个回调辅助类

    自动生成一个唯一 id 对应于一个动作

    /** 保存一些常用操作 */
    class ActiveStore {
        /** 线程安全的计数器 */
        private val nextActiveCounter: AtomicInteger = AtomicInteger()
        /** 保存动作 */
        private val nextActiveMap = mutableMapOf<Int, (Intent?) -> Unit>()
        /** 保存一个动作 */
        fun putActive(active: (Intent?) -> Unit): Int {
            val idx = this.nextActiveCounter.getAndIncrement()
            this.nextActiveMap[idx] = active
            return idx
        /** 执行一个动作 */
        private fun runActive(idx: Int, data: Intent?) {
            val active = this.remove(idx) ?: throw Resources.NotFoundException("找不到编号为 $idx 的动作")
            active(data)
        /** 移除一个动作 */
        private fun remove(idx: Int): ((Intent?) -> Unit)? {
            return this.nextActiveMap.remove(idx)
        /** 使用 onActivityResult 参数执行 */
        fun runWithActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
            if (resultCode == Activity.RESULT_OK) {
                this.runActive(requestCode, data)
            } else {
                this.remove(requestCode)
    
    一个权限的回调辅助类

    第三方库 https://github.com/guolindev/PermissionX

    /** 我写的一个简易保存权限申请动作 */
    class PermissionsActionStore {
        // 定义动作 id
        private val nextActiveCounter: AtomicInteger = AtomicInteger()
        // 存储 id 与动作绑定
        private val nextActiveMap =
            mutableMapOf<Int, (permissions: Array<out String>, grantResults: IntArray) -> Unit>()
        /** 定义授权动作, 指定申请的一个权限  */
        fun putRequestPermissionsAction(
            activity: Activity,
            premission: String,
            grantedAction: () -> Unit, // 通过时执行动作
            deniedAction: () -> Unit = {}, // 禁用时执行动作
            rationaleAction: () -> Unit = {} // 如果没有权限时执行动作
        ): Int
            return this.putRequestPermissionsAction(activity, arrayOf(premission), grantedAction, deniedAction, rationaleAction)
        /** 定义授权动作,指定申请的多个权限  */
        fun putRequestPermissionsAction(
            activity: Activity,
            premissions: Array<String>,
            grantedAction: () -> Unit, // 通过时执行动作
            deniedAction: () -> Unit = {}, // 禁用时执行动作
            rationaleAction: () -> Unit = {} // 如果没有权限时执行动作
        ): Int {
            // 指定的权限是否都有
            val allSuccess = premissions.all { it -> ContextCompat.checkSelfPermission(activity, it) == PackageManager.PERMISSION_GRANTED }
            if (allSuccess) {
                // 已经有指定权限,直接调用
                grantedAction?.invoke()
                return -1
            rationaleAction?.invoke()
            // 定义一个有权限反馈时怎么执行动作
            val action: (Array<out String>, IntArray) -> Unit = { permissions: Array<out String>, grantResults: IntArray ->
                if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    grantedAction?.invoke() // 通过时执行
                else{
                    deniedAction?.invoke() // 禁用时执行
            // 存储要执行的动作
            val id = this.nextActiveCounter.getAndIncrement()
            this.nextActiveMap[id] = action
            // 请求权限
            ActivityCompat.requestPermissions( activity, premissions, id)
            return id
        /** 执行定义的授权动作, override fun onRequestPermissionsResult 中调用 */
        fun runRequestPermissionsAction(
            requestCode: Int,
            permissions: Array<out String>,
            grantResults: IntArray
            val action  = this.nextActiveMap.remove(requestCode)
            action?.invoke(permissions, grantResults)
    

    查询通讯录

    0. AndroidManifest.xml 中加入相应的权限
    1. 定义调用函数
    val callback: () -> Unit = {
        this.sendLog("开始查询电话")
        3. 进行查询, 与数据库操作很象    
        // 如果插入为 this.contentResolver.insert
        // 如果删除为 this.contentResolver.delete
        // 如果更新为 this.contentResolver.update
        this.contentResolver.query( 
                    ContactsContract.CommonDataKinds.Phone.CONTENT_URI, // 查询哪个表. 格式为 content://com.android.contacts/data/phones
                    null, // 查询哪个列
                    null,  // where 语句
                    null, // where 参数
                    null // 排序列
                )?.let {
                    while (it.moveToNext()) {
                        4. 查询电话号码 
                        val number =
                            it.getString(it.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
            } } }
    2. 我写的授权并调用函数, 见动态权限申请部分
    this.QueryAndInvoke(callback, Manifest.permission.READ_CONTACTS)
    

    监听数据更新 ContentObserver

    调用后监听 ContentResolver 查询到的数据变量 , 提供方调用 notifyChange()

    1. 实现一个 ContentObserver 接收监听内容   
    class XContentObserver(handler: Handler?) : ContentObserver(handler) {
            // 监听的数据发生了变化
            override fun onChange(selfChange: Boolean) {
                super.onChange(selfChange)
    2. 注册监听对象
    val uri = Telephony.Sms.CONTENT_URI // 要监听的内容,比如接收到短信
    val observer = XContentObserver(Handler())
    this.contentResolver.registerContentObserver(uri, true, observer)
    3. 释放监听对象
    this.contentResolver.unregisterContentObserver(observer)
    

    自定义 ContentProvider

     * 自定义一个  ContentProvider
     * 先定义一个路径:com.x.xsoft.ltd.app.provider, 使用  定义一个
     * 完整的 uri 表名 content://path/table, 数据列 content://path/table/32
     * 定义一个 ContentProvider 子类
     * 在 AndroidManifest.xml 中注册 provider 绑定 ContentProvider 类与 路径
     * 使用 UriMatcher 辅助对处理的 url 进行判断
     * 完成 ContentProvider 子类
    
       companion object {
            // 1. 定义路径
            const val authoriy: String = "com.x.xsoft.ltd.app.provider"
            // 2. 构建查询的链接的辅助函数,可以生成完成的链接
            public fun buildUri(table: String, id: String? = null): Uri {
                check(table.isNotBlank()) { "没有指定 table" }
                return if (id.isNullOrBlank()) {
                    Uri.parse("content://$authoriy/$table")
                } else {
                    Uri.parse("content://$authoriy/$table/$id")
        /** 本例中一项就是一个 book 对象 */
        inner class Book(val id: Int, var name: String, var price: Double)
        // 3. 定义 uriMatcher 用来辅助对链接进行判断 
        private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH) // 使用 UriMatcher 辅助解析查询的内容
        private val xContentProviderDir = 0 // 匹配到目录(或表)
        private val xContentProviderAllStrItem = 1 // 匹配到所有目录(或表)
        private val xContentProviderAllNumItem = 2 // 匹配到某一项
        private val books = mutableListOf<Book>() // 书本列表
        init {
            // 定义查询的 uriMatcher, 传入 uri 时可以判断是目录还是条目
            uriMatcher.addURI(authoriy, "book", xContentProviderDir) // 这里定义一个目录(或表)
            uriMatcher.addURI(authoriy, "book/*", xContentProviderAllStrItem) // 匹配所有目录下任意字符的子项
            uriMatcher.addURI(authoriy, "book/#", xContentProviderAllNumItem) // 匹配所有目录下任意数值的子项
            // 注意,注册 book/* 与 book/# 有冲突,* 会优先匹配
        /** 辅助函数,从 ContentValues 中解析出 book */
        private fun parseBook(values: ContentValues?): Book {
            val name = values?.getAsString("name")
            val price = values?.getAsDouble("price")
            return Book(counter.getAndIncrement(), name ?: "", price ?: 32.0)
    
        override fun insert(uri: Uri, values: ContentValues?): Uri? {
            val book: Book = this.parseBook(values) // 使用辅助函数将 ContentValues 转为 Book 对象
            return when (uriMatcher.match(uri)) { // 判断连接的形式,如果是约定的格式,
                xContentProviderDir -> { // 如果是指定目录的,则进行添加
                    books.add(book) // 将对象加入到队列
                    buildUri("book", "${book.id}") // 使用辅助函数返回 uri
                else -> null
    
        override fun update( uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>? ): Int {
            return when (uriMatcher.match(uri)) { // 判断链接的类型 
                xContentProviderAllNumItem -> {
                    val id = uri.pathSegments[1] // uri 传入格式为 content://xxx/book/id, 所以 uri.pathSegments[1] 为 id,  uri.pathSegments[0] 为 book
                    val book = this.books.firstOrNull { it.id == id.toInt() } // 查出 book 
                    book?.let { it.name = book.name; it.price = book.price } // 进行更新 
                    return if (book == null) { 0 } else { 1 } // 返回更新数
                else -> { 0 }
    
        override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
            return when (uriMatcher.match(uri)) { // 判断链接的类型
                xContentProviderAllNumItem -> { // 
                    val id = uri.pathSegments[1] // 返回连接的 id 部分, content://xxx/book/id
                    val books = this.books.filter { it.id == id.toInt() } //列出的匹配对象
                    this.books.forEach { this.books.remove(it) } // 移除所有匹配对象
                    return books?.count() ?: 0
                else -> { 0 }
    
        override fun query(
            uri: Uri,
            projection: Array<out String>?, // 返回表名,
            selection: String?, // 查询条件
            selectionArgs: Array<out String>?, // 查询条件参数
            sortOrder: String? // 排序条件
        ): Cursor? {
            return when (uriMatcher.match(uri)) { // 判断链接的类型
                xContentProviderDir -> { // 匹配到 book 队列
                    // 可以从数据库直接返回,这里使用 MatrixCursor 自定义一个游标对象
                    val cursor = MatrixCursor(arrayOf("name", "price"))
                    // 这里简单处理,不考虑查询条件,直接返回所有 book 
                    this.books.forEach() {
                        val newRow = cursor.newRow()
                        newRow.add("id", it.id)
                        newRow.add("name", it.name)
                        newRow.add("price", it.price)
                    return cursor // 返回游标对象
                else -> { null }
    
            return when (uriMatcher.match(uri)) { // 使用 uriMatcher 判断类型,并返回约定的格式。格式有要求,请查资料进行返回。
                xContentProviderDir -> {
                    "vnd.android.cursor.dir/vnd.$authoriy.book"
                xContentProviderAllNumItem -> {
                    "vnd.android.cursor.item/vnd.$authoriy.book"
                xContentProviderAllStrItem -> {
                    "vnd.android.cursor.item/vnd.$authoriy.book"
                else -> {
    
            val resolver = this.contentResolver
            if (true) { // 插入操作
                val uri = XContentProvider.buildUri("book") // 构建查询 uri content://xxx/book
                val result = resolver.insert( // 插入返回完整的数据 uri
                    ContentValues().apply { put("name", "xxx"); put("price", 64); })
                val resultUri = Uri.parse(result.toString())
                val id = resultUri.pathSegments[1] // 从返回链接中链接 id
            if (true) { // 更新操作
                val uri = XContentProvider.buildUri("book", "1") // 构建更新 uri content://xxx/book/1
                val result = resolver.update(  // 更新返回多少条数据更新了
                    ContentValues().apply { put("name", "xxx"); put("price", 64); }, null, null
            if (true) { // 删除操作
                val uri = XContentProvider.buildUri("book", "1") // 构建删除 uri content://xxx/book/1
                val result = resolver.delete(  // 删除,返回多少条数据更新了
                    uri, null, null
            if (true) { // 查询操作
                val uri = XContentProvider.buildUri("book") // 构建查询 uri content://xxx/book
                val cursor: Cursor? = resolver.query(uri, null, "id = ?", arrayOf("32"), null) // 输入查询条件 实际不考虑
                cursor?.let {
                    while (it.moveToNext()) { // 通过游标解析出内容
                        val name = it.getString(it.getColumnIndex("name"))
                        val price = it.getDouble(it.getColumnIndex("price"))
                        sendLog("name: $name price: $price")
    
    // implementation "com.android.support:support-compat:28.0.0"
        /** 测试函数 */
        private fun testFunc() {
            // 1. 返回通知管理器
            val manager = this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            // 2. 返回该通知准备使用的频道
            var channelId = this.getChannelId(manager)
            // 3. 创建通知内容
            val notification: Notification = this.createNotification(channelId)
            // 4. 定义一个通知编号,并进行通知
            val id = nextActiveCounter.getAndIncrement()
            manager.notify(id, notification) // 如果两次通知使用相同的 id, 则更新上次通知内容
            NotificationManagerCompat.from(this).apply{ notify(id, notification) } // 最新手册中的显示调用方式 
        // 创建通知内容
        private fun createNotification(channelId: String): Notification {
            // 1. 通知的一些文字内容
            val sdf = SimpleDateFormat("dd/M/yyyy hh:mm:ss")
            val currentDate = sdf.format(Date())
            // 2. 创建点击后的动作, PendingIntent 作为满足条件后进行的动作
            val intent = Intent(Intent.ACTION_CALL)
            intent.data = Uri.parse("tel:13111111111")
            val pi: PendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
            // 3. 创建通知内容
            val largeIcon = BitmapFactory.decodeResource(this.resources, R.drawable.whatsapp_512px) // 载入图像资源
            return NotificationCompat.Builder(this, channelId)
                .setContentTitle("通知标题")
                .setContentText("我是内容, 现在是 $currentDate") // 内容,但是太长会被截断
                .setSmallIcon(R.drawable.whatsapp_64px)
                .setLargeIcon(largeIcon) 
                .setStyle(NotificationCompat.BigPictureStyle().bigPicture(largeIcon)) // 设置背景,与其它 setStyle 只有一种生效
                .setStyle(NotificationCompat.BigTextStyle().bigText("这里可以放很长的文字 , 现在时间 $currentDate")) // 设置长文字不截断
                .setContentIntent(pi) // 设置点击后执行的动作
                .setAutoCancel(true) // 点击通知动作后自动关闭通知,或是调用 manager.cancel(id) 关闭通知
                .build()
        // 1. 新版本的通知需要属于基个频道。
        // 2. 不同的频道可以定义名称及重要程度。以方便使用者进行管理
        private fun getChannelId(manager: NotificationManager): String {
            val channelId = "xChannelId" // 频道 id,自定义
            // 1. 低版本 android 不需要频道
            if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O ){ // andorid 8.0
                return ""
            // 2. 如果频道已创建,直接返回频道 id
            var channel = manager.getNotificationChannel(channelId)
            if(channel != null ){
                return channelId // 频道已经存在,
            // 3. 新建频道并指定这个频道的重要性, 以后可以在通知管理中进行管理
            channel = NotificationChannel(channelId, "x 的普通消息", NotificationManager.IMPORTANCE_HIGH)
            manager.createNotificationChannel(channel)
            return channelId
    

    调用摄像头与相册

    主要是权限问题,一般建议使用 context.getExternalFilesDir() 获得应用对应的目录,这个目录下的文件读写没有限制

    Android 10适配要点,作用域存储_郭霖的专栏-CSDN博客

    Android 11新特性,Scoped Storage又有了新花样_郭霖的专栏-CSDN博客

    Android 10、11 存储完全适配(上) - 简书 (jianshu.com)

    调用摄像头

       // 1. 开始拍照
        private fun begin() {
            // 1.1. 创建一个文件并返回 uri
            var imageUri: Uri = this.createFile("output_image.jpg", "xsoft.demo.xapp.fileprovider")
            // 1.2. 定义拍照动作,并指定图像路径
            val intent = Intent("android.media.action.IMAGE_CAPTURE")
            intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri)
            // 1.3. 我的辅助函数,指定动作 id 及对应的 动作,并启用拍照动作
            val idx = this.activeStore.putActive { shwoImage(imageUri) }
            this.startActivityForResult(intent, idx)
        /** 创建一个文件,并返回对应的 uri */
        private fun createFile(fileName: String, authority: String): Uri {
            // 1. 因为权限文件,不要直接读写 sd 卡上文件,使用 externalCacheDir 目录
            val file = File(this.externalCacheDir, fileName)
            if (file.exists()) { file.delete() }
            file.createNewFile()
            // 2. 返回文件对应的 uri, 因为新的系统使用 uri 来进行图像处理
            return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                FileProvider.getUriForFile(this, authority, file)
            } else {
                Uri.fromFile(file)
        /** 2. 动作的返回结果, 使用我的辅助函数 */
        override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
            this.activeStore.runWithActivityResult(requestCode, resultCode, data)
        /** 3. 将图像显示至 ImageView */
        private fun shwoImage(imageUri: Uri) {
            val stream = this.contentResolver.openInputStream(imageUri)
            stream.use {
                val bitmap = BitmapFactory.decodeStream(stream)
                val view = this.findViewById<ImageView>(R.id.ImageView)
                view.setImageBitmap(bitmap)
    // AndroidManifest.xml 中定义一个文件共享配置 // 在 meta-data 中指定可以访问的文件
            <provider
                android:authorities="xsoft.demo.xapp.fileprovider"
                android:name="androidx.core.content.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, 指定可以读写 sd 卡的根目录
    <?xml version="1.0" encoding="utf-8"?>
    <paths xmlns:android="http://schemas.android.com/apk/res/android">
        <external-path name="my_images" path="/" />
    </paths>
    
        /** 1. 打开文件选择 */
        private fun begin() {
            // 设置打开文件选择
            val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
            intent.addCategory(Intent.CATEGORY_OPENABLE)
            intent.type = "image/*" // 文件类型
            val idx = activeStore.putActive { data -> shwoImage(data) }
            this.startActivityForResult(intent, idx)
        /** 2. 动作的返回结果, 使用我的辅助函数 */
        override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
            this.activeStore.runWithActivityResult(requestCode, resultCode, data)
        /** 3. 将图像显示至 ImageView */
        private fun shwoImage(data: Intent?) {
            data?.data?.let{
                uri ->
                val descriptor = this.contentResolver.openFileDescriptor(uri, "r")
                descriptor?.use {
                    val bitmap = BitmapFactory.decodeFileDescriptor(it.fileDescriptor)
                    val view = this.findViewById<ImageView>(R.id.ImageView)
                    view.setImageBitmap(bitmap)
    

    读写相册等权限

    新版本已经不允许直接读写sd 卡下的文件

    读写文件通过 MediaStore (contentResolver) 进行。

    早期版本中加入 <application android:requestLegacyExternalStorage="true" /> 获得读写权限 然后动态申请权限,可以参考我的 PermissionsActionStore 类 1. 使用我的辅助函数请示权限,然后处理 listAllImage 获取所有图像数据 val premissions = arrayOf( Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE this.permissionsActionStore.putRequestPermissionsAction(this, premissions, { this.listAllImage() }) 2. 获取所有图片数据,使用 contentResolver 读取数据 fun listAllImage():Unit { val cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, "${MediaStore.MediaColumns.DATE_ADDED} desc") if (cursor != null) { while (cursor.moveToNext()) { val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)) val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) this.sendLog("图像地址: $uri") // content://media/external/images/media/66 processImage(uri) // 这里进行复制图片操作 cursor.close() 3. 进行图片处理,将指定地址的图片创建 bitmap 对象 private fun processImage(uri: Uri) { val fd = contentResolver.openFileDescriptor(uri, "r") with(fd){ val bitmap = BitmapFactory.decodeFileDescriptor(this?.fileDescriptor) with(bitmap){ addBitmapToAlbum(bitmap, "我是副本", "image/jpeg", Bitmap.CompressFormat.JPEG ) 4. 进行图片复制,并加入到相册中 fun addBitmapToAlbum(bitmap: Bitmap, displayName: String, mimeType: String, compressFormat: Bitmap.CompressFormat) { val fileName = "${displayName}_${ System.currentTimeMillis()}" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // 4.1 如果是新的系统使用 contentResolver 进行处理 val values = ContentValues() values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) // 设置名称 values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType) // 设置类型 values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1000); values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()); values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES) // 文件保存路径, 其它 DIRECTORY_PICTURES、DIRECTORY_MOVIES、DIRECTORY_MUSIC values.put(MediaStore.Images.Media.IS_PENDING, true) val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) // 存储数据 this.sendLog("新图片地址:$uri") // content://media/external/images/media/81 if(uri != null){ with( contentResolver.openOutputStream(uri)){ saveImageToStream(bitmap, compressFormat, this) values.put(MediaStore.Images.Media.IS_PENDING, false) // 通知一下数据处理完成 contentResolver.update(uri, values, null, null) this.sendLog("导入图片 $uri") } else { // 4.2 如果是旧的系统,使用文件处理文件 val directoryPath = "${Environment.getExternalStorageDirectory().path}/${Environment.DIRECTORY_PICTURES}" val directory = File(directoryPath) if(!directory.exists()){ directory.mkdirs() val filePath = "${directoryPath}/${fileName}.jpg" val file = File(filePath) saveImageToStream(bitmap, compressFormat, FileOutputStream(file)) var intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE).apply { data = Uri.fromFile(file) // 如果有多个图片,相册中只显示一个图片 [email protected](intent) this.sendLog("导入图片 $filePath") 5. 进行图片文件复制 private fun saveImageToStream(bitmap: Bitmap, compressFormat: Bitmap.CompressFormat, outputStream: OutputStream?) { with(outputStream){ val success = bitmap.compress(compressFormat, 100, this) // 我的辅助函数, val id = this.activeStore.putActive { val uri = it?.data [email protected]("选择文件:$uri") // 开始选择文件 val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) intent.addCategory(Intent.CATEGORY_OPENABLE) intent.type = "*/*" startActivityForResult(intent, id)

    要求整个设备权限

    // 1. 设置 AndroidManifest.xml <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" /> <!-- ignore 不加的话,系统会提示用户谨慎同意 --> // 2. 判断是否已经有权限了 if(Build.VERSION.SDK_INT < Build.VERSION_CODES.R || Environment.isExternalStorageManager()) { Toast.makeText(this, "已获得访问所有文件权限", Toast.LENGTH_SHORT).show() return // 3. 提示用户同意给权限 val builder = AlertDialog.Builder(this).setMessage("本程序需要您同意允许访问所有文件权限") .setPositiveButton("确定") { _, _ -> // 4. 进入文件授权设置界面,由用户进行设置 val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) this.startActivity(intent) builder.show()

    第三方应用配合使用

    第三方使用可能会要求旧版本中的传文件路径的函数。可以使用新版本的函数读取文件并保存至 context.getExternalFilesDir() 目录下,再行到文件路径,并将路径传入至第三方SDK中

    修改第三方应用文件

    默认信息下不允许第三方保存的图片等,需要请示权限。并且提示了一次申请多个文件的处理权限。这样删除、编辑等不需要一个一个文件进行申请了

    1. Build.VERSION_CODES.R 之后的版本
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                val urisToModify = listAllImage()
                // createWriteRequest 多文件写入, 
                // createFavoriteRequest 多文件收藏, 
                // createTrashRequest 多文件移到回收站, 
                // createDeleteRequest 多文件删除
                val editPendingIntent = MediaStore.createWriteRequest(contentResolver, urisToModify)
                startIntentSenderForResult(editPendingIntent.intentSender, 1243, null, 0, 0, 0)
    2. 之前的版本
    try {
        contentResolver.openFileDescriptor(imageContentUri, "w")?.use {
            Toast.makeText(this, "现在可以修改图片的灰度了", Toast.LENGTH_SHORT).show()
    } catch (securityException: SecurityException) { 
        3. 使用异常来判断是不是因为权限问题
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            4. 如果不是权限问题,直接再次抛出异常
            val recoverableSecurityException = securityException as?
                RecoverableSecurityException ?:
                throw RuntimeException(securityException.message, securityException)
            5. 申请权限,旧版本中只能一个一个文件申请
            val intentSender = recoverableSecurityException.userAction.actionIntent.intentSender
            intentSender?.let {
                startIntentSenderForResult(intentSender, IMAGE_REQUEST_CODE, null, 0, 0, 0, null)
        } else {
            throw RuntimeException(securityException.message, securityException)
    

    可以使用 Thread 子类,或直接使用函数 thread

    # 继承 Thread
    class Xthread : Thread() {
        override fun run() {
            val id = Thread.currentThread().id
            Log.d("测试", "thread $id")
    val thread = Xthread()
    Thread(thread).start()
    # 辅助函数
    thread {
        val id = Thread.currentThread().id
        Log.d("测试", "thread $id")
    

    使用 Handle 发送消息至 UI 线程

            // 1. 定义一个消息泵消息处理函数
            val handler = object: Handler(){
                override fun handleMessage(msg: Message){
                    val id = Thread.currentThread().id
                    Log.d("测试", "消息泵线程 $id")
            // 2. 在主线程中
            val id = Thread.currentThread().id
            Log.d("测试", "主线程 $id")
            // 3. 启动线程
          thread {
              val id = Thread.currentThread().id
              Log.d("测试", "后台线程 $id")
              // 4. 发送消息至消息泵中
              val msg = Message() // todo: 设置一个 msg 属性
              handler.sendMessage(msg)
    
    高级 handle
  • 每个线程中可以调用 Looper.perpare() 创建一个 messagequeue,
  • 定义一个 Handler 对象处理当前线程中接收到的 message,
  • 在其它线程中调用 handler 对的 sendMessage 发送消息至 messagequeue 中
  • Activity.runOnUiThread

    在 Activity 中

    thread { // 线程部分 runOnUiThread { // 切换至 ui 线程

    View.Post / View.postDelayed

       thread {
                val view = [email protected]<View>(android.R.id.content).rootView
                view.post { /* 在 ui 线程中运行 */ } 
                view.postDelayed( { /* 在 ui 线程中运行 */}, 3000 ) 
    

    整合前后台线程处理

    封装了 Handler, 不过已经不再推荐使用

    // 传入参数,进度显示,返回值 class ThreadTask : AsyncTask<Unit, Int, Boolean>(){ /** 执行任务, 后台线程中执行 */ override fun doInBackground(vararg params: Unit?): Boolean { // 通知后台进度更新 this.publishProgress(1) return true /** 执行任务前调用, UI 上下文 */ override fun onPreExecute() { /** 更新进度, UI 上下文 */ override fun onProgressUpdate(vararg values: Int?) { /** 任务执行完成后调用, UI 上下文 */ override fun onPostExecute(result: Boolean){ Log.d("测试", 任务完成) ThreadTask().execute()

    Service

    <!-- 1. 添加权限 -->
    <service android:name=".MyService" android:enabled="true" android:exported="true"></service>
    <!-- 2. 现在一个 Service -->
    class MyService : Service() {
        override fun onBind(intent: Intent): IBinder {
            // return this.reportBinder , 不需要,抛出异常即可
        override fun onCreate() {
            super.onCreate()
            this.showLog("onCreate, 第一次 startService 时执行")
        override fun onStartCommand( intent: Intent?, flags: Int, startId: Int ): Int {
            this.showLog("onStartCommand, 每次 startService 时执行")
            thread{
                Thread.sleep(3000)
                this.stopSelf() // 从自身关闭
            return super.onStartCommand(intent, flags, startId)
        private fun showLog(message: String) {
            Log.d("测试", message)
        override fun onDestroy() {
            super.onDestroy()
            this.showLog("onDestroy")
    <!-- 3. 启动或是停止 -->
    val intent = Intent(this, MyService::class.java)
    this.startService(intent)
    this.stopService(intent)
    

    Service 与 Activity 通讯

    <!-- 1. 实现一个 Binder --> class ReportBinder : Binder() {} <!-- 2. 在 Service 中创建一个 binder 实例, 并在 onBind 中返回 --> class MyService : Service() { val reportBinder: ReportBinder = ReportBinder() override fun onBind(intent: Intent): IBinder { return this.reportBinder <!-- 3. 在 Activity 中实现一个 ServiceConnection, 在 onServiceConnected 中能得到 Service 中的 binder 对象 --> private val connection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName, service: IBinder) { val reportBinder = service as ReportBinder <!-- 4. 启动或是停止服务并会传入 connection --> val intent = Intent(this, MyService::class.java) this.bindService(intent, this.connection, Context.BIND_AUTO_CREATE) // 使用 BIND_AUTO_CREATE, 自动创建 MyService 并调用 onCreate this.unbindService(this.connection) // 停止服务

    前台Service

    默认应用服务在应用关闭后服务也会被关闭。可以创建前台服务,会在任务栏中显示一个通知。创建过程与通知也很像