Kotlin&Anko, 扔掉XML开发Android应用
尝鲜使用Kotlin写了一段时间Android。说大幅度的减少了Java代码一点不夸张。用Java的时候动不动就new一个 Jetbrains给Android带来的不仅是Kotlin,还有Anko。从Anko的官方说明来看这是一个雄心勃勃的要代替XML写Layout的新的开发方式。Anko最重要的一点是引入了DSL(Domain Specific Language)的方式开发Android界面布局。当然,本质是代码实现布局。不过使用Anko完全不用经历Java纯代码写Android的痛苦。因为本身是来自Kotlin的,所以自然的使用这种方式开发就具有了: 来一个列子看一下。为了不太墨迹,一些不必要的xml声明此处略去。 <RelativeLayout>
<TextView android:id="@+id/sample_text_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:text="Sample text view" android:textSize="25sp" />
<Button android:id="@+id/sample_button" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@+id/sample_text_view" android:text="Sample button" />
</RelativeLayout>
relativeLayout { val textView = textView("Sample text view") { textSize = 25f }.lparams { width = matchParent alignParentTop() }
button("Sample button").lparams { width = matchParent below(textView) }
}
准备工作首先,安装一个Kotlin的插件是必须的。有了这个插件才可以使用Kotlin,然后才可以使用Anko。安装这个插件和Android Studio里安装别的插件市一样的。只需要使用Kotlin查找就可以找到,之后安装即可。 在build.gradle里添加下面的代码: dependencies {
compile 'org.jetbrains.anko:anko-sdk15:0.8.3' // sdk19,sdk21,sdk23 are also available
compile 'org.jetbrains.anko:anko-support-v4:0.8.3' // In case you need support-v4 bindings
compile 'org.jetbrains.anko:anko-appcompat-v7:0.8.3' // For appcompat-v7 bindings
}
然后sync一把。配置的问题解决。 写一个ListView热身首先创建一个ListView的item点击之后跳转的activity。这里叫做 现在就创建这个listview,并在listview的item点击之后调转到相应的activity去。 // 1
verticalLayout {
padding = dip(16)
// 2
val list = listView() {
// 3
adapter = ArrayAdapter<String>(this@MainActivity,android.R.layout.simple_list_item_1,items)
// 4
onItemClickListener = object : AdapterView.OnItemClickListener {
override fun onItemClick(parent: AdapterView<*>?,v: View?,position: Int,id: Long) {
when (position) {
0 -> {
// 5
startActivity<TabDemo1>()
}
}
}
}
}.lparams(width = matchParent) { // 6
height = matchParent
}
}
分别解释: 用Fragment写一个Tab布局热身结束。我们来开始真正的开发阶段。 下面要开发的是一个日记App。一共有三个tab,第一个是日记列表,第二个tab是写日记,第三个tab可以设置一些字体大小等(这里只用来占位,不做实现)。 每一个tab都用一个 现在就从这个托管activity: <style name="AppTheme.NoActionBar" parent="Theme.AppCompat.Light.NoActionBar"> </style>
之后把这个style应用在activity在AndroidManifest.xml配置中。 这个时候这个托管activity的界面布局就是一个完全的白板了。这个白板现在要分为上中下三部分。上部为我们自定义的action bar,最下面的是tab bar,剩下的部分就是每个tab的内容的fragment。 我们来看一下这个布局应该怎么写: // 1
relativeLayout {
id = ID_RELATIVELAYOUT
backgroundColor = Color.LTGRAY
// 2
linearLayout {
id = ID_TOP_BAR
backgroundColor = ContextCompat.getColor(ctx,R.color.colorPrimary)
orientation = LinearLayout.HORIZONTAL
titleTextView = textView {
text = "Some Title"
textSize = 16f
textColor = Color.WHITE
gravity = Gravity.CENTER_HORIZONTAL or Gravity.CENTER_VERTICAL
}.lparams {
width = dip(0)
height = matchParent
weight = 1f
}
}.lparams {
width = matchParent
height = dip(50)
alignParentTop()
}
// 3
linearLayout {
id = ID_BOTTOM_TAB_BAR
orientation = LinearLayout.HORIZONTAL
backgroundColor = Color.WHITE
// 4
homeListTab = weightTextView {
text = "List"
normalDrawable = resources.getDrawable(R.mipmap.tab_my_normal)
selectedDrawable = resources.getDrawable(R.mipmap.tab_my_pressed)
onClick { tabClick(0) }
}
detailTab = weightTextView {
text = "Detail"
normalDrawable = resources.getDrawable(R.mipmap.tab_channel_normal)
selectedDrawable = resources.getDrawable(R.mipmap.tab_channel_pressed)
onClick { tabClick(1) }
}
settingsTab = weightTextView {
text = "Settings"
normalDrawable = resources.getDrawable(R.mipmap.tab_better_normal)
selectedDrawable = resources.getDrawable(R.mipmap.tab_better_pressed)
onClick { tabClick(2) }
}
}.style { // 5
view ->
when (view) {
is TextView -> {
view.padding = dip(5)
view.compoundDrawablePadding = dip(3)
view.textSize = 10f
view.gravity = Gravity.CENTER
}
else -> {
}
}
}.lparams {
height = dip(50)
width = matchParent
alignParentBottom()
}
// 6
fragmentContainer = frameLayout {
id = ID_FRAMELAYOUT
backgroundColor = Color.GREEN
}.lparams {
below(ID_TOP_BAR)
above(ID_BOTTOM_TAB_BAR)
width = matchParent
height = matchParent
}
}
另外,在java写的时候常用的 上文第4点用到了 首先自定义一个view: class WeightTextView(context: Context) : TextView(context) {
var normalDrawable: Drawable? = null
var selectedDrawable: Drawable? = null
init {
var layoutParams = LinearLayout.LayoutParams(dip(50),LinearLayout.LayoutParams.MATCH_PARENT,1f)
layoutParams.weight = 1f
this.layoutParams = layoutParams
}
override fun setSelected(selected: Boolean) {
super.setSelected(selected)
if (selected) {
this.backgroundColor = ContextCompat.getColor(context,R.color.textGray)
this.textColor = ContextCompat.getColor(context,R.color.textYellow)
if (selectedDrawable != null) {
this.setCompoundDrawablesWithIntrinsicBounds(null,selectedDrawable,null,null)
}
} else {
this.backgroundColor = ContextCompat.getColor(context,android.R.color.transparent)
this.textColor = ContextCompat.getColor(context,R.color.textGray)
if (normalDrawable != null) {
this.setCompoundDrawablesWithIntrinsicBounds(null,normalDrawable,null)
}
}
}
}
附加解释: 下面看看如何**扩展**Anko,来使用我们上面的自定义view。 public inline fun ViewManager.weightTextView() = weightTextView {}
public inline fun ViewManager.weightTextView(init: WeightTextView.() -> Unit) = ankoView({ WeightTextView(it) },init)
这部分涉及到的语法内容可以参考官网。 class HTML {
fun body() { ... }
}
现在有这么一个 html { body() }
在这么一个lambda表达式里就可以直接这样调用 fun html(init: HTML.() -> Unit): HTML { val html = HTML() // create the receiver object html.init() return html }
其实灰常的简单呢。在方法 在方法执行的过程中,首先初始化了 为了帮助理解,这里给出一个参数是方法的方法: fun main(args: Array<String>) {
calling("yo") { p ->
println("method called $p")
}
calling("yoyo",::called)
}
fun calling(param: String,func: (String) -> Unit) { func(param) } fun called(p: String) { println("output string $p") }
第一个是用lambda表达式作为传入方法,第二个是已经定义好的一个方法作为传入方法。 Fragment的处理本文中的重点在于使用Anko做布局,具体的逻辑处理java写和Kotlin写没有什么区别。这里只简单介绍一下。 为了保证兼容,这里使用Support v4来处理Fragment的显示等操作。在activity的一开始就把需要的fragemnt都加载进来。 fun prepareTabFragments() {
val fm = supportFragmentManager
homeListFragment = HomeListFragment.newInstance()
fm.beginTransaction()
.add(ID_FRAMELAYOUT,homeListFragment)
.commit()
detailFragment = DetailFragment.newInstance(null)
detailFragment?.modelChangeListener = homeListFragment
fm.beginTransaction()
.add(ID_FRAMELAYOUT,detailFragment)
.commit()
settingsFragment = DiarySettingsFragment.newInstance()
fm.beginTransaction()
.add(ID_FRAMELAYOUT,settingsFragment)
.commit()
}
每一个tab项被点击的时候的处理: fun tabClick(index: Int) {
info("index is $index")
val ft = supportFragmentManager.beginTransaction()
ft.hide(homeListFragment)
ft.hide(detailFragment)
ft.hide(settingsFragment)
// unselect all textviews
homeListTab?.isSelected = false
detailTab?.isSelected = false
settingsTab?.isSelected = false
when (index) {
0 -> {
homeListTab?.isSelected = true
ft.show(homeListFragment)
}
1 -> {
detailTab?.isSelected = true
ft.show(detailFragment)
}
2 -> {
settingsTab?.isSelected = true
ft.show(settingsFragment)
}
else -> {
}
}
ft.commit()
}
分别开始每一个Fragment在开始之前需要考虑一个很严重的事情:数据存在什么地方。本来应该是SQLite或者存在云上的。存在云裳就可以实现同一个账号登录在任何地方都可以同步到同样的内容。这里只简单模拟,存放在app的内存里。存放在 class AnkoApplication : Application() { override fun onCreate() { super.onCreate() } companion object { var diaryDataSource = mutableListOf<DiaryModel>() } }
第一个tab,HomeListFragment
// 1
var view = with(ctx) {
verticalLayout {
backgroundColor = Color.WHITE
listView = listView {
adapter = ArrayAdapter<DiaryModel>(ctx,android.R.layout.simple_list_item_1,AnkoApplication.diaryDataSource)
onItemClick { adapterView,view,i,l ->
toast("clicked index: $i,content: ${AnkoApplication.diaryDataSource[i].toString()}")
}
}
// 2
emptyTextView = textView {
text = resources.getString(R.string.list_view_empty)
textSize = 30f
gravity = Gravity.CENTER
}.lparams {
width = matchParent
height = matchParent
}
}
}
// 3
listView?.emptyView = emptyTextView
return view
第二个tab,DetailFragment日记的内容包括,日记title,日记本身的内容还有日记的日期。 所以布局上就包括日记的title、内容输入用的 return with(ctx) {
verticalLayout {
padding = dip(10)
backgroundColor = Color.WHITE
textView("TITLE") {
}.lparams(width = matchParent)
titleEditText = editText {
hint = currentDateString()
lines = 1
}.lparams(width = matchParent) {
topMargin = dip(5)
}
textView("CONTENT") {
}.lparams(width = matchParent) {
topMargin = dip(15)
}
contentEditText = editText {
hint = "what's going on..."
setHorizontallyScrolling(false)
}.lparams(width = matchParent) {
// height = matchParent
topMargin = dip(5)
}
button(R.string.button_select_time) {
gravity = Gravity.CENTER
onClick {
val fm = activity.supportFragmentManager
var datePicker = DatePickerFragment.newInstance(diaryModel?.date)
datePicker.setTargetFragment(this@DetailFragment,DetailFragment.REQUEST_DATE)
datePicker.show(fm,"date")
}
}
// *
button(R.string.button_detail_ok) {
onClick {
v ->
println("ok button clicked")
try {
var model = diaryModel!!
model.title = titleEditText?.text.toString()
model.content = contentEditText?.text.toString()
AnkoApplication.diaryDataSource.add(model)
modelChangeListener?.modelChanged()
toast(R.string.model_saved_ok)
} catch(e: Exception) {
Log.d("##DetailFragment","error: ${e.toString()}")
toast(R.string.model_save_error)
}
}
}.lparams {
topMargin = dip(10)
width = matchParent
}
}.style {
view ->
when (view) {
is Button -> {
view.gravity = Gravity.CENTER
}
is TextView -> {
view.gravity = Gravity.LEFT
view.textSize = 20f
view.textColor = Color.DKGRAY
}
}
}
}
需要注意打星号的地方。按钮在点击之后会弹出一个dialog fragment来显示日期view。用户可以在这个日期view里选择相应的日期。但是,如何从日期dialog fragment传递选择的日期给 选择日期的dialog fragment是 var pickerView = DatePicker(activity)
pickerView.calendarViewShown = false
pickerView.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT)
pickerView.init(year,month,day) {
view,year,day ->
mDate = GregorianCalendar(year,day).time
arguments.putSerializable(EXTRA_DATE,mDate)
}
return AlertDialog.Builder(activity)
.setView(pickerView)
.setTitle(R.string.date_picker_title)
.setPositiveButton(R.string.picker_button_ok) { dialog,which ->
toast("hello world!")
sendResult(Activity.RESULT_OK)
}.create()
首先 在选择日期的时候,会触发 那么怎么发送呢?使用target fargment方法。在detail fragment弹出dialog fragment的时候,把detail fragment设置为target fragment。 button(R.string.button_select_time) {
gravity = Gravity.CENTER
onClick {
val fm = activity.supportFragmentManager
var datePicker = DatePickerFragment.newInstance(diaryModel?.date)
// *
datePicker.setTargetFragment(this@DetailFragment,DetailFragment.REQUEST_DATE)
datePicker.show(fm,"date")
}
}
在标星下面的一行代码中。 companion object Factory {
val REQUEST_DATE = 0`
}
在positive按钮点击之后执行方法 private fun sendResult(resultCode: Int) {
if (targetFragment == null)
return
var i = Intent()
i.putExtra(EXTRA_DATE,mDate)
// *
targetFragment.onActivityResult(targetRequestCode,resultCode,i)
}
调用 在 override fun onActivityResult(requestCode: Int,resultCode: Int,data: Intent?) { if (resultCode != Activity.RESULT_OK) { return }
if (requestCode != REQUEST_DATE) {
return
}
var date = data?.getSerializableExtra(DatePickerFragment.EXTRA_DATE) as Date
diaryModel?.date = date
}
日期数据传输这部分到这里结束。 全文也可以在这里画上一个句点了。以上还有很多关于Anko没有使用的地方。Anko也是可以实现代码界面分离的。继承 class SettingsUI<T> : AnkoComponent<T> {
override fun createView(ui: AnkoContext<T>) = with(ui) {
verticalLayout {
backgroundColor = ContextCompat.getColor(ctx,R.color.SnowWhite)
textView { text = resources.getString(R.string.settings_title) }
button("activity with the same `AnkoComponent`") {
id = ID_BUTTON
}
}
}
companion object Factory {
public val ID_BUTTON = 101
}
}
把这个布局文件用在 override fun onCreateView(inflater: LayoutInflater?,container: ViewGroup?,val view = SettingsUI<DiarySettingsFragment>().createView(AnkoContext.create(ctx,DiarySettingsFragment()))
return view
}
然后这个布局还可以用在我们刚刚创建的 override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SettingsUI<TempActivity>().setContentView(this)
val button = find<Button>(SettingsUI.ID_BUTTON)
button.text = "you are in `TempActivity`,CLICK!"
button.onClick {
toast("${TempActivity::class.java.simpleName}")
}
}
Activity上使用就简单很多了,只需要这么一句 代码在这里。除了布局Anko还有其他的一些语法糖糖也很是不错,不过这里就不多说了。有更多想了解的,请移步官网。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |