kotlin获取context_Kotlin:那些实战爆炸好用的语法糖!

前言

本文归纳了 Kotlin 在实际需求应用的爆炸好用的语法糖:

1、判断 View 是否在屏幕上

2、协程和视图生命周期相绑定

3、获取 DecorView

4、将 px 值转换成 dp 值

5、替代Builder模式

6、打印列表、map

7、将 data 类转换成 map

8、RecyclerView表项点击监听器

1 判断 View 是否在屏幕上
val View.inScreen: Boolean
    get() {
        // 获取屏幕宽度
        val screenWidth = context?.resources?.displayMetrics?.widthPixels ?: 0
        // 获取屏幕高度
        val screenHeight = context?.resources?.displayMetrics?.heightPixels ?: 0
        // 构建屏幕矩形
        val screenRect = Rect(0, 0, screenWidth, screenHeight)
        val array = IntArray(2)
        // 获取视图矩形
        getLocationOnScreen(array)
        val viewRect = Rect(array[0], array[1], array[0] + width, array[1] + height)
        // 判断屏幕和视图矩形是否有交集
        return screenRect.intersect(viewRect)
    }

为View增加一个扩展方法,返回布尔值,在其中获取视图和屏幕矩形区域,然后判断是否有交集,若有则表示视图出现在屏幕上。

2 协程和视图生命周期相绑定

若协程用于异步加载一张图片,这张图片显示在 ImageView 上。当 ImageView 所在界面已被销毁时,得及时取消协程的加载任务,以释放资源:

fun Job.autoDispose(view: View) {
    val isAttached = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && view.isAttachedToWindow || view.windowToken != null
    if (!isAttached){
        cancel()
    }

    // 监听视图生命周期
    val listener = object : View.OnAttachStateChangeListener {
        // 在视图生命周期结束时取消协程
        override fun onViewDetachedFromWindow(v: View?) {
            cancel()
            v?.removeOnAttachStateChangeListener(this)
        }

        override fun onViewAttachedToWindow(v: View?) = Unit
    }

    view.addOnAttachStateChangeListener(listener)
    invokeOnCompletion {
        view.removeOnAttachStateChangeListener(listener)
    }
}

为Job扩展方式,传入 View 类型的参数,表示该Job和该 View 的生命周期绑定,当 View 生命周期结束时,自动取消协程。

3 获取 DecorView
val Activity.decorView: FrameLayout?
    get() = (takeIf { !isFinishing && !isDestroyed }?.window?.decorView) as? FrameLayout

为Activity扩展属性decorView,它的类型是FrameLayout。在get()方法中定义如何获取该属性。

当Activity生命周期没有结束的时候,获取它的Window,再从从Window中获取DecorView,强转为FrameLayout。

用同样的思路可以获取Activity的Content View:

val Activity.contentView: FrameLayout?
    get() = takeIf { !isFinishing && !isDestroyed }?.window?.decorView?.findViewById(android.R.id.content)
4 将 px 值转换成 dp 值

在非 xml 环境下构建布局,需要将 px 转换为 dp 来进行多屏幕适配。Java 的做法是在Util类中新增一个静态函数。利用 Kotlin 的扩展属性可以更简洁地实现:

val Int.dp: Int
    get() {
        return TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP,
            this.toFloat(),
            Resources.getSystem().displayMetrics
        ).toInt()
    }

为 Int 扩展一个属性dp,它的类型是 Int。在get()中定义该属性的取值算法。

然后就可以像这样动态地将 Int 值 dp 化:

viewGroup.addView( textView, LayoutParam( 40.dp, 50.dp ) )

5 替代Builder模式

当构造复杂对象时,需要很多参数,如果将所有参数都通过一个构造函数来传递,缺乏灵活性,但如果重载若干个带有不同参数的构造函数,代码就变得臃肿。Builder 模式可以简化构建过程。

在 Java 中 Builder模式 代码如下:

public class Person {
    //'必选参数'
    private String name;
    //'以下都是可选参数'
    private int gender;
    private int age;
    private int height;
    private int weight;

