網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
1、背景
這天下班前,老板找到小莊:“有個(gè)頁(yè)面要優(yōu)化,小需求,你跟進(jìn)一下。”
小莊:“好的老板!”他看了看時(shí)間,忐忑地翻出原型,看到了這樣一個(gè)頁(yè)面:
需要優(yōu)化頁(yè)面的原型圖:
思索片刻后,小莊熟練地打開了某搜索引擎,沒(méi)有找到合適的輪子,小莊知道軟件開發(fā)的第一步必須是先進(jìn)行需求分析和設(shè)計(jì),而不是擼起袖子一把梭。于是他決定先分析下功能并整理思路。
2、分析
2.1功能分析
頁(yè)面的大致功能:
- 該頁(yè)面是個(gè)展示了某種流程的列表,每個(gè)列表項(xiàng)有不同的狀態(tài)(已完成、進(jìn)行中、未開始)
- 在列表的一側(cè)有個(gè)類似時(shí)間線的
view
,根據(jù)每個(gè)項(xiàng)的狀態(tài)不同,展示不同顏色的圓點(diǎn)和豎線
2.2細(xì)節(jié)分析
對(duì)于其中一個(gè)項(xiàng)的時(shí)間線view
,有哪些細(xì)節(jié)呢?
首先發(fā)現(xiàn),這個(gè)時(shí)間線view是由兩個(gè)大部分組成的,分別是:圓、線
然后我們自然可以注意到,在一個(gè)項(xiàng)的時(shí)間線中,又出現(xiàn)了兩種顏色:圓上面的線(以下簡(jiǎn)稱為上線)是綠色,圓本身和圓下面的線(以下簡(jiǎn)稱為下線)又是紅色
- 也就是說(shuō),這個(gè)view不僅要知道自身的顏色,還得知道上一個(gè)item是什么顏色的
- 也就是說(shuō),這個(gè)view的繪制應(yīng)該分成三個(gè)部分,分別是:上線、圓、下線
這是一個(gè)普通的中間的item。然而對(duì)于第一個(gè)item和最后一個(gè)item來(lái)說(shuō),它們是分別沒(méi)有上線和下線的
2.3方案設(shè)想
小莊的腦海里迅速地閃過(guò)了幾個(gè)想法:
第一個(gè)想法是根據(jù)數(shù)據(jù)的狀態(tài),在adapter中設(shè)置顏色和是否線顯示。
- 但是這么簡(jiǎn)單的圓和線還要找設(shè)計(jì)師要圖么?這樣豈不是顯得他很菜。那要用Drawable?然而將來(lái)要改顏色什么的,也是麻煩,而且要寫好幾個(gè)文件。所以這個(gè)想法很快就被pass了
第二個(gè)想法就是使用自定義view,在每一個(gè)item中畫出圓和線,然后用自定義屬性設(shè)置顏色。
- 他馬上寫了個(gè)
demo
嘗試了一下,結(jié)果是他自定義view學(xué)藝不精,遇到了難以解決的問(wèn)題[注],所以只能哭著放棄了
也許是命中注定他將推開一扇大門:旁友,也許你聽說(shuō)過(guò)RecyclerView.ItemDecoration嗎?
注:2000 years later,我發(fā)現(xiàn)我根本復(fù)現(xiàn)不出來(lái)那問(wèn)題,也許這就是緣分吧
RecyclerView.ItemDecoration簡(jiǎn)介
這是一款功能強(qiáng)大的神器,用來(lái)給列表添加分隔線只是它最常見又最普通的能力。這里簡(jiǎn)單介紹一下,不是本文的主要內(nèi)容。因?yàn)樗軐?shí)現(xiàn)的效果太多太厲害了,我學(xué)不過(guò)來(lái)(?_?)
實(shí)現(xiàn)自定義的一個(gè)ItemDecoration,需要繼承它并按需重寫以下兩個(gè)方法:
onDraw
:用于具體的繪制內(nèi)容
- 方法有個(gè)參數(shù)是
parent
:RecyclerView
,即列表本身,所以我們可以從這里獲取每個(gè)子項(xiàng)的內(nèi)容 - 要注意的是這個(gè)方法里的繪制維度是整個(gè)列表,所以我們需要遍歷列表,為每一個(gè)子項(xiàng)進(jìn)行計(jì)算位置和繪制
getItemOffsets
:用于控制item的四周的偏移量,onDraw
中繪制的內(nèi)容會(huì)在這些留白上畫出來(lái)
- 然而這個(gè)方法的繪制維度又是針對(duì)每一個(gè)itemView,所以設(shè)置的是每個(gè)item的上下左右邊距
3、編碼
小莊現(xiàn)在已經(jīng)有了基本的思路和知識(shí)儲(chǔ)備,他打開IDE準(zhǔn)備動(dòng)手編碼了。不過(guò)軟件開發(fā)是迭代的過(guò)程,即使是這樣的一個(gè)小需求,他也打算先從實(shí)現(xiàn)一個(gè)簡(jiǎn)單的版本開始。
3.1第一版
第一個(gè)版本,小莊打算只實(shí)現(xiàn)畫出圓和線的形狀,沒(méi)有狀態(tài)也沒(méi)有顏色,主要為了驗(yàn)證自己的想法是否可行,
具體的實(shí)現(xiàn)需要做以下幾個(gè)內(nèi)容:
準(zhǔn)備定義兩個(gè)重要屬性,它們將會(huì)參與計(jì)算位置和繪制內(nèi)容
-
radius
:用于確定圓的半徑 -
offset
:用于表示圓點(diǎn)到item頂部的距離
并且在getItemOffsets
中留出繪制整個(gè)時(shí)間線的空間,即item的左邊距
最重要的工作內(nèi)容是我們計(jì)算并繪制了圓和線(具體的計(jì)算可以看代碼)
class FirstVerTimeline : RecyclerView.ItemDecoration() { private val paint = Paint(Paint.ANTI_ALIAS_FLAG) var radius = 8f var offset = 15 override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { super.onDraw(c, parent, state) val count = parent.childCount for (i in 0 until count) { // 獲取當(dāng)前的itemView val itemView = parent.getChildAt(i) // 整個(gè)軸線的x坐標(biāo)都是相同的 val xPosition = radius // 畫上線。第一個(gè)item不畫 if (i != 0) { c.drawLine(xPosition, itemView.top.toFloat(), xPosition, itemView.top.toFloat() + offset, paint) } // 畫下線。最后一個(gè)item不畫 if (i != count - 1) { c.drawLine(xPosition, itemView.top + radius * 2 + offset, xPosition, itemView.bottom.toFloat(),paint) } // 畫圓 c.drawCircle(xPosition, itemView.top + offset + radius, radius, paint) } } override fun getItemOffsets(outRect: Rect, view: View, : RecyclerView, state: RecyclerView.State) { super.getItemOffsets(outRect, view, parent, state) // 設(shè)置item在左邊的偏移量 outRect.left = radius.toInt() * 2 } }
現(xiàn)在我們可以來(lái)定義一個(gè)虛擬的數(shù)據(jù)源Record
,把這個(gè)ItemDecoration
應(yīng)用到一個(gè)RecyclerView上康康效果:
rv_timeline1.adapter = RecordAdapter(ArrayList<Record>())// 省略構(gòu)造假數(shù)據(jù) rv_timeline1.addItemDecoration(FirstVerTimeline())
已經(jīng)初具規(guī)模了!只是時(shí)間線和文字之間擠了一點(diǎn),我們只需要加上一些合適的padding,換一下測(cè)試數(shù)據(jù),看起來(lái)就會(huì)像真的一樣了!
為了從圖1到達(dá)圖2,我們需要做:
定義paddingLeft和paddingRight屬性,用來(lái)表示軸線的左右padding
- 修改
getItemOffsets
為outRect.left = paddingLeft + paddingRight + radius.toInt() * 2
,留出偏移量的位置 - 修改xPosition的初始值為
radius + paddingLeft
,改變軸線的x坐標(biāo)
到這里第一個(gè)版本就算完成啦,第二個(gè)版本會(huì)有什么新功能呢↓↓↓
3.2第二版
小莊打算在第二版里實(shí)現(xiàn)狀態(tài)的不同顏色。為了實(shí)現(xiàn)這個(gè)需求,他陷入了深深的沉思:
- 數(shù)據(jù)類中肯定不可能耦合顏色這種UI實(shí)現(xiàn),所以需要一個(gè)由狀態(tài)獲取顏色的辦法
- 由于畫一個(gè)
item
還需要知道上一個(gè)item的顏色,干脆直接把整個(gè)數(shù)據(jù)源列表data傳入ItemDecoration
好了 - 結(jié)合以上兩點(diǎn),我們可以定義一個(gè)函數(shù)類型的屬性
var color: (item: T) -> Int
,實(shí)現(xiàn)這個(gè)屬性就可以讓使用者通過(guò)數(shù)據(jù)狀態(tài)設(shè)置想要的顏色了
函數(shù)類型是kotlin(或者說(shuō)函數(shù)式編程)的特性之一。如果是Java的話可以考慮用模板模式實(shí)現(xiàn),即定義一個(gè)抽象方法讓子類去重寫
class SecondVerTimeline<T> : RecyclerView.ItemDecoration() { // 其他屬性... var data: List<T> = ArrayList() //-->這里有更新,定義了數(shù)據(jù)源 var color: (item: T) -> Int = { _ -> Color.GRAY } //-->這里有更新,通過(guò)這個(gè)屬性設(shè)置顏色選擇策略 override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { super.onDraw(c, parent, state) val count = parent.childCount for (i in 0 until count) { // ... val adapterPosition = parent.getChildAdapterPosition(itemView) //-->這里有更新,獲取當(dāng)前項(xiàng)的真正位置 val item = data[adapterPosition] //-->這里有更新,獲取當(dāng)前項(xiàng)的數(shù)據(jù)源 // 畫上線。第一個(gè)item不畫 if (adapterPosition != 0) { paint.color = color(data[adapterPosition - 1]) //-->這里有更新,設(shè)置上線的顏色 c.drawLine(...) } paint.color = color(item) //-->這里有更新,設(shè)置圓和下線的顏色 // 畫下線。最后一個(gè)item不畫 if (adapterPosition != data.size - 1) {//-->這里有更新,改用數(shù)據(jù)源的大小判斷是否為最后一個(gè)item c.drawLine(...) } // 畫圓... } } // getItemOffsets... }
代碼中可能需要注意的點(diǎn):
- 繪制上線前,需要通過(guò)data數(shù)據(jù)源獲取到上一個(gè)item,并用color屬性獲得其狀態(tài)對(duì)應(yīng)的顏色
- 繪制圓和下線前,同樣需要改變到這一個(gè)item的顏色
- 用
parent.childCount
獲取到的子項(xiàng)數(shù)量指的是屏幕中可見的部分,必須要用parent.getChildAdapterPosition
獲取到該項(xiàng)在列表中的真正位置,才能確定下線要不要畫。否則會(huì)出現(xiàn)【當(dāng)前屏幕上可見的最后一項(xiàng)不是真正的最后一項(xiàng),但它卻沒(méi)有下線,但向下滑動(dòng)后它又有下線了】的尷尬場(chǎng)景 - 注意到此時(shí)用于判斷是否為最后一個(gè)item的方法,從count - 1變?yōu)榱薲ata.size - 1,用數(shù)據(jù)源的大小判斷,比count更加準(zhǔn)確(原因同上一條)
使用時(shí)也需要有一些變化:
- 把
data
設(shè)置給ItemDecoration
- 通過(guò)
color
屬性設(shè)置顏色策略
val secondVerTimeline = SecondVerTimeline<Record>() secondVerTimeline.data = records secondVerTimeline.color = { item -> when (item.status) { 1 -> color1 2 -> color2 ... } } rv_timeline2.addItemDecoration(secondVerTimeline)
然后就可以運(yùn)行看一下效果了:
哇哦,鵝妹子嚶,這樣就已經(jīng)實(shí)現(xiàn)根據(jù)狀態(tài)轉(zhuǎn)變顏色的功能了!第二版的功能也圓滿實(shí)現(xiàn)!
4、結(jié)語(yǔ)
后來(lái)小莊又根據(jù)UI一頓修修改改,很快就完成了這個(gè)需求~但是小莊是一個(gè)有追求的程序員,他開始思考起了這個(gè)代碼的擴(kuò)展性和通用性如何。不想不要緊,一想發(fā)現(xiàn)根本沒(méi)有鴨!如果產(chǎn)品想要把圓點(diǎn)變成圖片怎么辦?或者產(chǎn)品想要更隨風(fēng)飛翔自由是方向呢?
于是他想找個(gè)時(shí)間完善改進(jìn)一下這個(gè)ItemDecoration,最好能應(yīng)對(duì)產(chǎn)品的所有需求!具體升級(jí)內(nèi)容請(qǐng)看下集~
原文鏈接:https://juejin.cn/post/6844904110559330312
相關(guān)推薦
- 2022-12-29 Kotlin數(shù)據(jù)存儲(chǔ)方式全面總結(jié)講解_Android
- 2022-07-24 .Net行為型設(shè)計(jì)模式之職責(zé)鏈模式(Chain?of?Responsibility)_基礎(chǔ)應(yīng)用
- 2022-04-10 Blazor組件事件處理功能_基礎(chǔ)應(yīng)用
- 2023-01-14 C/C++高精度(加減乘除)算法的實(shí)現(xiàn)_C 語(yǔ)言
- 2022-06-06 ceph集群RadosGW對(duì)象存儲(chǔ)使用詳解_其它綜合
- 2022-03-14 has been blocked by CORS policy: Response to prefl
- 2022-12-23 Python?UI自動(dòng)化測(cè)試Web?frame及多窗口切換_python
- 2022-02-28 react高階函數(shù)和函數(shù)柯里化 學(xué)習(xí)
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過(guò)濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支