Kotlin之小试Anko

先声明,因项目需要。本人也是刚刚尝试,是看了大神的资料,才有了这篇文章。代码是自己跟着大神的脚步走的。

第一部分

资料地址

anko是什么

Anko是JetBrains开发的一个强大的库,它主要的目的是用来替代以前XML的方式来使用代码生成UI布局的,它包含了很多的非常有帮助的函数和属性来避免让你写很多的模版代码。

环境配置 (结合kotlin使用)#

项目 build.gradle

buildscript {
    ext.kotlin_version = '1.3.11'
    ext.anko_version = "0.10.8"
    repositories {
        google()
        jcenter()
        
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.3.0'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "org.jetbrains.kotlin:kotlin-android-extensions: $kotlin_version"
    }
}

app build.gradle

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

    // Anko Commons
    implementation "org.jetbrains.anko:anko-commons:$anko_version"

    // Anko Layouts
    implementation "org.jetbrains.anko:anko-sdk25:$anko_version" // sdk15, sdk19, sdk21, sdk23 are also available
    implementation "org.jetbrains.anko:anko-appcompat-v7:$anko_version"

    // Coroutine listeners for Anko Layouts
    implementation "org.jetbrains.anko:anko-sdk25-coroutines:$anko_version"
    implementation "org.jetbrains.anko:anko-appcompat-v7-coroutines:$anko_version"

    // Anko SQLite
    implementation "org.jetbrains.anko:anko-sqlite:$anko_version"
}

示例

xml中的控件的属性的设置

xml

<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">

    <TextView
            android:id="@+id/tv_home"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World!"
    />

</LinearLayout>

activity

tv_home.text = "test_1"

怎么样,就是这么神奇

java中创建布局

private fun showCustomerLayout() {
    verticalLayout {
        padding = dip(30)
        editText {
            hint = "Name"
            textSize = 24f
        }.textChangedListener {
            onTextChanged { str, _, _, _ ->
                println(str)
            }
        }
        editText {
            hint = "Password"
            textSize = 24f
        }.textChangedListener {
            onTextChanged { str, _, _, _ ->
                println(str)
            }
        }
        button("跳转到其它界面") {
            textSize = 26f
            id = BTN_ID
            onClick {
				// 界面跳转并携带参数
                startActivity<IntentActivity>("name" to "小明", "age" to 12)
            }
        }

        button("显示对话框") {
            onClick {
                makeAndShowDialog()
            }
        }
        button("列表selector") {
            onClick {
                makeAndShowListSelector()
            }
        }
    }
}

private fun makeAndShowListSelector() {
    val countries = listOf("Russia", "USA", "England", "Australia")
    selector("Where are you from", countries) { ds, i ->
        toast("So you're living in ${countries[i]},right?")
    }
}

private fun makeAndShowDialog() {
    alert("this is the msg") {
        customTitle {
            verticalLayout {
                imageView(R.mipmap.ic_launcher)
                editText {
                    hint = "hint_title"
                }
            }
        }

        okButton {
            toast("button-ok")
            // 会自行关闭不需要我们手动调用
        }
        cancelButton {
            toast("button-cancel")
        }
    }.show()
}

效果如下

Kotlin之小试Anko

对话框按钮点击效果

Kotlin之小试Anko

列表selector点击效果

Kotlin之小试Anko

列表条目点击效果

Kotlin之小试Anko

目标界面效果

Kotlin之小试Anko

补充:(目标界面代码)

private fun receiveAndShowResult() {
	// 数据的获取
    val name = intent.extras.getString("name")
    val age = intent.extras.getInt("age")

	// 界面的定义及展示
    verticalLayout {
        textView(name) {
            textSize = 18f
            textColor = Color.BLACK
        }
		// 布局参数设置
        view {
            backgroundColor = Color.GRAY
        }.lparams(width = wrapContent, height = 1) {
            topMargin = dip(5)
        }
        textView("$age") {
            textSize = 18f
            textColor = Color.BLACK
        }
        view {
            backgroundColor = Color.GRAY
        }.lparams(width = wrapContent, height = 1) {
            topMargin = dip(5)
        }
        button("返回") {
            onClick {
                finish()
            }
        }
    }
}

高级使用部分

anko-sqlite配置

// Anko SQLite
implementation "org.jetbrains.anko:anko-sqlite:$anko_version"

anko-sqlite的使用

db创建