    //'私有构造函数,限制必须通过构造者构建对象'
    private Person(Builder builder) {
        this.name = builder.name;
        this.gender = builder.gender;
        this.age = builder.age;
        this.height = builder.height;
        this.weight = builder.weight;
    }

    //'构造者'
    public static class Builder {
        private String name;
        private int gender;
        private int age;
        private int height;
        private int weight;

        //'必选参数必须在构造函数中传入'
        public Builder(String name) {
            this.name = name;
        }

        //'以下是每个非必要属性的设值函数,它返回构造者本身用于链式调用'
        public Builder age(int age) {
            this.age = age;
            return this;
        }

        public Builder gender(int gender) {
            this.gender = gender;
            return this;
        }

        public Builder height(int height) {
            this.height = height;
            return this;
        }

        public Builder weight(int weight) {
            this.weight = weight;
            return this;
        }

        //'构建对象'
        public Person build() {
            return new Person(this);
        }
    }

然后就可以像这样构建Person实例:

//'使用 Builder模式'
Person p = new Person.Builder("taylor")
            .age(50)
            .gender(1)
            .weight(43)
            .build();

//'使用构造函数'
Person p2 = new Person("taylor", 50, 1, 0, 43);

对比之下,Builder模式 有两个优势:

  1. 为参数标注语义:在Builder模式中,每个属性的赋值都是一个函数,函数名标注了属性语义。而直接使用构造函数时,很难分辨50,43哪个是年龄,哪个是体重。

  2. 可选参数:Builder模式中,除了必选参数,其他参数是可选的。但直接使用构造函数必须为所有参数赋值,比如上例中第四个参数身高被赋值为0。

但 Builder模式 也有代价,新增了一个中间类Builder。

使用 Kotlin 的命名参数+参数默认值+数据类语法,在没有任何副作用的情况下就能实现 Builder模式:

//'将Person定义为数据类'
data class Person(
    var name: String,
    //'为以下可选参数设置默认值'
    var gender: Int = 1,
    var age: Int= 0,
    var height: Int = 0,
    var weight: Int = 0
)

//'使用命名参数构建Person实例'
val p  = Person(name = “taylor”,gender = 1,weight = 43)

关于数据类、参数默认值、命名参数更详细的介绍可以点击这里

https://juejin.im/post/6844903844812423182

如果想增加参数约束条件可以调用require()方法:

data class Person(
    var name: String,
    var gender: Int = 1,
    var age: Int= 0,
    var height: Int = 0,
    var weight: Int = 0
){
    //'在构造函数被调用的时候执行参数合法检查'
    init {
        require(name.isNotEmpty()){”name cant be empty“}
    }
}

此时如果像下面这样构造 Person,则会抛出异常:

val p = Person(name="",gender = 1)
java.lang.IllegalArgumentException: name cant be empty

6 打印列表、map

调试程序时,经常需要打印列表内容,通常会这样打印:

for (String str:list) {
    Log.v("test", "str="+str);
}

不同业务界面的数据类型不同,为了调试,这样的 for 循环就会散落在各处,而且列表内容会分若干条 log 输出,中间极有可能被别的log打断。

有没有一个函数可以打印包含任意数据类型的列表,并将列表内容组织成更具可读性的字符串?

用 Kotlin  的扩展函数+泛型+高阶函数就能优雅地做到:

fun  Collection.print(map: (T) -> String) =
    StringBuilder("\n[").also { sb ->
        //'遍历集合元素,通过 map 表达式将元素转换成感兴趣的字串,并独占一行'
        this.forEach { e -> sb.append("\n\t${map(e)},") }
        sb.append("\n]")
    }.toString()

为集合的基类Collection新增一个扩展函数,它是一个高阶函数,因为它的参数是另一个函数,该函数用 lambda 表示。再把集合元素抽象成泛型。通过StringBuilder将所有集合内容拼接成一个自动换行的字符串。

写段测试代码看下效果:

data class Person(var name: String, var age: Int)

val persons = listOf(
    Person("Peter", 16),
    Person("Anna", 28),
    Person("Anna", 23),
    Person("Sonya", 39)
)

persons.print { "${it.name}_${it.age}" }.let { Log.v("test",it) }

打印结果如下:

V/test: [
        Peter_16,
        Anna_28,
        Anna_23,
        Sonya_39,
    ]

同样地,可以如法炮制一个打印 map 的扩展函数:

fun  Map.print(map: (V?) -> String): String =
    StringBuilder("\n{").also { sb ->
        this.iterator().forEach { entry ->
            sb.append("\n\t[${entry.key}] = ${map(entry.value)}")
        }
        sb.append("\n}")
    }.toString()
7 将 data 类转换成 map

有些数据类字段比较多,调试时,想把它们通通打印出来,在 Java 中,借助于 AndroidStudio 的 toString功能倒是可以方便地生成可读性很高的字串:

public class Person {
    private String name;
    private int age;

