简述: 关注我的Kotlin浅谈系列文章的小伙伴就知道关于Kotlin语法篇的内容已经发布了一些。然后就会有小伙伴问了一直都在讲语法是否来一波实战了,毕竟一切一切的学习都是为了解决实际问题的,所以准备来一波Kotlin实战篇,主要是用Kotlin来实现一些常见的功能和需求。实现同一个功能相比Java实现你也许会更钟爱于使用kotlin。
- 1、为什么要用Kotlin去实现Android中的自定义View?
- 2、为什么要去自定义View实现图片圆角定制化?
- 3、实现该功能需要具备的知识
- 4、图片圆角定制化需要满足哪些需求
- 5、实现原理和思路分析
- 6、自定义View中Java和Kotlin实现的对比
- 7、具体的代码实现
一、为什么要用Kotlin去实现Android中的自定义View?
针对这个问题的回答一般是给正在学习Kotlin或者Kotlin学习的新手而言,如果是刚刚学习Kotlin的时候,让你去用Kotlin实现一个自定义View,可能会有些不习惯,比如在Kotlin定义View的构造器重载怎么实现?是否要像Java暴露很多的set方法属性给外部调用,然后重绘页面呢?由于这是第一篇Kotlin实战篇,也就比较简单主要针对新手。
二、为什么要去自定义View实现图片圆角定制化?
实现图片圆形和圆角这个需求有很多种方式,经过开发试验最终比较稳的还是自定义View来实现。图片圆形或者圆角在一些图片加载框架中就集成好了,比如Glide中就有BitmapTransformation,开发者可以去继承BitmapTransformation,然后去实现Bitmap绘制逻辑在图片层面来达到图片圆角或者圆形的效果。有两点原因让我放弃使用它:
第一,单从面向对象的角度,库单一职责来说,图片加载库就是负责从网络源加载图片的,至于这个ImageView长得什么形状,则是通过ImageView来呈现的。
第二, 使用自定义BitmapTransformation来定义形状发现有bug,就是一张来自网络端的图片当它没有加载完成的时候是无法拿到图片尺寸的,而在BitmapTransformation中需要拿到图片宽和高。所以用post,Runnable机制等待加载完毕后就去定义形状,这样的实现在大部分场景是可以满足的。但是在一个需要刷新的列表中就会明显发现,每次刷新图片去加载,图片会有空白的过程很影响体验。
三、实现该功能需要具备知识
- 1、Kotlin中基本语法知识
- 2、Kotlin中自定义属性访问器
- 3、Kotlin中默认值参数实现构造器函数重载以及@JvmOverloads注解的使用
- 4、Kotlin标准库中常见的apply,run,with函数的使用
- 5、Kotlin中默认值参数函数的使用
- 6、自定义View的基本知识
- 7、Path的使用
- 8、Matrix的使用
- 9、BitmapShader的使用
四、图片圆角定制化需要满足哪些需求
- 1、支持图片圆形的定制化
- 2、支持图片圆角以及每个角的X,Y方向值的定制化
- 3、支持形状边框宽度颜色的定制化
- 4、支持图片圆角或者圆形右上角消息圆点定制化(一般用于圆形或者圆角头像)
五、自定义View实现的原理和思路分析
这个自定义View实现原理很简单,主要有三个比较重要的点,第一就是构建定义圆角矩形的Path;第二是利用Matrix矩阵变换按比例缩小或放大使得图片大小和ImageView大小保持一致;第三就是使用BitmapShader对已经定义好的path,用带shader的画笔进行渲染。
六、自定义View中Java和Kotlin实现的对比
- 1、自定义View构造器重载对比
java实现,这样的写法是java实现自定义View常用套路
public class PrettyImageView extends ImageView {
public PrettyImageView(Context context) {
this(context, null);
}
public PrettyImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PrettyImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
复制代码
kotlin实现,使用到了之前博客讲过的默认值参数实现函数重载以及使用@JvmOverloads注解是为了在Java中可以调用Kotlin中定义重载构造器方法。(这两个知识点都是之前博客有专门分析包括其原理)
class PrettyImageView @JvmOverloads constructor(context: Context, attributeSet: AttributeSet? = null, defAttrStyle: Int = 0)
: ImageView(context, attributeSet, defAttrStyle) {
}
复制代码
- 2、自定义View 属性改变的setter暴露
java实现,需要实现对应属性的setter方法,然后内部调用invalidate重绘
public class PrettyImageView extends ImageView {
private boolean mIsShowBorder;
private float mBorderWidth;
private int mBorderColor;
private boolean mIsShowDot;
private float mDotRadius;
private int mDotColor;
public PrettyImageView(Context context) {
this(context, null);
}
public PrettyImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PrettyImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setmIsShowBorder(boolean mIsShowBorder) {
this.mIsShowBorder = mIsShowBorder;
invalidate();
}
public void setmBorderWidth(float mBorderWidth) {
this.mBorderWidth = mBorderWidth;
invalidate();
}
public void setmBorderColor(int mBorderColor) {
this.mBorderColor = mBorderColor;
invalidate();
}
public void setmIsShowDot(boolean mIsShowDot) {
this.mIsShowDot = mIsShowDot;
invalidate();
}
public void setmDotRadius(float mDotRadius) {
this.mDotRadius = mDotRadius;
invalidate();
}
public void setmDotColor(int mDotColor) {
this.mDotColor = mDotColor;
invalidate();
}
}
复制代码
Kotlin实现则不需要定义那么多setter方法,因为Kotlin中var变量就自带setter和getter方法,可以我们又想达到当重新改变值后需要调用invalidate函数。这是就需要用之前讲过自定义变量访问器。
class PrettyImageView @JvmOverloads constructor(context: Context, attributeSet: AttributeSet? = null, defAttrStyle: Int = 0)
: ImageView(context, attributeSet, defAttrStyle) {
private var mBorderWidth: Float = 20f
set(value) {
field = value
invalidate()
}
private var mBorderColor: Int = Color.parseColor("#ff9900")
set(value) {
field = value
invalidate()
}
private var mShowBorder: Boolean = true
set(value) {
field = value
invalidate()
}
private var mShowCircleDot: Boolean = false
set(value) {
field = value
invalidate()
}
private var mCircleDotColor: Int = Color.RED
set(value) {
field = value
invalidate()
}
private var mCircleDotRadius: Float = 20f
set(value) {
field = value
invalidate()
}
}
复制代码
七、具体代码实现
- 1、开放的自定义View属性
开放属性name
开放属性含义
shape_type
形状类型,目前只有圆角和圆形两种类型
left_top_radiusX
左上角X轴方向半径
left_top_radiusY
左上角Y轴方向半径
right_top_radiusX
右上角X轴方向半径
right_top_radiusY
右上角Y轴方向半径
right_bottom_radiusX
右下角X轴方向半径
right_bottom_radiusY
右下角Y轴方向半径
left_bottom_radiusX
左下角X轴方向半径
left_bottom_radiusY
左下角Y轴方向半径
show_border
是否显示边框
border_width
边框宽度
border_color
边框颜色
show_circle_dot
是否显示右上角圆点
circle_dot_color
右上角圆点颜色
circle_dot_radius
右上角圆点半径
-
2、attrs.xml的定义声明
-
3、具体实现代码
class PrettyImageView @JvmOverloads constructor(context: Context, attributeSet: AttributeSet? = null, defAttrStyle: Int = 0) : ImageView(context, attributeSet, defAttrStyle) {
enum class ShapeType { SHAPE_CIRCLE, SHAPE_ROUND } //defAttr var private var mShapeType: ShapeType = ShapeType.SHAPE_CIRCLE set(value) { field = value invalidate() } private var mBorderWidth: Float = 20f set(value) { field = value invalidate() } private var mBorderColor: Int = Color.parseColor("#ff9900") set(value) { field = value invalidate() } private var mLeftTopRadiusX: Float = 0f set(value) { field = value invalidate() } private var mLeftTopRadiusY: Float = 0f set(value) { field = value invalidate() } private var mRightTopRadiusX: Float = 0f set(value) { field = value invalidate() } private var mRightTopRadiusY: Float = 0f set(value) { field = value invalidate() } private var mLeftBottomRadiusX: Float = 0f set(value) { field = value invalidate() } private var mLeftBottomRadiusY: Float = 0f set(value) { field = value invalidate() } private var mRightBottomRadiusX: Float = 0f set(value) { field = value invalidate() } private var mRightBottomRadiusY: Float = 0f set(value) { field = value invalidate() } private var mShowBorder: Boolean = true set(value) { field = value invalidate() } private var mShowCircleDot: Boolean = false set(value) { field = value invalidate() } private var mCircleDotColor: Int = Color.RED set(value) { field = value invalidate() } private var mCircleDotRadius: Float = 20f set(value) { field = value invalidate() } //drawTools var private lateinit var mShapePath: Path private lateinit var mBorderPath: Path private lateinit var mBitmapPaint: Paint private lateinit var mBorderPaint: Paint private lateinit var mCircleDotPaint: Paint private lateinit var mMatrix: Matrix //temp var private var mWidth: Int = 200//View的宽度 private var mHeight: Int = 200//View的高度 private var mRadius: Float = 100f//圆的半径 init { initAttrs(context, attributeSet, defAttrStyle)//获取自定义属性的值 initDrawTools()//初始化绘制工具 } private fun initAttrs(context: Context, attributeSet: AttributeSet?, defAttrStyle: Int) { val array = context.obtainStyledAttributes(attributeSet, R.styleable.PrettyImageView, defAttrStyle, 0) (0..array.indexCount) .asSequence() .map { array.getIndex(it) } .forEach { when (it) { R.styleable.PrettyImageView_shape_type -> mShapeType = when { array.getInt(it, 0) == 0 -> ShapeType.SHAPE_CIRCLE array.getInt(it, 0) == 1 -> ShapeType.SHAPE_ROUND else -> ShapeType.SHAPE_CIRCLE } R.styleable.PrettyImageView_border_width -> mBorderWidth = array.getDimension(it, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4f, resources.displayMetrics)) R.styleable.PrettyImageView_border_color -> mBorderColor = array.getColor(it, Color.parseColor("#ff0000")) R.styleable.PrettyImageView_left_top_radiusX -> mLeftTopRadiusX = array.getDimension(it, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0f, resources.displayMetrics)) R.styleable.PrettyImageView_left_top_radiusY -> mLeftTopRadiusY = array.getDimension(it, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0f, resources.displayMetrics)) R.styleable.PrettyImageView_left_bottom_radiusX -> mLeftBottomRadiusX = array.getDimension(it, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0f, resources.displayMetrics)) R.styleable.PrettyImageView_left_bottom_radiusY -> mLeftBottomRadiusY = array.getDimension(it, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0f, resources.displayMetrics)) R.styleable.PrettyImageView_right_bottom_radiusX -> mRightBottomRadiusX = array.getDimension(it, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0f, resources.displayMetrics)) R.styleable.PrettyImageView_right_bottom_radiusY -> mRightBottomRadiusY = array.getDimension(it, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0f, resources.displayMetrics)) R.styleable.PrettyImageView_right_top_radiusX -> mRightTopRadiusX = array.getDimension(it, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0f, resources.displayMetrics)) R.styleable.PrettyImageView_right_top_radiusY -> mRightTopRadiusY = array.getDimension(it, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0f, resources.displayMetrics)) R.styleable.PrettyImageView_show_border -> mShowBorder = array.getBoolean(it, false) R.styleable.PrettyImageView_show_circle_dot -> mShowCircleDot = array.getBoolean(it, false) R.styleable.PrettyImageView_circle_dot_color -> mCircleDotColor = array.getColor(it, Color.parseColor("#ff0000")) R.styleable.PrettyImageView_circle_dot_radius -> mCircleDotRadius = array.getDimension(it, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics)) } } array.recycle() } private fun initDrawTools() { mBitmapPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {//最终绘制图片的画笔,需要设置BitmapShader着色器,从而实现把图片绘制在不同形状图形上 style = Paint.Style.FILL } mBorderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {//绘制边框画笔 style = Paint.Style.STROKE color = mBorderColor strokeCap = Paint.Cap.ROUND strokeWidth = mBorderWidth } mCircleDotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {//绘制右上角圆点画笔 style = Paint.Style.FILL color = mCircleDotColor } mShapePath = Path()//描述形状轮廓的path路径 mBorderPath = Path()//描述图片边框轮廓的path路径 mMatrix = Matrix()//用于缩放图片的矩阵 scaleType = ScaleType.CENTER_CROP } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {//View的测量 super.onMeasure(widthMeasureSpec, heightMeasureSpec) if (mShapeType == ShapeType.SHAPE_CIRCLE) { mWidth = Math.min(measuredWidth, measuredHeight) mRadius = mWidth / 2.0f setMeasuredDimension(mWidth, mWidth) } else { mWidth = measuredWidth mHeight = measuredHeight setMeasuredDimension(mWidth, mHeight) } } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {//确定了最终View的尺寸 super.onSizeChanged(w, h, oldw, oldh) mBorderPath.reset() mShapePath.reset() when (mShapeType) { ShapeType.SHAPE_ROUND -> { mWidth = w mHeight = h buildRoundPath() } ShapeType.SHAPE_CIRCLE -> { buildCirclePath() } } } private fun buildCirclePath() {//构建圆形类型的Path路径 if (!mShowBorder) {//绘制不带边框的圆形实际上只需要把一个圆形扔进path即可 mShapePath.addCircle(mRadius, mRadius, mRadius, Path.Direction.CW) } else {//绘制带边框的圆形需要把内部圆形和外部圆形边框都要扔进path mShapePath.addCircle(mRadius, mRadius, mRadius - mBorderWidth, Path.Direction.CW) mBorderPath.addCircle(mRadius, mRadius, mRadius - mBorderWidth / 2.0f, Path.Direction.CW) } } private fun buildRoundPath() {//构建圆角类型的Path路径 if (!mShowBorder) {//绘制不带边框的圆角实际上只需要把一个圆角矩形扔进path即可 floatArrayOf(mLeftTopRadiusX, mLeftTopRadiusY, mRightTopRadiusX, mRightTopRadiusY, mRightBottomRadiusX, mRightBottomRadiusY, mLeftBottomRadiusX, mLeftBottomRadiusY).run { mShapePath.addRoundRect(RectF(0f, 0f, mWidth.toFloat(), mHeight.toFloat()), this, Path.Direction.CW) } } else {//绘制带边框的圆角实际上只需要把一个圆角矩形和一个圆角矩形的变量都扔进path即可 floatArrayOf(mLeftTopRadiusX - mBorderWidth / 2.0f, mLeftTopRadiusY - mBorderWidth / 2.0f, mRightTopRadiusX - mBorderWidth / 2.0f, mRightTopRadiusY - mBorderWidth / 2.0f, mRightBottomRadiusX - mBorderWidth / 2.0f, mRightBottomRadiusY - mBorderWidth / 2.0f, mLeftBottomRadiusX - mBorderWidth / 2.0f, mLeftBottomRadiusY - mBorderWidth / 2.0f).run { mBorderPath.addRoundRect(RectF(mBorderWidth / 2.0f, mBorderWidth / 2.0f, mWidth.toFloat() - mBorderWidth / 2.0f, mHeight.toFloat() - mBorderWidth / 2.0f), this, Path.Direction.CW) } floatArrayOf(mLeftTopRadiusX - mBorderWidth, mLeftTopRadiusY - mBorderWidth, mRightTopRadiusX - mBorderWidth, mRightTopRadiusY - mBorderWidth, mRightBottomRadiusX - mBorderWidth, mRightBottomRadiusY - mBorderWidth, mLeftBottomRadiusX - mBorderWidth, mLeftBottomRadiusY - mBorderWidth).run { mShapePath.addRoundRect(RectF(mBorderWidth, mBorderWidth, mWidth.toFloat() - mBorderWidth, mHeight.toFloat() - mBorderWidth), this, Path.Direction.CW) } } } override fun onDraw(canvas: Canvas?) {//由于经过以上根据不同逻辑构建了boderPath和shapePath,path中已经储存相应的形状,现在只需要把相应shapePath中形状用带BitmapShader画笔绘制出来,boderPath用普通画笔绘制出来即可 drawable ?: return mBitmapPaint.shader = getBitmapShader()//获得相应的BitmapShader着色器对象 when (mShapeType) { ShapeType.SHAPE_CIRCLE -> { if (mShowBorder) { canvas?.drawPath(mBorderPath, mBorderPaint)//绘制圆形图片边框path } canvas?.drawPath(mShapePath, mBitmapPaint)//绘制圆形图片形状path if (mShowCircleDot) { drawCircleDot(canvas)//绘制圆形图片右上角圆点 } } ShapeType.SHAPE_ROUND -> { if (mShowBorder) { canvas?.drawPath(mBorderPath, mBorderPaint)//绘制圆角图片边框path } canvas?.drawPath(mShapePath, mBitmapPaint)//绘制圆角图片形状path } } } private fun drawCircleDot(canvas: Canvas?) { canvas?.run { drawCircle((mRadius + mRadius * (Math.sqrt(2.0) / 2.0f)).toFloat(), (mRadius - mRadius * (Math.sqrt(2.0) / 2.0f)).toFloat(), mCircleDotRadius, mCircleDotPaint) } } private fun getBitmapShader(): BitmapShader { val bitmap = drawableToBitmap(drawable) return BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP).apply { var scale = 1.0f if (mShapeType == ShapeType.SHAPE_CIRCLE) { scale = (mWidth * 1.0f / Math.min(bitmap.width, bitmap.height)) } else if (mShapeType == ShapeType.SHAPE_ROUND) { // 如果图片的宽或者高与view的宽高不匹配,计算出需要缩放的比例;缩放后的图片的宽高,一定要大于我们view的宽高;所以我们这里取大值; if (!(width == bitmap.width && width == bitmap.height)) { scale = Math.max(width * 1.0f / bitmap.width, height * 1.0f / bitmap.height) } } // shader的变换矩阵,我们这里主要用于放大或者缩小 mMatrix.setScale(scale, scale) setLocalMatrix(mMatrix) } } private fun drawableToBitmap(drawable: Drawable): Bitmap { if (drawable is BitmapDrawable) { return drawable.bitmap } return Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888).apply { drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) drawable.draw(Canvas(this@apply)) } } companion object { private const val STATE_INSTANCE = "state_instance" private const val STATE_INSTANCE_SHAPE_TYPE = "state_shape_type" private const val STATE_INSTANCE_BORDER_WIDTH = "state_border_width" private const val STATE_INSTANCE_BORDER_COLOR = "state_border_color" private const val STATE_INSTANCE_RADIUS_LEFT_TOP_X = "state_radius_left_top_x" private const val STATE_INSTANCE_RADIUS_LEFT_TOP_Y = "state_radius_left_top_y" private const val STATE_INSTANCE_RADIUS_LEFT_BOTTOM_X = "state_radius_left_bottom_x" private const val STATE_INSTANCE_RADIUS_LEFT_BOTTOM_Y = "state_radius_left_bottom_y" private const val STATE_INSTANCE_RADIUS_RIGHT_TOP_X = "state_radius_right_top_x" private const val STATE_INSTANCE_RADIUS_RIGHT_TOP_Y = "state_radius_right_top_y" private const val STATE_INSTANCE_RADIUS_RIGHT_BOTTOM_X = "state_radius_right_bottom_x" private const val STATE_INSTANCE_RADIUS_RIGHT_BOTTOM_Y = "state_radius_right_bottom_y" private const val STATE_INSTANCE_RADIUS = "state_radius" private const val STATE_INSTANCE_SHOW_BORDER = "state_radius_show_border" } //View State Save override fun onSaveInstanceState(): Parcelable = Bundle().apply { putParcelable(STATE_INSTANCE, super.onSaveInstanceState()) putInt(STATE_INSTANCE_SHAPE_TYPE, when (mShapeType) { ShapeType.SHAPE_CIRCLE -> 0 ShapeType.SHAPE_ROUND -> 1 }) putFloat(STATE_INSTANCE_BORDER_WIDTH, mBorderWidth) putInt(STATE_INSTANCE_BORDER_COLOR, mBorderColor) putFloat(STATE_INSTANCE_RADIUS_LEFT_TOP_X, mLeftTopRadiusX) putFloat(STATE_INSTANCE_RADIUS_LEFT_TOP_Y, mLeftTopRadiusY) putFloat(STATE_INSTANCE_RADIUS_LEFT_BOTTOM_X, mLeftBottomRadiusX) putFloat(STATE_INSTANCE_RADIUS_LEFT_BOTTOM_Y, mLeftBottomRadiusY) putFloat(STATE_INSTANCE_RADIUS_RIGHT_TOP_X, mRightTopRadiusX) putFloat(STATE_INSTANCE_RADIUS_RIGHT_TOP_Y, mRightTopRadiusY) putFloat(STATE_INSTANCE_RADIUS_RIGHT_BOTTOM_X, mRightBottomRadiusX) putFloat(STATE_INSTANCE_RADIUS_RIGHT_BOTTOM_Y, mRightBottomRadiusY) putFloat(STATE_INSTANCE_RADIUS, mRadius) putBoolean(STATE_INSTANCE_SHOW_BORDER, mShowBorder) } //View State Restore override fun onRestoreInstanceState(state: Parcelable?) { if (state !is Bundle) { super.onRestoreInstanceState(state) return } with(state) { super.onRestoreInstanceState(getParcelable(STATE_INSTANCE)) mShapeType = when { getInt(STATE_INSTANCE_SHAPE_TYPE) == 0 -> ShapeType.SHAPE_CIRCLE getInt(STATE_INSTANCE_SHAPE_TYPE) == 1 -> ShapeType.SHAPE_ROUND else -> ShapeType.SHAPE_CIRCLE } mBorderWidth = getFloat(STATE_INSTANCE_BORDER_WIDTH) mBorderColor = getInt(STATE_INSTANCE_BORDER_COLOR) mLeftTopRadiusX = getFloat(STATE_INSTANCE_RADIUS_LEFT_TOP_X) mLeftTopRadiusY = getFloat(STATE_INSTANCE_RADIUS_LEFT_TOP_Y) mLeftBottomRadiusX = getFloat(STATE_INSTANCE_RADIUS_LEFT_BOTTOM_X) mLeftBottomRadiusY = getFloat(STATE_INSTANCE_RADIUS_LEFT_BOTTOM_Y) mRightTopRadiusX = getFloat(STATE_INSTANCE_RADIUS_RIGHT_TOP_X) mRightTopRadiusY = getFloat(STATE_INSTANCE_RADIUS_RIGHT_TOP_Y) mRightBottomRadiusX = getFloat(STATE_INSTANCE_RADIUS_RIGHT_BOTTOM_X) mRightBottomRadiusY = getFloat(STATE_INSTANCE_RADIUS_RIGHT_BOTTOM_Y) mRadius = getFloat(STATE_INSTANCE_RADIUS) mShowBorder = getBoolean(STATE_INSTANCE_SHOW_BORDER) } } 复制代码
}
项目GitHub地址
运行效果截图:
欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不定期翻译一篇Kotlin国外技术文章。如果你也喜欢Kotlin,欢迎加入我们~~~