標籤列表選擇view:ChooseFlowView
主要是針對不規則ITEM TAG
標籤的流式LIST
佈局,如果是規則的用Recyclerview
就可以完全勝任了,而且還會有很好的內存管理,但是不規則的就需要自己來寫了,因爲文章可能比較長,這裏先放一下效果圖,在說之前有個大體的瞭解。
一、引入使用
本來想再單獨寫一篇文章介紹使用的,但是接入比較簡單,就直接說了
1.1 引入
根目錄build.gradle
maven {
url "https://dl.bintray.com/fanyafeng/ripple"
}
項目module
的build.gradle
implementation 'com.ripple.component:ui:0.0.6'
1.2 使用
1.2.1 定義數據model
因爲是演示,簡單定義了數據model
data class ChooseModel(var title: String, var checkable: Boolean, var checked: Boolean) :
IChooseModel {
override fun getChooseItemTitle(): String {
return title
}
override fun getChooseItemCheckable(): Boolean {
return checkable
}
override fun getChooseItemChecked(): Boolean {
return checked
}
override fun setChooseItemChecked(isChecked: Boolean) {
checked = isChecked
}
}
1.2.2 activity中使用
下面就是填充數據的操作,類似給adapter
設置數據
5.forEach {
val model = ChooseModel("我是第$it", it != 3, it == 0)
list.add(model)
val itemView = ChooseItemView(this)
itemView.setInnerTagWrapContent()
itemView.chooseViewUnselected = R.drawable.choose_view_normal
chooseItemView.addItemView(itemView, model)
}
1.2.3 獲取回調結果
chooseItemView.onItemClickListener={ first, position, third, fourth, fifth ->
//不論按鈕狀態,只要點擊就會有回調
Log.d(TAG, "被點擊:" + position)
}
chooseItemView.onItemAbleClickListener = { view, position, model ->
Log.d(TAG, "被點擊:" + position)
showToast("被點擊:" + position)
}
chooseItemView.onItemUnableClickListener = { view, position, model ->
Log.d(TAG, "不能被點擊的被點擊了:" + position)
showToast("不能被點擊的被點擊了:" + position)
}
二、設計原理
老生常談的一件事,寫通用類的東西需要對修改關閉對擴展開放,再者就是使用者接入成本必須要小,最好帶有默認實現,但是又需要支持用戶對每一個細節的修改可以做到定製化。
2.1 需求簡介
首先,gif
圖分爲三部分,第一部分是像列表選擇控件隨意添加view,第二個就是這裏要說的重點ChooseFlowView
,第三個是修改佈局以及回調結果
下面來細說ChooseFLowView
,這裏就是大家所熟悉的某寶或者某東商品詳情頁的多規格選擇的view
,需求就是按照他們的需求來的,這樣需求就確定了
列一下需求表:
- 列表的
ITEM
有三種狀態:選中,非選中,不可選 - 列表最大和最小選取數量,超過最大數量後按照
FIFO
算法更新選中列表 - 三種狀態的
ITEM
點擊回調都需要監聽結果 - 數據的填充以及樣式的定製
- 列表更新時重用列表(算是內部調優吧)
2.2 設計思想
通過以上需求分析可以將這個ChooseFlowView
的骨架圖列出來了,因爲是做通用的view
,這裏還是老樣子採用**接口和泛型**去進行具體的實現
2.2.1 首先來看ITEM的抽象
/**
* Author: fanyafeng
* Data: 2020/6/28 19:24
* Email: [email protected]
* Description: 多項選擇view控件的單個控件的行爲
*/
interface IChooseItemView:Serializable {
/**
* 是否可以被選中
*/
fun isCheckable(): Boolean
/**
* 設置其是否可以被選中
*/
fun setCheckable(isCheckable: Boolean)
/**
* 是否被選中
*/
fun isChecked(): Boolean
/**
* 設置其被選中
*/
fun setChecked(isChecked: Boolean)
/**
* Change the checked state of the view to the inverse of its current state
*/
fun toggle()
}
2.2.2 再來看數據接口的定義
/**
* Author: fanyafeng
* Data: 2020/6/29 09:31
* Email: [email protected]
* Description: 流式佈局item
*
* data model需要繼承這個接口
*/
interface IChooseModel : Serializable {
/**
* 單個view標題
*/
fun getChooseItemTitle(): String
/**
* 是否可以被點擊
*/
fun getChooseItemCheckable(): Boolean
/**
* 是否被選中
*/
fun getChooseItemChecked(): Boolean
/**
* 當有最大選取數量時控件會根據FIFO更新data model
*/
fun setChooseItemChecked(isChecked: Boolean)
}
2.2.3 下面就是ChooseFlowView提供的功能
/**
* Author: fanyafeng
* Data: 2020/6/29 09:53
* Email: [email protected]
* Description: 選取模式
* 分爲單選和多選
*/
interface IChooseFlowView : Serializable {
/**
* 設置最大選取數量
*/
fun setMaxChooseCount(maxCount: Int)
/**
* 最大的選取數量,默認爲1
*/
fun getMaxChooseCount(): Int
/**
* 獲取最小選取數量
*/
fun getMinChooseCount(): Int
/**
* 設置最小選取數量
* 默認爲0,並且最大數量不能小於最小數量,但是可以相等
*/
fun setMinChooseCount(minCount: Int)
}
三、實現
當時關於ChooseFlowView
的定製方面有過好多想法,但是沒想到好的實現方法,後來一邊寫一邊改,最後選取了一種相對比較好方案
3.1 view定製
首先是tag view
,這個會有一個默認的實現,但是默認實現是實現了IChooseItemView
接口的,因爲要統一行爲,所以必須要實現此接口,同時當給ChooseFlowView
設置ITEM
時也是需要實現這個接口的,再有就是data model
,它是對數據類型的抽象,這裏說的話比較抽象,先大體來看一下方法的定義:
fun <T : ChooseItemView, M : IChooseModel> addItemView(
itemView: T,
model: M,
params: LayoutParams? = null
)
支持用戶添加自定義view
,但是model
也是要實現IChooseModel
的。
3.2 更新view
更新ChooseFlowView
有一種簡單粗暴的方法就是幹掉所有ITEM
再去新加,但是這樣不太好,這裏可以仿照Recyclerview ViewHolder
的方案,有的話就拿來再去更新,沒用的話就刪除,但是重用的問題還是需要在用之前進行重置,這裏要切記。
3.3 操作
這裏算是文章的重點了,也是ChooseFlowView
實現的核心代碼最多的地方了
- 用戶首次填充數據初始化用戶界面之前用戶可選的最大數量和最小數量時確定的,爲什麼呢,因爲用戶可能過來一堆數據但是裏面的選中態超過最大值,這時候就需要控件按照
FIFO
(符合用戶的選取習慣)去篩選,同樣在用戶更新數據事也會遇到相同的問題,這樣數據的顯示就解決了 - 以上數據篩選完成後便初始化頁面,此時頁面是按照用戶的要求展示的,並且此時獲取的結果是可靠的,而且是符合要求的
- 涉及到用戶點擊操作時,當需要有多選,單選,反選的情況時可以去設置最大數量,最小數量來控制**(PS:後面可以添加是否支持反選,但是感覺意義不大暫時先擱置)**
- 更新
ChooseFlowView
,更新頁面時會重新篩選選中數據,這裏有三種情況,新數據大於,等於,小於舊數據,相等的話是最好處理的,只需要更新data list
刷新頁面即可,小於的話需要將多餘的view
進行remove
同時刷新頁面,大於的話就需要去新建ITEM
再將其加入到ChooseFlowView
中
3.4 核心代碼
3.4.1 初始化數據
/**
* 填充數據
* 一般都是動態填充
*/
@JvmOverloads
fun <T : ChooseItemView, M : IChooseModel> addItemView(
itemView: T,
model: M,
params: LayoutParams? = null
) {
position++
allModelList.add(model)
itemView.initData(model)
itemView.tag = position
val initCount = selectList.size
if (model.getChooseItemChecked()) {
if (initCount >= maxCount) {
val first = selectList.first
(getChildAt(first) as ChooseItemView).toggle()
setItemCheckStatus(selectList.first,false)
selectList.removeFirst()
selectList.addLast(position)
setItemCheckStatus(position,true)
} else {
selectList.addLast(position)
}
}
itemView.setOnClickListener {
val pos = it.tag as Int
val isCheckable: Boolean
/**
* 小於最小數量想取消選中但是不可以
* 標記爲重複選取,不相應操作
*/
var checkRepeat = true
if (itemView.isCheckable()) {
isCheckable = true
val mCount = selectList.size
if (itemView.isChecked()) {
//取消選中
if (mCount <= minCount) {
//當用戶選取數量小於最小數量時不允許取消選中
checkRepeat = false
} else {
selectList.remove(pos)
setItemCheckStatus(pos,false)
itemView.toggle()
}
} else {
if (mCount >= maxCount) {
//取消第一個加入最後一個
val first = selectList.first
(getChildAt(first) as ChooseItemView).toggle()
itemView.toggle()
setItemCheckStatus(selectList.first,false)
selectList.removeFirst()
selectList.addLast(pos)
setItemCheckStatus(pos,true)
} else {
//添加選中
itemView.toggle()
selectList.addLast(pos)
setItemCheckStatus(pos,true)
}
}
onItemAbleClickListener?.invoke(it, pos, model)
} else {
isCheckable = false
onItemUnableClickListener?.invoke(it, pos, model)
}
onItemClickListener?.invoke(it, pos, model, isCheckable, checkRepeat)
}
if (params != null) {
addView(itemView, params)
} else {
addView(itemView)
}
}
3.4.2 更新數據
/**
* 更新當前的view
* 爲了不去每次都添加刪除單個的view
* 進行原有view的重用
*/
fun <T : ChooseItemView> updateView(list: List<Pair<IChooseModel, T>>) {
selectList.clear()
val newCount = list.size
val oldCount = allModelList.size
if (newCount == oldCount) {
list.forEachIndexed { index, model ->
val chooseModel = model.first
updateSelectList(index, chooseModel)
//更新原有的model列表
allModelList[index] = chooseModel
//更新原有的view顯示
(getChildAt(index) as ChooseItemView).initData(chooseModel)
}
} else if (newCount > oldCount) {
list.forEachIndexed { index, model ->
val chooseModel = model.first
updateSelectList(index, chooseModel)
//新數據與原數據重疊部分
if (index < oldCount) {
allModelList[index] = chooseModel
(getChildAt(index) as ChooseItemView).initData(chooseModel)
} else {
addItemView(model.second, chooseModel)
}
}
} else {
list.forEachIndexed { index, model ->
val chooseModel = model.first
updateSelectList(index, chooseModel)
//更新原有的model列表
allModelList[index] = chooseModel
//更新原有的view顯示
(getChildAt(index) as ChooseItemView).initData(chooseModel)
//更新選中態
setItemCheckStatus(index,true)
}
(newCount until oldCount).forEach {
allModelList.removeAt(it)
removeViewAt(it)
}
}
}
/**
* 更新選中列表
* 正常情況應該是外部控制,但是因爲顯示的問題內部進行了重新的篩選
* 按理說控件不能修改用戶的datamodel,可是如果用戶傳入的數據有問題的話需要用戶自己去檢查
* 此時控件會更新數據的選中態
* 但是本地選中的結果是正常的,算法是FIFO
* 所以在此時獲取的選擇用戶是完全可以信任的
*/
private fun updateSelectList(selectPosition: Int, chooseModel: IChooseModel) {
val initCount = selectList.size
//先去判斷當前item是否是選中狀態
if (chooseModel.getChooseItemChecked()) {
//如果是選中狀態,並且被選中的數量大於最大的可選數量
if (initCount >= maxCount) {
//首先更新被選中的第一個數據model
setItemCheckStatus(selectList.first,false)
//取消選中還需要更新控件狀態
(getChildAt(selectList.first) as ChooseItemView).toggle()
//此時需要把第一個item刪除
selectList.removeFirst()
//同時將選中狀態的item添加到選中列表的最後位置
//以下同理
selectList.addLast(selectPosition)
//此時更新被選中態item
setItemCheckStatus(selectPosition,true)
} else {
selectList.addLast(selectPosition)
setItemCheckStatus(selectPosition,true)
}
}
}