    @Override
    public String toString() {
        return ”Person{“ +
                ”name=‘“ + name + ’\” +
                ”, age=“ + age +
                ‘}’;
    }
}

但是每新建一个数据类都要手动生成一个toString()方法也挺麻烦。

利用 Kotlin 的 data class可以省去这一步,但打印效果是所有字段都在同一行中:

data class Person(var name: String, var age: Int)

Log.v(“test”, “person=${Person("Peter", 16)}”)

//输出如下:
V/test: person=Person(name=Peter, age=16)

如果字段很多,把它们都打印在一行中可读性很差。

有没有一种方法,可以读取一个类中所有的字段信息?这样我们就可以将他们组织成想要的形状。请看下面这个方法:

fun Any.ofMap() =
    //'过滤掉除data class以外的其他类'
    this::class.takeIf { it.isData }
        //'遍历类的所有成员,过滤掉成员方法,只考虑成员属性'
        ?.members?.filterIsInstance>()//'将成员属性名和值存储在Pair中'
        ?.map { it.name to it.call(this) }//'将Pair转换成map'
        ?.toMap()

为任意 Kotlin 中的类添加一个扩展函数,它的功能是将data class中所有的字段名及其对应值存在一个 map 中。其中用到的 Kotlin 语法糖如下:

  • isData是KClass中的一个属性,用于判断该类是不是一个data class。KClass是 Kotlin 中用来描述 类的类型,KClass可以通过对象::class语法获得。

  • members也是KClass中的一个属性,它包含了所有类的方法和属性。

  • filterIsInstance()是Iterable接口的扩展函数,用于过滤出集合中指定的类型。

