美文网首页Android开发技术分享
DSL实战:仿Flutter代码布局实战

DSL实战:仿Flutter代码布局实战

作者: 珠穆朗玛小王子 | 来源:发表于2020-07-23 18:18 被阅读0次

    前言

    最近简书投稿专题好像有问题,喜欢我的文章的话,欢迎关注我或关注我的专题。

    我的第一份IT工作是Web前端,转眼已过去8年,之前在学习Flutter的时候情不自禁想起了当年苦逼的div生活,之后还略微研究了一下JetPack Compose。这两个库都是代码实现GUI,关于JetPack Compose的用法一直颇有争议,有人说在Android上,使用代码布局是技术的倒退,这一点我有一些自己的看法:

    • Android把布局和代码分离,必须说非常有眼光,相比其他平台,例如ios,Android开发者非常幸福。
    • 由于开发者对于App的性能要求越来越苛刻,使用XML布局,每次都需要XML解析成对应的View,苛刻的开发者开始寻求更优的运行速度,于是诞生了更多的GUI相关的库。

    目前在布局方面最流行的优化方法有两种:

    1. 以JetPack Compose, Flutter为代表,核心还是代码布局,但是志在简化代码;
    2. 以掌阅开源的X2C框架为例,在编译期间,直接把XML转成源文件,编译时间变长,但是仍然可以使用XML布局,修改量小。

    相信在众多开发者的努力下,将来还会有更多的优化方案。

    如果你还没有了解Kotlin与DSL相关的知识,建议先阅读上一篇:
    读书笔记:Kotlin自定义DSL语法

    正文

    既然已经了解了DSL语法。这一篇的我们就模仿JetPack Compose和Flutter的布局用法,实现一个自己的布局框架,就叫DSL-UI。

    首先贴出一段JetPack Compose的代码:

    setContent {
          Column{
            Text("A day in Shark Fin Cove")
            Text("Davenport, California")
            Text("December 2018")
        }
    }
    

    代码非常的简单,我们的目标就是实现这样一个代码布局的方式。细节上可能略微有差别。

    image

    我们就实现上图的布局。

    因为Android的布局种类比较多,特性也五花八门,为了实现简单,我们只是用LinearLayout,并改名为Column。

    首先定义Column类,从图上我们知道,Column类中可以添加的组件为:ImageView,TextView以及Column自己。所以我们在Column类中,分别定义ImageView(), TextView(),Column()方法:

    class Column(private val linearLayout: LinearLayout) {
    
        fun ImageView(
            id: Int,
            width: Int,
            height: Int,
            backgroundColor: Int,
            @DrawableRes src: Int = 0,
            onClickListener: View.OnClickListener? = null,
            block: (ImageView.() -> Unit)? = null
        ) {
            val imageView = ImageView(linearLayout.context)
            imageView.id = id
            imageView.setImageResource(src)
            imageView.setBackgroundColor(backgroundColor)
            onClickListener?.let {
                imageView.setOnClickListener(it)
            }
            block?.let { imageView.it() }
    
            val layoutParams = LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT
            )
            layoutParams.width = width
            layoutParams.height = height
    
            linearLayout.addView(imageView, layoutParams)
        }
    
        fun TextView(text: String, block: (TextView.() -> Unit)? = null) {
            val textView = TextView(linearLayout.context)
            textView.text = text
            block?.let { textView.it() }
            linearLayout.addView(textView)
        }
    
        fun Column(
            orientation: Int,
            leftMargin: Int = 0,
            topMargin: Int = 0,
            rightMargin: Int = 0,
            bottomMargin: Int = 0,
            layoutGravity: Int = Gravity.START,
            block: Column.() -> Unit
        ) {
    
            val linearLayout = LinearLayout(linearLayout.context)
            initColumn(
                linearLayout,
                orientation,
                leftMargin,
                topMargin,
                rightMargin,
                bottomMargin,
                layoutGravity
            )
            Column(linearLayout).block()
            this.linearLayout.addView(linearLayout)
        }
    
        fun initColumn(
            linearLayout: LinearLayout,
            orientation: Int,
            leftMargin: Int = 0,
            topMargin: Int = 0,
            rightMargin: Int = 0,
            bottomMargin: Int = 0,
            layoutGravity: Int = Gravity.START
        ) {
            linearLayout.orientation = orientation
    
            val layoutParams = LinearLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT
            )
    
            layoutParams.leftMargin = leftMargin
            layoutParams.topMargin = topMargin
            layoutParams.rightMargin = rightMargin
            layoutParams.bottomMargin = bottomMargin
            layoutParams.gravity = layoutGravity
            linearLayout.layoutParams = layoutParams
        }
    
    }
    

    在上面的三个方法中,根据要使用的场景添加了一些属性参数,剩下的都是基本的属性设置。

    定义好了之后,我们需要定义Column的高级函数,因为创建View需要Context,如果Context作为参数传入显得不够优雅,所以就直接把高级函数扩展到Activity上:

    fun Activity.Column(
        orientation: Int,
        leftMargin: Int = 0,
        topMargin: Int = 0,
        rightMargin: Int = 0,
        bottomMargin: Int = 0,
        block: Column.() -> Unit
    ): View {
        val linearLayout = LinearLayout(this)
        Column(linearLayout)
            .apply {
                initColumn(
                    linearLayout = linearLayout,
                    orientation = orientation,
                    leftMargin = leftMargin,
                    topMargin = topMargin,
                    rightMargin = rightMargin,
                    bottomMargin = bottomMargin
                )
                block()
            }
        return linearLayout
    }
    

    代码和Column中的几乎是一样。目前我们仅仅定义了一个类和一个高级函数,但是创建布局的代码已经发生了巨大的变化:

    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(
                // 这里调用的是Activty.column
                Column(orientation = android.widget.LinearLayout.HORIZONTAL) {
    
                    ImageView(
                        id = R.id.img,
                        width = 150,
                        height = 150,
                        src = R.drawable.ic_launcher_foreground,
                        backgroundColor = ContextCompat.getColor(
                            this@MainActivity,
                            R.color.colorAccent
                        ),
                        onClickListener = View.OnClickListener { toast("img click") }
                    )
                    // 这里调用的是Column内部的Column()方法
                    Column(
                        orientation = android.widget.LinearLayout.VERTICAL,
                        leftMargin = 30,
                        layoutGravity = Gravity.CENTER_VERTICAL
                    ) {
    
                        TextView(text = "这是一个标题")
                        TextView(text = "这是一个描述")
                    }
    
                }
            )
    
        }
    }
    

    最后看一下实现效果:


    在这里插入图片描述

    源码下载地址:https://github.com/li504799868/DSL-UI

    总结

    DSL特性的实现,其实仅仅是Kotlin高级函数发光的一部分,只要我们设计合理,将会大大减少开发成本,不得不说Google大力支持Kotlin的确是眼光独到,Kotlin的发展未来可期。还没有把Kotlin提上日程的朋友必须要抓紧时间了。

    相关文章

      网友评论

        本文标题:DSL实战:仿Flutter代码布局实战

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