【Android 6.0】RecyclerView - 高效能的ListView
現在要介紹的是RecyclerView,這應該是手機上最常用的元件之一,就是列表。RecyclerView可以說是ListView的改良版,和ListView相同的是,RecyclerView一樣是由配置器(Adapter)來處理ItemView呈現的內容,不同的是,RecyclerView有實做Recycle機制,就像iOS的UITableView的Reuse機制。話不多說,現在就來一步步做一個長得像這樣的APP吧,以下的說明還是會以iOS做類比。
Toolbar
關閉ActionBar
- 因為一開始Android就有內建ActionBar,不過隨著時代的演進已經不敷使用了,所以現在就使用比較新的Toolbar。所以首先第一件事就是到androidmanifest.xml去把ActionBar關掉。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.recyclerview">
<application
...
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
設定Toolbar
- 接下來呢,就跟一般使用UI元件一樣,利用findViewById()之類的方式去處理,把Toolbar元件拉一拉,利用setSupportActionBar()加到畫面上。
class MainActivity : AppCompatActivity() {
private lateinit var toolbar: Toolbar
private lateinit var recyclerView: RecyclerView
private lateinit var alertDialog: AlertDialog
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initIdentity()
toolbarSetting("我是Toolbar")
}
/// 初始化UI的ID
private fun initIdentity() {
toolbar = findViewById(R.id.toolbar)
}
/// 設定Toolbar (加在畫面上)
private fun toolbarSetting(title: String) {
val backgroundColor = resources.getColor(R.color.colorPrimaryDark, null)
val titleTextColor = resources.getColor(R.color.colorPrimary, null)
toolbar.title = title
toolbar.setBackgroundColor(backgroundColor)
toolbar.setTitleTextColor(titleTextColor)
toolbar.setNavigationIcon(R.drawable.ic_add_box_black_24dp)
setSupportActionBar(toolbar)
}
}
讓Toolbar的按鍵按下有反應
- 主要是設定setNavigationOnClickListener()這個Listener。
class MainActivity : AppCompatActivity() {
/// 設定ToolbarIcon按下的Listener
private fun toolbarIconOnClickSetting() {
toolbar.setNavigationOnClickListener {
Toast.makeText(this, "我是Toolbar Icon", Toast.LENGTH_SHORT).show()
}
}
}
RecyclerView
設定RecyclerView
- 接下來就是加上主角RecyclerView,如果沒有的話,也在Paleete點擊下載。另外Android出了一個[ConstraintLayout],這個就有iOS AutoLayout的影子,不得不說在畫面的設定上,Xcode的確比較人性化一點。
使用RecyclerView
- 這裡主要要說明的是使用RecyclerView.Adapter,它有Reuse的機制,所以會類似UITableView的Delegate / DataSource一樣,會要實作三個interface (DataSource?)。這裡跟UITableView不一樣的地方是,iOS的DataSource會跟UITableView綁在一起,但是Android的是跟Adapter綁在一起。
class ModelAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> {
/// cell的數量 (tableView(_:numberOfRowsInSection:))
override fun getItemCount(): Int {}
/// cell的長相 (tableView(_:cellForRowAt:))
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : RecyclerView.ViewHolder {}
/// cell的設定 (tableView(_:cellForRowAt:) + tableView(_:didSelectRowAt:))
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {}
}
實做interface
- 接下來我們就把假資料放到RecyclerView內,讓它有真實資料的產生。這裡要說明的是,在設定item畫面的時候,有一個叫做layout_weight - 權重的設定,在iOS也有這樣的東西去設定大小比例。在這裡就可以試試RecyclerView的基本功能是否正常。
class ModelAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> {
private var context: Context
private var modelArray: ArrayList<CellModel>
/// 初始化資料 (UITableViewDelegate)
constructor(context: Context, modelArray: ArrayList<CellModel>) : super() {
this.context = context
this.modelArray = modelArray
}
/// cell的數量 (tableView(_:numberOfRowsInSection:))
override fun getItemCount(): Int {
return modelArray.count()
}
/// cell的長相 (tableView(_:cellForRowAt:))
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : RecyclerView.ViewHolder {
val cellView = LayoutInflater.from(context).inflate(R.layout.recyclerview_cell, parent, false)
return CellHolder(cellView)
}
/// cell的設定 (tableView(_:cellForRowAt:) + tableView(_:didSelectRowAt:))
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val cellHolder = holder as? CellHolder ?: return
val modelData = modelArray[position]
holder.nameTextView.text = modelData.name
holder.regionTextView.text = modelData.region
}
}
object Utility {
/// 測試用假資料 (Cell)
fun demoModelData(count: Int) : ArrayList<CellModel> {
var modelArray = ArrayList<CellModel>()
for (index in 0..count) {
var model = CellModel()
model.name = "William - $index"
model.region = "Earth - $index"
modelArray.add(model)
}
return modelArray
}
}
// MARK: - 基本的Model長相
interface BaseModel {}
// MARK: - CellModel (內容的Model)
class CellModel : BaseModel {
var name: String? = null
var region: String? = null
}
class MainActivity : AppCompatActivity() {
private lateinit var recyclerView: RecyclerView
override fun onCreate(savedInstanceState: Bundle?) {
...
recyclerViewSetting()
recyclerView.adapter = modelAdapterMaker(20)
}
/// 初始化UI的ID
private fun initIdentity() {
...
recyclerView = findViewById(R.id.recyclerView)
}
/// [假的] RecyclerView上的資料
private fun modelAdapterMaker(count: Int) : ModelAdapter {
return ModelAdapter(this, Utility.demoModelData(20))
}
}
RecyclerView Holder
- 接下來我們可以看到RecyclerView實際上是在Toolbar的下方,所以第一個Cell是看不到的,所以要手動加上一個空白的Header,讓RecyclerView的資料能完整的顯示出來。為了讓Header跟Toolbar一樣大,高度是要設定成「?attr/actionBarSize」,然後利用「BaseModel」來做基本類型的使用,用getItemViewType(position: Int)來區分類別。
// MARK: - Cell的類型 (Header / Cell)
enum class CellType(val value: Int) {
Header(0),
Cell(1),
}
// MARK: - 基本的Model長相
interface BaseModel {
val cellType: CellType
}
// MARK: - HeaderModel (內容的Model)
class HeaderModel : BaseModel {
override val cellType: CellType
get() = CellType.Header
}
// MARK: - CellModel (內容的Model)
class CellModel : BaseModel {
override val cellType: CellType
get() { return CellType.Cell }
var name: String? = null
var region: String? = null
}
object Utility {
/// 空白的Header
fun headerModelData() : HeaderModel {
return HeaderModel()
}
}
class ModelAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> {
private var context: Context
private var modelArray: ArrayList<BaseModel>
/// 初始化資料 (UITableViewDelegate)
constructor(context: Context, modelArray: ArrayList<BaseModel>) : super() {
this.context = context
this.modelArray = modelArray
}
/// cell的數量 (tableView(_:numberOfRowsInSection:))
override fun getItemCount(): Int {
return modelArray.count()
}
/// cell的長相 (tableView(_:cellForRowAt:))
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : RecyclerView.ViewHolder {
val viewTypeEnum = CellType.values()[viewType]
var viewHolder: RecyclerView.ViewHolder
when(viewTypeEnum) {
CellType.Header -> { viewHolder = headerViewMaker(parent) }
CellType.Cell -> { viewHolder = cellViewMaker(parent) }
}
return viewHolder
}
/// cell的設定 (tableView(_:cellForRowAt:) + tableView(_:didSelectRowAt:))
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when(holder) {
is HeaderHolder -> { headerHolderSetting(holder, position) }
is CellHolder -> { cellHolderSetting(holder, position) }
}
}
/// item的類型 (cell or header)
override fun getItemViewType(position: Int): Int {
val cellType = modelArray[position].cellType
return cellType.value
}
/// XML -> HeaderView -> Holder
private fun headerViewMaker(parent: ViewGroup): HeaderHolder {
val headerView = LayoutInflater.from(context).inflate(R.layout.recyclerview_header, parent, false)
return HeaderHolder(headerView)
}
/// XML -> CellView -> Holder
private fun cellViewMaker(parent: ViewGroup): CellHolder {
val cellView = LayoutInflater.from(context).inflate(R.layout.recyclerview_cell, parent, false)
return CellHolder(cellView)
}
/// Header的內容細部設定
private fun headerHolderSetting(holder: HeaderHolder, position: Int) {
holder.itemView.setBackgroundColor(0x00ff00)
}
/// Cell的內容細部設定 [假資料]
private fun cellHolderSetting(holder: CellHolder, position: Int) {
val modelData = modelArray[position] as? CellModel ?: return
holder.nameTextView.text = modelData.name
holder.regionTextView.text = modelData.region
}
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
recyclerView.adapter = modelAdapterMaker(20)
}
/// [假的] RecyclerView上的資料 (Header +Cells)
private fun modelAdapterMaker(count: Int) : ModelAdapter {
var modelArrayList = ArrayList<BaseModel>()
val demoModelArray = Utility.demoModelData(count)
modelArrayList.add(headerModelData())
modelArrayList.all(demoModelArray)
return ModelAdapter(this, modelArrayList)
}
}
使用第三方套件
安裝第三方套件
- Android Studio在安裝第三方套件的方面就比Xcode來的好多了。直接在build.gradle上implementation套件的全名,然後按下右上角的「Sync Now」就可以了,這裡要安裝的是okhttp - 網路連線套件 + Gson - 解json套件。
WebAPI
- 這裡要真正從網路上取得資料,利用okhttp來讀取API - uinames的資料,當然跟iOS一樣,要先打開網路的權限才可以喲。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.recyclerview">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
getUINamesBody(10) { responseBody ->
responseBody?.let {
Log.e("[ResponseBody]", "${responseBody.toString()}")
}
}
}
/// 取得API的資料 (非同步)
private fun getUINamesBody(count: Int, callback: (ResponseBody?) -> Unit) {
val urlString = "https://uinames.com/api/?amount=$count"
val request = Request.Builder().get().url(urlString).build()
val newCall = OkHttpClient().newCall(request)
newCall.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
callback(null)
}
override fun onResponse(call: Call, response: Response) {
callback(response.body)
}
})
}
/// [真的] RecyclerView上的資料 (Header + Sections + Cells)
private fun webModelAdapterMaker(models: ArrayList<UINameModel>) : ModelAdapter {
val demoSectionData = Utility.demoSectionData(5)
var modelArrayList = ArrayList<BaseModel>()
modelArrayList.add(headerModelData())
for (sectionData in demoSectionData) {
modelArrayList.add(sectionData)
modelArrayList.addAll(models)
}
return ModelAdapter(this, modelArrayList)
}
}
JSON ==> Model
- 利用Gson,將JSON字串轉成model
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
getUINamesBody(10) { responseBody ->
responseBody?.let {
val jsonString = responseBody.string()
val arrayType = object : TypeToken<ArrayList<UINameModel>>(){}.type
val uiNameModelArray = Gson().fromJson<ArrayList<UINameModel>>(jsonString, arrayType)
Log.e("[GSON]", "$jsonString")
for (model in uiNameModelArray) {
Log.e("[GSON]", ${model.name})
}
}
}
}
}
// MARK: - UINameModel (內容的Model)
class UINameModel : BaseModel {
override val cellType: CellType
get() { return CellType.Cell }
@SerializedName("name")
var name: String? = null
@SerializedName("region")
var region: String? = null
}
讀取真實資料
- 這裡將從網路讀來的資料加入,然後再加上Section資料,完成一個簡單的RecyclerView功能。比較要注意的是,如果要更新主畫面的話,也是一樣要在runOnUiThread - main queue中執行。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
getUINamesBody(20) { responseBody ->
responseBody?.let {
val uiNameModelArray = nameModelArrayMaker(responseBody)
runOnUiThread {
recyclerView.adapter = webModelAdapterMaker(uiNameModelArray)
}
}
}
}
/// [GSON] 將 ResponseBody => JSON String => ArrayList<UINameModel>
private fun nameModelArrayMaker(responseBody: ResponseBody) : ArrayList<UINameModel> {
val jsonString = responseBody.string()
val arrayType = object : TypeToken<ArrayList<UINameModel>>(){}.type
return Gson().fromJson<ArrayList<UINameModel>>(jsonString, arrayType)
}
}
// MARK: - Cell的類型 (Header / Cell)
enum class CellType(val value: Int) {
Header(0),
Cell(1),
Section(2),
}
// MARK: - 基本的Model長相
interface BaseModel {
val cellType: CellType
}
// MARK: - HeaderModel (Header的Model)
class HeaderModel : BaseModel {
override val cellType: CellType
get() = CellType.Header
}
// MARK: - SectionModel (Section的Model)
class SectionModel : BaseModel {
override val cellType: CellType
get() = CellType.Section
var title: String? = null
}
// MARK: - CellModel (內容的Model)
class CellModel : BaseModel {
override val cellType: CellType
get() { return CellType.Cell }
var name: String? = null
var region: String? = null
}
// MARK: - UINameModel (內容的Model)
class UINameModel : BaseModel {
override val cellType: CellType
get() { return CellType.Cell }
@SerializedName("name")
var name: String? = null
@SerializedName("region")
var region: String? = null
}
// MARK: - 類似UITableView的Delegate / DataSource
class ModelAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> {
private var context: Context
private var modelArray: ArrayList<BaseModel>
/// 初始化資料 (UITableViewDelegate)
constructor(context: Context, modelArray: ArrayList<BaseModel>) : super() {
this.context = context
this.modelArray = modelArray
}
/// cell的數量 (tableView(_:numberOfRowsInSection:))
override fun getItemCount(): Int {
return modelArray.count()
}
/// cell的長相 (tableView(_:cellForRowAt:))
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : RecyclerView.ViewHolder {
val viewTypeEnum = CellType.values()[viewType]
return when(viewTypeEnum) {
CellType.Header -> { headerViewMaker(parent) }
CellType.Section -> { sectionViewMaker(parent) }
CellType.Cell -> { cellViewMaker(parent) }
}
}
/// cell的設定 (tableView(_:cellForRowAt:) + tableView(_:didSelectRowAt:))
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when(holder) {
is HeaderHolder -> { headerHolderSetting(holder, position) }
is SectionHolder -> { sectionHolderSetting(holder, position) }
is CellHolder -> { nameHolderSetting(holder, position) }
}
}
/// item的類型 (cell or header)
override fun getItemViewType(position: Int): Int {
val cellType = modelArray[position].cellType
return cellType.value
}
/// XML -> HeaderView -> Holder
private fun headerViewMaker(parent: ViewGroup): HeaderHolder {
val headerView = LayoutInflater.from(context).inflate(R.layout.recyclerview_header, parent, false)
return HeaderHolder(headerView)
}
/// XML -> SectionView -> Holder
private fun sectionViewMaker(parent: ViewGroup): SectionHolder {
val cellView = LayoutInflater.from(context).inflate(R.layout.recyclerview_section, parent, false)
return SectionHolder(cellView)
}
/// XML -> CellView -> Holder
private fun cellViewMaker(parent: ViewGroup): CellHolder {
val cellView = LayoutInflater.from(context).inflate(R.layout.recyclerview_cell, parent, false)
return CellHolder(cellView)
}
/// Header的內容細部設定
private fun headerHolderSetting(holder: HeaderHolder, position: Int) {
holder.itemView.setBackgroundColor(0x00ff00)
}
/// Section的內容細部設定
private fun sectionHolderSetting(holder: SectionHolder, position: Int) {
val modelData = modelArray[position] as? SectionModel ?: return
holder.titleTextView.text = modelData.title
}
/// Cell的內容細部設定 [假資料]
private fun cellHolderSetting(holder: CellHolder, position: Int) {
val modelData = modelArray[position] as? CellModel ?: return
holder.nameTextView.text = modelData.name
holder.regionTextView.text = modelData.region
}
/// Cell的內容細部設定 [Web]
private fun nameHolderSetting(holder: CellHolder, position: Int) {
val modelData = modelArray[position] as? UINameModel ?: return
holder.nameTextView.text = modelData.name
holder.regionTextView.text = modelData.region
}
}
AlertDailog
- 如果再加上一個「讀取轉圈圈」就更完美了,這裡使用「AlertDailog」來做ProgressBar。
<resources>
<!-- 透明的AlertDailog -->
<style name="TransparentAlertDialog">
<item name="android:windowFrame">@null</item>
<item name="android:windowIsFloating">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowTitleStyle">@null</item>
<item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
<item name="android:windowSoftInputMode">stateUnspecified|adjustPan</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:background">@android:color/transparent</item>
</style>
</resources>
class MainActivity : AppCompatActivity() {
/// 初始化AlertDialog
private fun progressAlertDialogSetting() {
val progressView = layoutInflater.inflate(R.layout.progressbar_dialog, null)
val builder = AlertDialog.Builder(this, R.style.TransparentAlertDialog)
builder.setView(progressView)
builder.setCancelable(false)
alertDialog = builder.create()
}
}
更換ICON圖示
- 利用免費的MakeAppIcon來製做APP ICON,然後放入「res」下的相關位置就可以了喲。
範例程式碼下載
後記
- 果然是隔行如隔山啊,寫起來超沒效率的,不過為了更上一層樓是值得的,加油。