  • to是一个infix扩展函数,它的定义如下:

public infix fun  A.to(that: B): Pair = Pair(this, that)
  • 带有infix标识的函数只允许带有一个参数,并且在调用时可以省略包裹参数的括号。这种语法叫中缀表达式

写段测试代码,结合上一节的打印 map 函数看下效果:

data class Person(var name: String, var age: Int)

Person("Peter", 16).ofMap()?.print { it.toString() }.let { Log.v("test","$it") }

测试代码先将Person实例转换成 map,然后打印 map。输出结果如下:

V/test:
{
    [age] = 16
    [name] = Peter
}

若data class嵌套会发生什么?

//'位置,嵌套在Person类中'
data class Location(var x: Int, var y: Int)
data class Person(var name: String, var age: Int, var locaton: Location? = null)

Person("Peter", 16, Location(20, 30)).ofMap()?.print { it.toString() }.let { Log.v("test", "$it") }

//'打印结果如下'
    {
        [age] = 16
        [locaton] = Location(x=20, y=30)
        [name] = Peter
    }

期望得到类似 Json 的打印效果,但输出结果还差一点。是因为将Person转化成Map时并没有将嵌套的Location也转化成键值对。

需要将ofMap()方法重构成递归调用:

fun Any.ofMap(): Map? {return this::class.takeIf { it.isData }
        ?.members?.filterIsInstance>()
        ?.map { member ->val value = member.call(this)?.let { v->//'若成员变量是data class,则递归调用ofMap(),将其转化成键值对,否则直接返回值'if (v::class.isData) v.ofMap()else v
            }
            member.name to value
        }
        ?.toMap()
}

为了让打印结果也有嵌套缩进效果,打印 Map 的函数也需要相应地重构:

/**
 * 打印 Map,生成结构化键值对子串
 * @param space 行缩进量
 */
fun  Map.print(space: Int = 0): String {
    //'生成当前层次的行缩进,用space个空格表示,当前层次每一行内容都需要带上缩进'
    val indent = StringBuilder().apply {
        repeat(space) { append(" ") }
    }.toString()
    return StringBuilder("\n${indent}{").also { sb ->
        this.iterator().forEach { entry ->
            //'如果值是 Map 类型,则递归调用print()生成其结构化键值对子串,否则返回值本身'
            val value = entry.value.let { v ->
                (v as? Map)?.print("${indent}${entry.key} = ".length) ?: v.toString()
            }
            sb.append("\n\t${indent}[${entry.key}] = $value,")
        }
        sb.append("\n${indent}}")
    }.toString()
}

写段测试代码,看看效果:

//'坐标类,嵌套在Location类中'
data class Coordinate(var x: Int, var y: Int)
//'位置类,嵌套在Person类中'
data class Location(var country: String, var city: String, var coordinate: Coordinate)
data class Person(var name: String, var age: Int, var locaton: Location? = null)

Person("Peter", 16, Location("china", "shanghai", Coordinate(10, 20))).ofMap()?.print().let { Log.v("test", "$it") }

//'打印如下'
  {
      [age] = 16,
      [locaton] =
            {
                [city] = shanghai,
                [coordinate] =
                         {
                             [x] = 10,
                             [y] = 20,
                         },
                [country] = china,
            },
      [name] = Peter,
  }
8 RecyclerView表项点击监听器
RecyclerView没有子控件点击事件监听器,那就用Kotlin扩展方法扩展一个:
//'为 RecyclerView 扩展表项点击监听器'
fun RecyclerView.setOnItemClickListener(listener: (View, Int) -> Unit) {
    //'为 RecyclerView 子控件设置触摸监听器'
    addOnItemTouchListener(object : RecyclerView.OnItemTouchListener {
        //'构造手势探测器,用于解析单击事件'
        val gestureDetector = GestureDetector(context, object : GestureDetector.OnGestureListener {
            override fun onShowPress(e: MotionEvent?) {
            }

            override fun onSingleTapUp(e: MotionEvent?): Boolean {
                //'当单击事件发生时,寻找单击坐标下的子控件,并回调监听器'
                e?.let {
                    findChildViewUnder(it.x, it.y)?.let { child ->
                        listener(child, getChildAdapterPosition(child))
                    }
                }
                return false
            }

            override fun onDown(e: MotionEvent?): Boolean {
                return false
            }

            override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
                return false
            }

            override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
                return false
            }

            override fun onLongPress(e: MotionEvent?) {
            }
        })

        override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) {

        }

        //'在拦截触摸事件时,解析触摸事件'
        override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {
            gestureDetector.onTouchEvent(e)
            return false
        }

        override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
        }
    })
}

至此,关于Kotlin的语法糖技巧讲解完毕。


本文作者

本文由作者:唐子玄授权发布

链接:https://juejin.im/post/6844904132080320526


「Carson每天带你学习一个Android知识点」,长按扫描关注公众号,我们明天见哦!

7506e08835cc5463cb7051f0759e5e04.png


最后福利:学习资料赠送

3fdee27be7d17b6b8573a343e7481b94.png
  • 福利:由本人亲自撰写 & 整理的「Android学习方法资料」
  • 数量:10名
  • 参与方式:「点击文章右下角”在看“ -> 回复截图到公众号 即可,我将从中随机抽取」99068faec853cf467bb10b5e54e5f531.png点击“在看”就能升职 & 加薪水哦!ff8593897345c10810f38927e572f010.gif
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值