class MyDb(ctx: Context) : ManagedSQLiteOpenHelper(ctx, "MyDb", null, 1) {
    companion object {
        private var instance: MyDb? = null

        // 构建线程安全的单例
        @Synchronized
        fun getInstance(ctx: Context): MyDb {
            if (instance == null) {
                instance = MyDb(ctx)
            }
            return instance!!
        }
    }

    override fun onCreate(db: SQLiteDatabase?) {
        // 创建表
        db?.createTable(
            "Customer", true,
            "id" to INTEGER + PRIMARY_KEY + UNIQUE,
            "t_name" to TEXT,
            "t_photo" to BLOB
        )
    }

    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
        when (newVersion) {
            2 -> {
                if (oldVersion < newVersion) {
                    println("newVersion -- $newVersion")

                    db?.execSQL("ALTER TABLE Person RENAME To tmp_person")
                    db?.createTable(
                        "Person", true,
                        "id" to INTEGER + PRIMARY_KEY + UNIQUE,
                        "name" to TEXT,
                        "age" to INTEGER,
                        "address" to TEXT,
                        "sex" to INTEGER
                    )
                    db?.execSQL("INSERT INTO Person Select id,name,age,address FROM tmp_person")
                    db?.dropTable("tmp_person")
                }
            }
        }
    }
}

db扩展方法

val Context.database: MyDb
    get() = MyDb.getInstance(applicationContext)

使用示例

创建表

database.use {
    if (attempt {
            createTable(
                "book", true,
                "id" to INTEGER + PRIMARY_KEY + UNIQUE,
                "name" to TEXT,
                "author" to TEXT,
                "price" to INTEGER,
                "pubtime" to TEXT
            )
            toast("表创建成功")
        }.isError) {
        toast("表创建失败")
    }
}

添加数据

database.use {
    if (attempt {
            insert(
                "book",
                "name" to "奇迹",
                "author" to "未知",
                "price" to 32,
                "pubtime" to "2012-12-11"
            )
            insert(
                "book",
                "name" to "奇迹1",
                "author" to "未知",
                "price" to 32,
                "pubtime" to "2012-12-11"
            )
            insert(
                "book",
                "name" to "奇迹2",
                "author" to "未知",
                "price" to 33,
                "pubtime" to "2011-12-11"
            )
            insert(
                "book",
                "name" to "金瓶梅",
                "author" to "他二爷",
                "price" to 25,
                "pubtime" to "2010-1-1"
            )
            insert(
                "book",
                "name" to "平凡的世界",
                "author" to "路遥",
                "price" to 15,
                "pubtime" to "208-10-15"
            )
            toast("数据插入成功")
        }.isError) {
        toast("数据插入成功")
    }
}

更新数据

if (attempt {
        database.use {
            if (attempt {
                    execSQL("update book set price = 85 where name = '平凡的世界'")
                    toast("更新数据成功")
                }.isError) {
                toast("更新数据失败")
            }
        }
    }.isError) {
    toast("错误,表不存在")
}

查找数据

单一查找
val whereArgs = select("book", "name", "price", "author")
                            .whereArgs("(price = {price})", "price" to 33)
// 另一种方式用的是三元元组,这种是自定义式的数据解析器
// parseSingle 和 parseOpt数据都只适用于结果只有一条的时候,如果有多条,会报错
val list = whereArgs.parseSingle(classParser<Book>())
println(list)

Book(name=奇迹2, price=33, author=未知)

查找全部
val whereArgs = select("book", "name", "price", "author")
var parser = rowParser { name: String, price: Int, author: String ->
Triple(name, price, author)
}

val list = whereArgs.parseList(parser)
println(list)

[(奇迹, 32, 未知), (奇迹1, 32, 未知), (奇迹2, 33, 未知), (金瓶梅, 25, 他二爷), (平凡的世界, 85, 路遥)]

条件过滤查找
val whereArgs = select("book", "name", "price", "author")
    .whereArgs("(price > {price})", "price" to 30)
var parser = rowParser { name: String, price: Int, author: String ->
    Triple(name, price, author)
}

val list = whereArgs.parseList(parser)
println(list)

[(奇迹, 32, 未知), (奇迹1, 32, 未知), (奇迹2, 33, 未知), (平凡的世界, 85, 路遥)]

val whereArgs = select("book", "name", "price", "author")
    .whereArgs("(price > {price})", "price" to 30)
// 另一种方式用的是三元元组,这种是自定义式的数据解析器
val list = whereArgs.parseList(classParser<Book>())
println(list)

