本章我們將介紹UI開發的相關知識。常用控件包括了TextView、Button、EditText、ImageView、PrograssBar和AlertDialog等;三種佈局包括了LinearLayout、RelativeLayout以及FrameLayout;ListView的簡單用法、基於圖片文字的ListView、利用ConvertView和ViewHolder去提升效率以及setOnItemClickListener點擊事件響應;RecyclerView的基本用法、適配器、橫向滾動、瀑布流佈局以及View的點擊事件處理;隨後進行了實踐(基於nine-patch圖片編寫聊天界面);最後是延遲初始化lateinit和密封類sealed class。
4.1.如何編寫程序界面?
主要通過編寫XML來實現,另外Google近些年推出了ConstrainLayout,它是通過拖拽控件來對界面進行操作。在本章我們僅介紹xml。
4.2.常用控件使用方法
4.2.1.TextView
TextView的功能是顯示一段文本信息。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!--layout_width指明寬度,layout_height指明高度。三個值可供選擇:match_parent(當前控件大小與父佈局相同)、
wrap_content(剛好包住)和固定值dp(與屏幕密度無關,不同分辨率下儘可能一致)-->
<!--android:gravity指明文字對齊方式,可選值有top、bottom、top等。可以用|來同時指定多個值。譬如center等價於center_vertical|center_horizontal-->
<!--android:textColor指明瞭文字顏色,android:textSize文字大小,文字大小以sp爲單位-->
<TextView
android:id="@+id/text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="#00ff00"
android:textSize="24sp"
android:text="Hello World!" />
</LinearLayout>
4.2.2.Button
與用戶進行交互的一個重要控件。先編寫xml。代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
.....
<!--Android默認將英文字母轉換成大寫,android:textAllCaps設置爲false保留指定原始文件內容-->
<Button
android:id="@+id/btn_click01"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAllCaps="false"
android:text="Button" />
</LinearLayout>
其次,點擊事件可以採用函數式API進行事件響應,也可以通過實現接口的方式來進行註冊。
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity(), View.OnClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//第一種調用方法:匿名內部類,利用Java單抽象方法接口特性,使用函數式API來寫監聽事件。
// btn_click01.setOnClickListener{
// //在此添加處理邏輯
// }
//第二種調用方法:實現接口方法進行註冊。讓MainActivity實現了View.OnClickListener接口,並重寫onClick方法
//setOnClickListener將MainActivity實例傳了進去
btn_click01.setOnClickListener(this)
}
//重寫的方法,很簡單。
override fun onClick(v: View?) {
when (v?.id) {
R.id.btn_click01 -> {
//在此添加邏輯
}
}
}
}
4.2.3.EditText
用戶在該控件中輸入和編輯文本。應用場景譬如發微博、聊QQ等。XML中定義如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
.....
<!--android:hint指定提示性的文本-->
<!--android:maxLines指明最大行數爲2行,若超過,文本向上滾動,EditText不會再拉伸。-->
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/edit_text01"
android:maxLines="2"
android:hint="type sth please"/>
</LinearLayout>
點擊按鈕獲取EditText裏面的內容,代碼如下:
//重寫的方法,很簡單。
override fun onClick(v: View?) {
when (v?.id) {
R.id.btn_click01 -> {
//點擊按鈕完成顯式EditText文本內容的功能
//edit_text01.text通過語法糖調用了getText方法,編寫代碼直接調用實際方法即可,getText可以自動轉換爲text。
val inputText = edit_text01.text.toString()
Toast.makeText(this, inputText, Toast.LENGTH_SHORT).show()
}
}
}
4.2.4.ImageView
ImageView用於在界面顯示圖片,圖片通常放在Drawable開頭的目錄,並且要附上具體分辨率。目前主流屏幕分辨率是xxhdpi的,所以在res目錄再建一個drawable-xxhdpi目錄,將事先準備好的照片複製到該目錄。Xml如下:
<!--android:src爲ImageView指定了一張圖片,由於寬高未知,因此使用兩個wrap_content使得其能顯示出來-->
<ImageView
android:id="@+id/image_view01"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/image_1" />
相應的響應事件如下:
//通過代碼動態更改ImageView圖片
R.id.btn_click02 ->{
image_view01.setImageResource(R.drawable.image_2)
}
4.2.5.PrograssBar
PrograssBar是顯示一個進度條,表示正在加載數據。Xml代碼如下:
<!-- 默認是圓形樣式,通過style屬性將其變爲水平進度條,修改其中的代碼-->
<!--android:max給進度條設置一個最大值,在代碼中動態的更改進度條進度。-->
<ProgressBar
android:id="@+id/progress_bar01"
style="?android:attr/progressBarStyleHorizontal"
android:max="100"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
Kotlin代碼中有相應的響應事件和進度條遞增代碼。
//android:visibility有三個可選值:visiable(控件可見,默認)、invisible(空間不可見,仍佔據原來未知)
// 和gone(控件不僅不可見,也不再佔用之前的屏幕),在使用setVisibility使用View.可選值
R.id.btn_click03 -> {
//使用getVisibility判斷prograssBar是否可見,如果可見隱藏,否則顯示
if (progress_bar01.visibility == View.VISIBLE) {
//調用了setVisibility
progress_bar01.visibility = View.GONE
} else {
progress_bar01.visibility = View.VISIBLE
}
}
//進度條增加
R.id.btn_click04 -> {
progress_bar01.progress += 10
}
4.2.6.AlertDialog
AlertDialog在當前界面顯示一個置頂於所有界面之上的對話框,屏蔽掉其他控件的交互,在這裏譬如實現防止用戶誤刪除的AlertDialog。
R.id.btn_click05 -> {
//構建一個對話框並使用apply函數
AlertDialog.Builder(this).apply {
//消息標題
setTitle("This is Dialog")
//消息內容
setMessage("Sth important")
//不可取消
setCancelable(false)
//確定按鈕點擊事件
setPositiveButton("OK") { dialog, which -> "刪除吧" }
//取消按鈕點擊事件
setNegativeButton("Cancel") { dialog, which -> "保留吧" }
//對話框顯示出來
show()
}
}
4.3.詳解三種基本佈局
佈局是放置很多控件的容器,佈局內部可以放置佈局或者控件,也可以通過多層佈局嵌套完成複雜界面。
4.3.1.LinearLayout
線性佈局,控件在線性方向上水平或者豎直排列,在這裏使用android:orientation屬性來指定。這裏着重注意android:gravity、android:layout_gravity和android:layout_weight三個屬性。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!--vertical豎直方向上佈局,horiziontal水平佈局,默認是水平佈局。wrap_content剛好包含文字。-->
<!--介紹一下android:gravity和android:layout_gravity,前者是文字在控件中的對齊方式,後者是指定控件在佈局中的對齊方式-->
<!--兩者可選值差不多,但horizontal時,只有垂直方向上的對齊方式可以改變,換之亦然。每添加一個控件,水平上的長度會改變。-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="9"
android:orientation="horizontal">
<Button
android:id="@+id/btn_01"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:text="Button 01" />
<Button
android:id="@+id/btn_02"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="Button 03" />
<Button
android:id="@+id/btn_03"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:text="Button 03" />
</LinearLayout>
<!--可能有人說爲什麼layout_width是0dp呢?這是因爲使用android:layout_weight,0dp是比較規範的寫法-->
<!--android:layout_weight先將layout_weight所有值相加,再按照比例進行劃分。-->
<!--如果將後者的layout_width設置爲wrap_content。前者layout_weight等於1,那麼後面剛好包着,前面是屏幕剩餘的所有空間-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_gravity="bottom"
android:orientation="horizontal">
<EditText
android:id="@+id/edit_text01"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="4"
android:hint="Type sth please" />
<Button
android:id="@+id/btn_send"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Send" />
</LinearLayout>
</LinearLayout>
4.3.2.RelativeLayout
RelativeLayout相對定位的方式可以讓控件出現在佈局中的任何位置。控件不僅可以相對於父佈局進行定位,也可以相對於其他控件進行定位。代碼如下所示:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--Button 1與父佈局左上角對齊,layout_alignParentLeft和layout_alignParentTop指定-->
<Button
android:id="@+id/btn_click_01"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:text="Button 1" />
<!--Button 2與父佈局右上角對齊-->
<Button
android:id="@+id/btn_click_02"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:text="Button 2" />
<!--Button 3居中顯示-->
<Button
android:id="@+id/btn_click_03"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Button 3" />
<!--相對於控件進行定位,控件左上,需要注意的是:若一個控件引用另一個控件的id,那麼該控件一定要定義在引用控件的後面-->
<Button
android:id="@+id/btn_click_06"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/btn_click_03"
android:layout_toLeftOf="@id/btn_click_03"
android:text="Button 6" />
<!--相對於控件進行定位,控件右上-->
<Button
android:id="@+id/btn_click_07"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/btn_click_03"
android:layout_toRightOf="@id/btn_click_03"
android:text="Button 7" />
<!--相對於控件進行定位,控件左下-->
<Button
android:id="@+id/btn_click_08"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/btn_click_03"
android:layout_toLeftOf="@id/btn_click_03"
android:text="Button 8" />
<!--相對於控件進行定位,控件右下-->
<Button
android:id="@+id/btn_click_09"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/btn_click_03"
android:layout_toRightOf="@id/btn_click_03"
android:text="Button 9" />
<!--Button 4與父佈局左下角對齊-->
<Button
android:id="@+id/btn_click_04"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentBottom="true"
android:text="Button 4" />
<!--Button 5與父佈局右下角對齊-->
<Button
android:id="@+id/btn_click_05"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:text="Button 5" />
</RelativeLayout>
4.3.3.FrameLayout
幀佈局,所有空間都會默認擺放在佈局左上角,可能存在控件重疊的情況。除了默認效果之外,可以使用layout_gravity屬性來指定控件在佈局中的對齊方式。總體來講,應用場景很少。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:text="This is TextView" />
<Button
android:id="@+id/btn_001"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:text="Button" />
</FrameLayout>
4.4.創建自定義控件
所有控件直接或者間接繼承自View的,所有佈局直接或者間接繼承自ViewGroup的。ViewGroup是特殊的View,包含很多子View和ViewGroup是用於放置空間和佈局的容器。
4.4.1.引入佈局
自定義一個標題欄並讓所有Activity引用,防止代碼大量重複。Layout目錄下建立標題欄title.xml佈局,代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@drawable/title_bg">
<!--android:background表示用圖片或者顏色填充背景-->
<!--android:layout_margin用於指定控件在上下左右方向上的舉例,當然也可以通過android:layout_marginLeft等指定-->
<Button
android:id="@+id/titleBack"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="left"
android:layout_margin="5dp"
android:background="@drawable/back_bg"
android:text="Back"
android:textColor="#fff" />
<TextView
android:id="@+id/title_text"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="Title text"
android:textColor="#fff"
android:textSize="24sp" />
<Button
android:id="@+id/titleEdit"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="right"
android:layout_margin="5dp"
android:background="@drawable/edit_bg"
android:text="Edit"
android:textColor="#fff" />
</LinearLayout>
那麼如何在程序中引入,修改Activity_main.xml裏面的代碼:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/title" />
</LinearLayout>
另外,需要在MainActivity中隱藏系統自帶標題欄:
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//通過語法糖調用getSupportActionBar來獲得ActionBar的實例,然後調用hide方法將標題隱藏
//由於ActionBar可能爲空,因此使用了?.操作符,目的是隱藏原始標題欄
supportActionBar?.hide()
}
}
4.4.2.創建自定義控件
有些佈局中需要有一些控件有響應事件的能力,但在每個Activity中單獨編寫無疑有很多冗餘的代碼,新建TitleLayout繼承自LinearLayout,讓它成爲我們自定義的標題欄控件,代碼如下:
package com.example.myapplication
import android.app.Activity
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import android.widget.Toast
import kotlinx.android.synthetic.main.title.view.*
//繼承自LinearLayout使其成爲我們的自定義控件。聲明兩個參數,在init結構體中對標題欄佈局進行動態加載
class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
init {
//LayoutInflater的from方法可以構建出一個LayoutInflater對象,然後inflate方法動態加載一個佈局文件
//inflate加載兩個參數,一個是佈局文件ID,一個是給加載好的佈局再添加一個父佈局,在這裏指定爲TitleLayout,直接傳入this。
LayoutInflater.from(context).inflate(R.layout.title, this)
titleBack.setOnClickListener {
//context參數實際上是Activity實例,首先將其抓換爲Activity類型,然後再行銷燬。
// Kotlin的強制類型轉換用as
val activity = context as Activity
activity.finish()
}
titleEdit.setOnClickListener {
Toast.makeText(context, "You clicked the edit", Toast.LENGTH_SHORT).show()
}
}
}
在Activity_main.xml中引入該自定義控件。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- <include layout="@layout/title" />-->
<com.example.myapplication.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
4.5.ListView
4.5.1.ListView的簡單用法
新建activity_main.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id = "@+id/listView_main"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
修改MainActivity、構建適配器,並將其傳入ListView中去。這樣可以通過滾動來看屏幕外數據。
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.ArrayAdapter
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
//提供待展示數據,使用listOf初始化集合
private val data = listOf(
"Apple", "Banana", "Orange", "WaterMelon", "Pear", "Grape", "Cherry","Mango", "Apple", "Banana", "Orange", "WaterMelon", "Pear", "Grape", "Cherry", "Mango"
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//集合數據無法直接傳遞,需要藉助適配器。比較好用的是ArrayAdapter,泛型指定爲String
//在構造方法中分別傳入Activity實例、ListView子項佈局的id以及數據源
//simple_list_item_1爲子項佈局id,Android內置佈局文件,裏面只有TextView用於顯示一段文本
val adapter = ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data)
//最後利用setAdapter方法將構建好的適配器傳遞進去,這樣ListView和數據之間的關聯算是搞定了
listView_main.adapter = adapter
}
}
4.5.2.定製ListView的界面
ListView的每個子項中包含圖片和文字,即自定義。新建實體類Fruit:
package com.example.myapplication
//Fruit類有兩個字段,一個是水果名,一個是水果對應圖片資源的ID
class Fruit(val name: String, val imageid: Int)
在layout目錄下新建fruit_item.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="60dp">
<!--讓兩個玩意在垂直方向上居中顯示-->
<ImageView
android:id="@+id/fruit_image"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp" />
<TextView
android:id="@+id/fruit_Name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp" />
</LinearLayout>
新建自定義適配器FruitAdapter,繼承自ArrayAdapter,泛型指定爲Fruit類。
package com.example.myapplication
import android.app.Activity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView
//定義一個主構造函數來將Activity實例、ListView子項佈局id和數據源傳遞進來
class FruitAdapter(activity: Activity, val resourceId: Int, data: List<Fruit>) :
ArrayAdapter<Fruit>(activity, resourceId, data) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
//首先使用LayoutInflater子項加載我們的佈局,三個參數。
// 最後一個false的意思是父佈局聲明的layout生效,不會爲該View增加父佈局。保準寫法
val view = LayoutInflater.from(context).inflate(resourceId, parent, false)
//獲取到ImageView和TextView的實例
val fruitImage: ImageView = view.findViewById(R.id.fruit_image)
val fruitName: TextView = view.findViewById(R.id.fruit_Name)
//得到當前項的Fruit實例
val fruit = getItem(position)
//設置圖片和文字
if (fruit != null) {
fruitImage.setImageResource(fruit.imageid)
fruitName.text = fruit.name
}
//將佈局返回
return view
}
}
最後修改MainActivity裏的代碼。
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.ArrayAdapter
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private val fruit_list = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initFruits()//初始化水果數據
//創建Adapter對象,並將其作爲適配器傳遞給ListView
val adapter = FruitAdapter(this, R.layout.fruit_item, fruit_list)
listView_main.adapter = adapter
}
fun initFruits() {
//repeat函數將水果數據重複兩遍
repeat(2) {
//構造方法中將水果名字和水果id傳入,將創建好的對象添加到水果列表中。
fruit_list.add(Fruit("Apple", R.drawable.apple_pic))
fruit_list.add(Fruit("Banana", R.drawable.banana_pic))
fruit_list.add(Fruit("Orange", R.drawable.orange_pic))
fruit_list.add(Fruit("WaterMelon", R.drawable.watermelon_pic))
fruit_list.add(Fruit("Pear", R.drawable.pear_pic))
fruit_list.add(Fruit("PineApple", R.drawable.pineapple_pic))
fruit_list.add(Fruit("StrawBerry", R.drawable.strawberry_pic))
fruit_list.add(Fruit("Cherry", R.drawable.cherry_pic))
}
}
}
4.5.3.提升ListView的效率
ListView快速滑動時,性能成爲一個瓶頸,在這裏我們藉助了兩種方案:getView的convertView參數和ViewHolder進行性能優化。
convertView用於將之前已經加載好的佈局進行緩存,以便以後重用;如果convertView爲空,使用LayoutInflater直接加載,如果不爲空,直接對convertView進行重用。
優化convertView後,雖然已經不會加載重複佈局,但仍然在調用getView方法時調用view的findviewByID獲取控件實例,因此藉助ViewHolder來進行優化。優化後的代碼如下所示:
package com.example.myapplication
import android.app.Activity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView
//定義一個主構造函數來將Activity實例、ListView子項佈局id和數據源傳遞進來
class FruitAdapter(activity: Activity, val resourceId: Int, data: List<Fruit>) :
ArrayAdapter<Fruit>(activity, resourceId, data) {
//innerclass來定義內部類
inner class ViewHolder(val fruitImage: ImageView, val fruitName: TextView)
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
//首先使用LayoutInflater子項加載我們的佈局,三個參數。
// 最後一個false的意思是父佈局聲明的layout生效,不會爲該View增加父佈局。保準寫法
val view: View
val viewHolder: ViewHolder
if (convertView == null) {
view = LayoutInflater.from(context).inflate(resourceId, parent, false)
//用於對ImageView和TextView的控件實例進行緩存
val fruitImage: ImageView = view.findViewById(R.id.fruit_image)
val fruitName: TextView = view.findViewById(R.id.fruit_Name)
//創建ViewHolder對象並將控件實例放在ViewHolder裏
viewHolder = ViewHolder(fruitImage, fruitName)
//使用View的setTag方法將ViewHolder存儲在View中。
view.tag = viewHolder
} else {
view = convertView
//緩存對象不爲空時,使用View的getTag方法將ViewHolder重新取出,這樣所有控件的實例都存儲於ViewHolder中了
viewHolder = view.tag as ViewHolder
}
//得到當前項的Fruit實例
val fruit = getItem(position)
//設置圖片和文字
if (fruit != null) {
viewHolder.fruitImage.setImageResource(fruit.imageid)
viewHolder.fruitName.text = fruit.name
}
//將佈局返回
return view
}
}
4.5.4.ListView的點擊事件
使用setOnItemClickListener爲ListView註冊一個監聽器,當點擊子項時,調用Lambda表達式,通過position確定哪一個子項。代碼示例如下:
// listView_main.setOnItemClickListener { parent, view, position, id ->
// val fruit = fruit_list[position]
// Toast.makeText(this, fruit.name, Toast.LENGTH_SHORT).show()
// }
//沒用到的參數可以用_來代替。因爲是Java單抽象方法接口,所以可用函數式API的寫法。onItemClick中接收四個參數。
listView_main.setOnItemClickListener { _, _, position, _ ->
val fruit = fruit_list[position]
Toast.makeText(this, fruit.name, Toast.LENGTH_SHORT).show()
}
4.6.RecyclerView
ListView的運行效率和擴展性(只能縱向滾動)仍然有待提高,因此我們介紹一下RecyclerView。RecycleView屬於新增空間,需要在app/build.gradle中添加該庫依賴。
dependencies {
...
compile 'androidx.recyclerview:recyclerview:1.0.0'
....
}
在這裏如果使用implementation引入新庫時會報錯”org.gradle.internal.metaobject.AbstractDynamicObject”。需要將其改變爲compile。隨後sync now。修改activity_main.xml。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id = "@+id/listView_main"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
複製上一小節的fruir_item.xml、Fruit類和所有圖片,爲RecyclerView新建FruitAdapter適配器,並制定相應的泛型,ViewHolder是一個內部類。代碼如下:
package com.example.myapplication
import android.app.Activity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView
//定義一個主構造函數來將Activity實例、ListView子項佈局id和數據源傳遞進來
class FruitAdapter(activity: Activity, val resourceId: Int, data: List<Fruit>) :
ArrayAdapter<Fruit>(activity, resourceId, data) {
//innerclass來定義內部類
inner class ViewHolder(val fruitImage: ImageView, val fruitName: TextView)
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
//首先使用LayoutInflater子項加載我們的佈局,三個參數。
// 最後一個false的意思是父佈局聲明的layout生效,不會爲該View增加父佈局。保準寫法
val view: View
val viewHolder: ViewHolder
if (convertView == null) {
view = LayoutInflater.from(context).inflate(resourceId, parent, false)
//用於對ImageView和TextView的控件實例進行緩存
val fruitImage: ImageView = view.findViewById(R.id.fruit_image)
val fruitName: TextView = view.findViewById(R.id.fruit_Name)
//創建ViewHolder對象並將控件實例放在ViewHolder裏
viewHolder = ViewHolder(fruitImage, fruitName)
//使用View的setTag方法將ViewHolder存儲在View中。
view.tag = viewHolder
} else {
view = convertView
//緩存對象不爲空時,使用View的getTag方法將ViewHolder重新取出,這樣所有控件的實例都存儲於ViewHolder中了
viewHolder = view.tag as ViewHolder
}
//得到當前項的Fruit實例
val fruit = getItem(position)
//設置圖片和文字
if (fruit != null) {
viewHolder.fruitImage.setImageResource(fruit.imageid)
viewHolder.fruitName.text = fruit.name
}
//將佈局返回
return view
}
}
最後修改MainActivity中的代碼。
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private val fruit_list = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initFruits()
//創建一個LinearLayoutManager線性佈局對象,並將其設置到RecyclerView當中
val layoutmanager = LinearLayoutManager(this)
recycler_view.layoutManager = layoutmanager
//創建FruitAdapter實例並將水果數據傳入FruitAdapter構造函數中
val adapter = FruitAdapter(fruit_list)
//最後調用setAdapter來完成適配器設置,從而完成RecyclerView與數據的關聯
recycler_view.adapter = adapter
}
private fun initFruits() {
//repeat函數將水果數據重複兩遍
repeat(2) {
//構造方法中將水果名字和水果id傳入,將創建好的對象添加到水果列表中。
fruit_list.add(Fruit("Apple", R.drawable.apple_pic))
fruit_list.add(Fruit("Banana", R.drawable.banana_pic))
fruit_list.add(Fruit("Orange", R.drawable.orange_pic))
fruit_list.add(Fruit("WaterMelon", R.drawable.watermelon_pic))
fruit_list.add(Fruit("Pear", R.drawable.pear_pic))
fruit_list.add(Fruit("PineApple", R.drawable.pineapple_pic))
fruit_list.add(Fruit("StrawBerry", R.drawable.strawberry_pic))
fruit_list.add(Fruit("Cherry", R.drawable.cherry_pic))
}
}
}
4.6.2.實現橫向滾動和瀑布流佈局
ListView擴展性不好的原因是隻能進行縱向滾動,RecyclerView能做到挺多滾動方式,譬如橫向滾動。簡單修改fruit_item佈局文件。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/fruit_image"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp" />
<TextView
android:id="@+id/fruit_Name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp" />
</LinearLayout>
核心的是在MainActivity.java中加上一句橫向排列即可。
layoutmanager.orientation = LinearLayoutManager.HORIZONTAL
相比ListView爲什麼如此簡單?ListView基於自身管理,RecyclerView則將這個工作交給了LayoutManager,除了LinearLayoutManager之外,還有網格佈局GridLayoutManager和瀑布流佈局StaggeredGridLayoutManager。下面談談瀑布流佈局,還是得先修改fruit_item.xml。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:orientation="vertical">
<ImageView
android:id="@+id/fruit_image"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp" />
<!-- layout_gravity使得文字左對齊-->
<TextView
android:id="@+id/fruit_Name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:layout_marginTop="10dp" />
</LinearLayout>
其次,修改MainActivity的代碼:
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import kotlinx.android.synthetic.main.activity_main.*
import java.lang.StringBuilder
class MainActivity : AppCompatActivity() {
private val fruit_list = ArrayList<Fruit>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initFruits()
//兩個參數,第一個是幾列;第二個是佈局的排列方向,我們選擇垂直
val layoutmanager = StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL)
recycler_view.layoutManager = layoutmanager
//創建FruitAdapter實例並將水果數據傳入FruitAdapter構造函數中
val adapter = FruitAdapter(fruit_list)
//最後調用setAdapter來完成適配器設置,從而完成RecyclerView與數據的關聯
recycler_view.adapter = adapter
}
private fun initFruits() {
//repeat函數將水果數據重複兩遍
repeat(2) {
//構造方法中將水果名字和水果id傳入,將創建好的對象添加到水果列表中。
fruit_list.add(Fruit(getRandomLengthString("Apple"), R.drawable.apple_pic))
fruit_list.add(Fruit(getRandomLengthString("Banana"), R.drawable.banana_pic))
fruit_list.add(Fruit(getRandomLengthString("Orange"), R.drawable.orange_pic))
fruit_list.add(Fruit(getRandomLengthString("WaterMelon"), R.drawable.watermelon_pic))
fruit_list.add(Fruit(getRandomLengthString("Pear"), R.drawable.pear_pic))
fruit_list.add(Fruit(getRandomLengthString("PineApple"), R.drawable.pineapple_pic))
fruit_list.add(Fruit(getRandomLengthString("StrawBerry"), R.drawable.strawberry_pic))
fruit_list.add(Fruit(getRandomLengthString("Cherry"), R.drawable.cherry_pic))
}
}
private fun getRandomLengthString(str:String):String{
val n = (1..20).random()
val builder = StringBuilder()
repeat(n){
builder.append(str)
}
return builder.toString()
}
}
4.6.3.RecyclerView的點擊事件
RecyclerView沒有setOnItemClickListener這樣的註冊監聽器方法,而是需要我們給子項具體的View去註冊點擊事件。修改適配器FruitAdapter的onCreateViewHolder方法。
//用於創建ViewHolder實例,將fruit_item加載進來,然後創建ViewHolder實例,最後將佈局傳入構造函數當中
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FruitAdapter.ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.fruit_item, parent, false)
val viewholder = ViewHolder(view)
//爲最外層的佈局和ImageView都註冊了點擊事件,先獲取用戶點擊的position,然後通過position獲取相應的Fruit實例,最後Toast
viewholder.itemView.setOnClickListener {
val position = viewholder.adapterPosition
val fruit = fruitList[position]
Toast.makeText(parent.context, "you clicked view ${fruit.name}", Toast.LENGTH_SHORT)
.show()
}
viewholder.fruitImage.setOnClickListener{
val position = viewholder.adapterPosition
val fruit = fruitList[position]
Toast.makeText(parent.context, "you clicked image ${fruit.name}", Toast.LENGTH_SHORT)
.show()
}
return viewholder
}
4.7.編寫界面的最佳實踐
實戰一下。
4.7.1.製作9-Patch圖片
9-Patch圖片是經過特殊處理的png圖片,能夠指定哪些區域可以被拉伸,哪些區域不能被拉伸。普通照片拉伸效果非常差。對普通圖片右擊->Create 9-Patch file->對圖片拖拽表示拉伸部分->刪除原來的普通照片。
4.7.2.編寫聊天界面
仿聊天軟件的聊天界面。首先在app/build.gradle裏面添加RecyclerView的依賴庫:
compile 'androidx.recyclerview:recyclerview:1.0.0'
其次,修改activity_main.xml裏面的代碼,放置顯示聊天內容RecyclerView、EditText和發送button。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#d8e0e8"
android:orientation="vertical">
<!--主界面放置一個RecyclerView來顯示聊天內容-->
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="0dp"
android:id="@+id/recycler_view"
android:layout_weight="1" />
<!--EditText輸入消息,Button用於發送消息-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/input_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="type sth here"
android:maxLines="2" />
<Button
android:id="@+id/btn_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send" />
</LinearLayout>
</LinearLayout>
再者,編寫消息實體類Msg。
package com.example.myapplication
//消息實體類,兩個參數,一個是消息內容,一個是消息類型,消息類型爲發出消息和接收消息兩種。
class Msg(val content: String, val type: Int) {
companion object {
//將其定義成常量,定義常量的關鍵字爲const,只有在單例類、companion object和頂層方法才能使用const關鍵字
const val TYPE_RECEIVED = 0
const val TYPE_SENT = 1
}
}
隨後,編寫RecyclerView的子項左佈局msg_left_item.xml和右佈局msg_right_item.xml。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="70dp"
android:orientation="vertical"
android:padding="10dp">
<!-- 接受消息的子佈局,並將收到的消息左對齊,使用相應的nine-patch背景圖片。-->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:background="@drawable/message_left_original">
<TextView
android:id="@+id/left_Msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="10dp"
android:textColor="#fff" />
</LinearLayout>
</FrameLayout>
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="70dp"
android:orientation="vertical"
android:padding="10dp">
<!--發出的消息右對齊,並使用相應的背景圖-->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:background="@drawable/message_right">
<TextView
android:id="@+id/right_Msg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="10dp"
android:textColor="#000" />
</LinearLayout>
</FrameLayout>
創建RecyclerView適配器MsgAdapter,根據不同的ViewType來創建不同的界面。代碼如下:
package com.example.myapplication
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import java.lang.IllegalArgumentException
//根據不同的type創建不同的界面
class MsgAdapter(val msgList: List<Msg>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
//定義兩個ViewHolder用於緩存msg_left_item和msg_right_item佈局中的控件
inner class LeftViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val leftMsg: TextView = view.findViewById(R.id.left_Msg)
}
inner class RightViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val rightMsg: TextView = view.findViewById(R.id.right_Msg)
}
//返回當前position相應的消息類型
override fun getItemViewType(position: Int): Int {
val msg = msgList[position]
return msg.type
}
//根據不同的ViewType加載不同的佈局並創建不同的ViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
if (viewType == Msg.TYPE_RECEIVED) {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.msg_left_item, parent, false)
return LeftViewHolder(view)
} else {
val view =
LayoutInflater.from(parent.context).inflate(R.layout.msg_right_item, parent, false)
return RightViewHolder(view)
}
}
override fun getItemCount(): Int = msgList.size
//判斷ViewHolder類型,若爲LeftViewHolder,顯示左邊消息佈局,否則顯示右邊。
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val msg = msgList[position]
when (holder) {
is LeftViewHolder -> holder.leftMsg.text = msg.content
is RightViewHolder -> holder.rightMsg.text = msg.content
else -> throw IllegalArgumentException()
}
}
}
最後修改MainActivity裏的代碼,爲RecyclerView初始化數據,並添加相應的點擊事件。
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity(), View.OnClickListener {
private val msgList = ArrayList<Msg>()
private var adapter: MsgAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//初始化幾條數據顯示
initMsg()
//構建RecyclerView,指定一個LayoutManager和一個適配器
val layoutManager = LinearLayoutManager(this)
recycler_view.layoutManager = layoutManager
adapter = MsgAdapter(msgList)
recycler_view.adapter = adapter
btn_send.setOnClickListener(this)
}
fun initMsg() {
val msg1 = Msg("Hello guy.", Msg.TYPE_RECEIVED)
msgList.add(msg1)
val msg2 = Msg("Hello,who is that?", Msg.TYPE_SENT)
msgList.add(msg2)
val msg3 = Msg("Tom", Msg.TYPE_RECEIVED)
msgList.add(msg3)
}
override fun onClick(p0: View?) {
when (p0) {
btn_send -> {
//獲取EditText裏面的內容
val content = input_text.text.toString()
if (!content.isEmpty()) {
//如果不爲空,則新建Msg將其添加至msgList列表中
val msg = Msg(content, Msg.TYPE_SENT)
msgList.add(msg)
//notifyItemInserted通知列表有新數據插入,顯示出來,刷新RecyclerView中的顯示
adapter?.notifyItemInserted(msgList.size - 1)
//將RecyclerView顯示的數據定位至最後一行
recycler_view.scrollToPosition(msgList.size - 1)
//清空輸入框內容
input_text.setText("")
}
}
}
}
}
4.8.Kotlin之延遲初始化和密封類
4.8.1.對變量延遲初始化
很多全局變量不可能爲空,但由於Kotlin語法特性,你不得不做許多非空判斷保護,即使你很確定這玩意不可能爲空。
class MainActivity : AppCompatActivity(), View.OnClickListener {
//因爲初始化是在onCreate方法中進行,因此不得不將adapter賦值爲null,同時將他的類型聲明改成 MsgAdapter?
private var adapter: MsgAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
...
adapter = MsgAdapter(msgList)
}
override fun onClick(p0: View?) {
.....
//肯定要進行判空處理,否則編譯無法進行
adapter?.notifyItemInserted(msgList.size - 1)
....
}
}
上述的代碼全局變量實例越多,編寫額外的判空處理代碼就越多。因此我們使用lateinit延遲初始化對上面代碼進行優化,告訴編譯器我待會初始化,一開始不用置null。另外,我們需要判斷全局變量是否已經完成了初始化,以避免某個變量重複初始化。代碼示例如下:
class MainActivity : AppCompatActivity(), View.OnClickListener {
private lateinit var adapter: MsgAdapter
override fun onCreate(savedInstanceState: Bundle?) {
...
//判斷變量是否進行初始化,如果初始化,則不用重複對變量初始化,否則初始化
if(!::adapter.isInitialized) {
adapter = MsgAdapter(msgList)
}
}
override fun onClick(p0: View?) {
.....
adapter.notifyItemInserted(msgList.size - 1)
....
}
}
4.8.2.使用密封類優化代碼
目的是解決爲滿足編譯器要求編寫無用條件分支的情況。優化前代碼示例如下:
package com.example.myapplication
import java.lang.IllegalArgumentException
//定義一個接口,表示某個操作的執行結果
interface Result
//Success類用於表示成功時的結果
class Success(val msg: String) : Result
//Failure類用於表示失敗時的結果
class Failure(val error: String) : Result
//接受一個Result參數,通過判斷result類型返回不同的結果
fun getResultMsg(result: Result) = when (result) {
is Success -> result.msg
is Failure -> result.error
//else這塊是完全執行不到的,但缺少的話代碼將無法編譯過。
//如果新增一個UnKnown類並實現了Result接口,用於表示未知的執行結果,但忘記寫分支,將會拋出異常使程序崩潰
else -> throw IllegalArgumentException()
}
解決方法是使用Kotlin密封類,密封類的關鍵字是sealed class。優化後的代碼如下所示:
package com.example.myapplication
import java.lang.IllegalArgumentException
//密封類
sealed class Result
//繼承類需要後面加上一對括號
class Success(val msg: String) : Result()
class Failure(val error: String) : Result()
//class unkonwn(val time: String) : Result()
//else條件已經沒有了?爲什麼呢?
//when傳入密封類時,Kotlin會自動檢查該密封類有哪些子類,並強制要求你對每一個都需要處理(若不處理,編譯不會通過)。即使沒有else,也不會出現遺漏分支
fun getResultMsg(result: Result) = when (result) {
is Success -> result.msg
is Failure -> result.error
}