[Book(name=奇迹, price=32, author=未知), Book(name=奇迹1, price=32, author=未知), Book(name=奇迹2, price=33, author=未知), Book(name=平凡的世界, price=85, author=路遥)]

删除表

database.use {
    if (attempt {
            dropTable("book")
            toast("表删除成功")
        }.isError) {
        toast("表删除失败")
    }
}

再次感谢大神的资料,大神的博客地址

第二部分

资料

Anko的源码解析

目标示例代码

verticalLayout {
    textView("注册") {
        textSize = dip(22).toFloat()
        textColor = Color.BLUE
        gravity = Gravity.CENTER_HORIZONTAL
    }
    editText {
        hint = "请输入姓名"
    }

    // 水平线性布局
    linearLayout {
        textView("忘记密码")
        textView("没有账户重新注册一个")
    }

    button("确定") {
        onClick {
            toast("注册用户")
        }
    }
    loadRes()
}

其次给出代码工作流程

Kotlin之小试Anko

1. 创建控件 
2. 调用init(),其实就是调用我们传递的lambda表达式
3. addView(),根据传入的上下文执行不同的操作,activity->setContentView,viewManager->addView()

verticalLayout函数

inline fun Activity.verticalLayout(theme: Int = 0): LinearLayout = verticalLayout(theme) {}
inline fun Activity.verticalLayout(theme: Int = 0, init: (@AnkoViewDslMarker _LinearLayout).() -> Unit): LinearLayout {
    return ankoView(`$$Anko$Factories$CustomViews`.VERTICAL_LAYOUT_FACTORY, theme, init)
}

verticalLayout() 其实是个函数,参数也是个函数。这里就涉及到了“闭包”,简单来说就是:“verticalLayout”这个方法的参数(init)也是个函数,这个参数可以理解为在_LinearLayout类中扩展的匿名方法或者代码块。其中_LinearLayout是LinearLayout的子类,这个咱们后面讲的lparam时候再说。这个方法返回是一个LineaerLayout,咱们先来看看他的代码是怎么生成LinearLayout。

对象的生成

@PublishedApi
internal object `$$Anko$Factories$CustomViews` {
	// 单例类的创建
    val VERTICAL_LAYOUT_FACTORY = { ctx: Context ->
        val view = _LinearLayout(ctx)
        view.orientation = LinearLayout.VERTICAL
        view
    }
}

创建一个单例工厂类,里面有个函数属性:

val VERTICAL_LAYOUT_FACTORY:(Context)-> _LinearLayout

ankoView函数

inline fun <T : View> Activity.ankoView(factory: (ctx: Context) -> T, theme: Int, init: T.() -> Unit): T {
    val ctx = AnkoInternals.wrapContextIfNeeded(this, theme)
    val view = factory(ctx)
    view.init() -->> 传入的lambda表达式
    AnkoInternals.addView(this, view) // 先创建AnkoContextImpl对象,然后再调用AnkoContextImpl的addView
    return view
}

AnkoInternals.addView函数

fun <T : View> addView(activity: Activity, view: T) {
	// 先创建上下文
    createAnkoContext(activity,
					 { 
						AnkoInternals.addView(this, view)
					 }, true)
}

createAnkoContext函数

inline fun <T> T.createAnkoContext(
        ctx: Context,
        init: AnkoContext<T>.() -> Unit,
        setContentView: Boolean = false
): AnkoContext<T> {
    val dsl = AnkoContextImpl(ctx, this, setContentView)
    dsl.init() // 这里其实是调用了我们传入的labmda表达式,调用的是addView方法
    return dsl
}

AnkoInternals.addView

fun <T : View> addView(manager: ViewManager, view: T) = when (manager) {
    is ViewGroup -> manager.addView(view)  // 如果是ViewGroup直接调用addView
    is AnkoContext<*> -> manager.addView(view, null) // 如果是AnkoContext直接调用AnkoContextImpl的addView
    else -> throw AnkoException("$manager is the wrong parent")
}

AnkoContextImpl

open class AnkoContextImpl<T>(
        override val ctx: Context,
        override val owner: T,
        private val setContentView: Boolean
) : AnkoContext<T> {
    private var myView: View? = null

    override fun addView(view: View?, params: ViewGroup.LayoutParams?) {
        if (view == null) return

        if (myView != null) {
            alreadyHasView()
        }

        this.myView = view

        if (setContentView) { // 上面传入的是true
            doAddView(ctx, view)
        }
    }

    private fun doAddView(context: Context, view: View) {
        when (context) {
            is Activity -> context.setContentView(view)  // 如果是activity,调用setContentView
            is ContextWrapper -> doAddView(context.baseContext, view) // 调用addView
            else -> throw IllegalStateException("Context is not an Activity, can't set content view")
        }
    }

    open protected fun alreadyHasView(): Unit = throw IllegalStateException("View is already set: $myView")
}

lparams实现原理分析

示例代码

private fun @AnkoViewDslMarker _LinearLayout.test_include() {
    include<View>(R.layout.layout_test01) {
        backgroundColor = Color.LTGRAY
    }.lparams(width = matchParent) {
        margin = dip(12)
    }
}

T.lparams

inline fun <T: View> T.lparams(
        width: Int = android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
        height: Int = android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
        init: RelativeLayout.LayoutParams.() -> Unit
): T {
    val layoutParams = RelativeLayout.LayoutParams(width, height)
    layoutParams.init()
    [email protected] = layoutParams
    return this
}

Fragment之列表展示

Activity

class UIFragmentAct : AppCompatActivity() {

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

        supportFragmentManager.beginTransaction().replace(R.id.ll_root, MyFragment()).commit()
    }
}

Fragment

class MyFragment : Fragment() {
    var swipeLaout: SwipeRefreshLayout? = null
    var recView: RecyclerView? = null
    var dataList: ArrayList<String>? = null
    var ctx: Context? = null

    override fun onAttach(context: Context?) {
        super.onAttach(context)
        ctx = context
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return createView()
    }

    private fun createView(): View {
        return UI {
            frameLayout {
                swipeLaout = swipeRefreshLayout {
                    setColorSchemeResources(android.R.color.holo_blue_bright)
                    setOnRefreshListener {
                        getData()
                        swipeLaout?.isRefreshing = false
                    }

                    recView = recyclerView {
                        layoutManager = LinearLayoutManager(context)
                        backgroundColor = Color.parseColor("#f3f3f3")
                    }
                }.lparams(width = matchParent, height = matchParent)

            }
        }.view
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        getData()
        recView?.adapter = ListItemAdapter(ctx!!, dataList)
    }

    private fun getData() {
        dataList = ArrayList<String>()
        dataList?.add("test01")
        dataList?.add("test02")
        dataList?.add("test03")
        dataList?.add("test04")
        dataList?.add("test05")
        dataList?.add("test06")
        dataList?.add("test07")
        dataList?.add("test08")
    }
}

Adapter

class ListItemAdapter(var context: Context, var dataList: ArrayList<String>?) :
    RecyclerView.Adapter<ListItemAdapter.ViewHolder>() {
    var inflater: LayoutInflater = LayoutInflater.from(context)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListItemAdapter.ViewHolder {
        // 老方式
//        val itemView = inflater.inflate(R.layout.layout_test_list_item, parent, false)
        // 新方式创建ViewHolder
        return ViewHolder(createItemView(context))
    }

    private fun createItemView(context: Context): View {
        return with(context) {
            linearLayout {
                lparams(width = matchParent, height = dip(100)) {
                    bottomMargin = dip(5)
                }
                cardView {
                    linearLayout {
                        gravity = Gravity.CENTER_VERTICAL
                        imageView(R.mipmap.ic_launcher_round)
                            .lparams(width = dip(65), height = dip(65)) {
                                leftMargin = dip(10)
                            }

                        textView {
                            id = R.id.tv_title
                            textSize = dip(18).toFloat()
                            textColor = Color.BLUE
                        }.lparams(width = matchParent, height = wrapContent) {
                            leftMargin = dip(10)
                            rightMargin = dip(10)
                        }
                    }

                }.lparams(width = matchParent, height = matchParent) {
                    padding = dip(5)
                }
            }
        }
    }

    override fun getItemCount(): Int {
        return dataList?.size ?: 0
    }

    override fun onBindViewHolder(holder: ListItemAdapter.ViewHolder, position: Int) {
        if (dataList != null) {
            val txt = dataList?.get(position)
            holder.tvTitle.text = txt
            holder.itemView.onClick {
                context.toast("点击了: $txt")
            }
        }
    }

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        var tvTitle: TextView = itemView.findViewById(R.id.tv_title)
    }
}

界面效果

Kotlin之小试Anko

异步任务及界面刷新

async { // 在表达式中执行异步任务
    println("ThreadId = ${Thread.currentThread().id}")
    Thread.sleep(500)
    uiThread { // 任务回调到ui线程
        println("ThreadId = ${Thread.currentThread().id}")
        tvResult?.text = "www.baidu.com"
    }
}