又來新需求了,急,Android怎麼實現時間線效果(上)?

NO IMAGE

背景

這天下班前,老闆找到小莊:有個頁面要優化,小需求,你跟進一下。
小莊:好的老闆! 他看了看時間,忐忑地翻出原型,看到了這樣一個頁面:

又來新需求了,急,Android怎麼實現時間線效果(上)?

思索片刻後,小莊熟練地打開了某搜索引擎,沒有找到合適的輪子,小莊知道軟件開發的第一步必須是先進行需求分析和設計,而不是擼起袖子一把梭。於是他決定先分析下功能並整理思路。

預警:本文非常囉嗦,而且沒有乾貨(害怕.jpg)

分析

功能分析

頁面的大致功能:

  • 該頁面是個展示了某種流程的列表,每個列表項有不同的狀態(已完成、進行中、未開始)
  • 在列表的一側有個類似時間線的view,根據每個項的狀態不同,展示不同顏色的圓點和豎線

細節分析

某一個項的時間線view,其中有哪些細節呢?

又來新需求了,急,Android怎麼實現時間線效果(上)?

  • 首先發現,這個時間線view是由兩個大部分組成的,分別是:圓、線
  • 然後我們自然可以注意到,在一個項的時間線中,又出現了兩種顏色:圓上面的線(以下簡稱為上線)是綠色,圓本身圓下面的線(以下簡稱為下線)又是紅色
    • 也就是說,這個view不僅要知道自身的顏色,還得知道上一個item是什麼顏色的
    • 也就是說,這個view的繪製應該分成三個部分,分別是:上線、圓、下線
  • 這是一個普通的中間的item。然而對於第一個item和最後一個item來說,它們是分別沒有上線和下線的

方案設想

小莊的第一個想法是使用自定義view,在每一個item中畫出圓和線,然後用自定義屬性設置顏色。可是命中註定他將推開一扇大門:旁友,也許你聽說過RecyclerView.ItemDecoration嗎?

RecyclerView.ItemDecoration簡介
這是一款功能強大的神器,用來給列表添加分隔線只是它最常見又最普通的能力。這裡簡單介紹一下,不是本文的主要內容。因為它能實現的效果太多太厲害了,我學不過來(ಥ_ಥ)

實現自定義的一個ItemDecoration,需要繼承它並按需重寫以下兩個方法:

  • onDraw:用於具體的繪製內容
    • 方法有個參數是parent: RecyclerView,即列表本身,所以我們可以從這裡獲取每個子項的內容,也就可以得到上一個項的顏色了
    • 要注意的是這個方法裡的繪製維度是整個列表,所以我們需要遍歷列表,為每一個子項進行計算位置和繪製
  • getItemOffsets:用於控制item的四周的偏移量,onDraw中繪製的內容會在這些留白上畫出來
    • 然而這個方法的繪製維度又是針對每一個itemView,所以設置的是每個item的上下左右邊距

具體學習ItemDecoration推薦這篇文章!講解的十分清楚,超讚!

開始編碼

小莊現在已經有了基本的思路和知識儲備,他打開IDE準備動手編碼了。不過軟件開發是迭代的過程,即使是這樣的一個小需求,他也打算先從實現一個簡單的版本開始。

第一版

第一個版本,小莊打算只實現畫出圓和線的形狀,沒有狀態也沒有顏色,主要為了驗證自己的想法是否可行,具體的實現需要做以下幾個內容:

  • 準備定義兩個重要屬性,它們將會參與計算位置和繪製內容
    • radius:用於確定圓的半徑
    • offset:用於表示圓點到item頂部的距離
  • 並且在getItemOffsets中留出繪製整個時間線的空間,即item的左邊距
  • 最重要的工作內容是我們計算並繪製了圓和線(具體的計算可以看代碼)
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) {
// 獲取當前的itemView
val itemView = parent.getChildAt(i)
// 整個軸線的x座標都是相同的
val xPosition = radius
// 畫上線。第一個item不畫
if (i != 0) {
c.drawLine(xPosition, itemView.top.toFloat(),
xPosition, itemView.top.toFloat() + offset, paint)
}
// 畫下線。最後一個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)
// 設置item在左邊的偏移量
outRect.left = radius.toInt() * 2
}
}

現在我們可以來定義一個虛擬的數據源Record,把這個ItemDecoration應用到一個RecyclerView上康康效果:

rv_timeline1.adapter = RecordAdapter(ArrayList<Record>())// 省略構造假數據
rv_timeline1.addItemDecoration(FirstVerTimeline())

又來新需求了,急,Android怎麼實現時間線效果(上)?

已經初具規模了!只是時間線和文字之間擠了一點,我們只需要加上一些合適的padding,看起來就會像真的一樣了!

又來新需求了,急,Android怎麼實現時間線效果(上)?

為了從圖1到達圖2,我們需要做:

  • 定義paddingLeftpaddingRight屬性,用來表示軸線的左右padding
    • 修改getItemOffsetsoutRect.left = paddingLeft + paddingRight + radius.toInt() * 2,留出偏移量的位置
    • 修改xPosition的初始值為radius + paddingLeft,改變軸線的x座標

到這裡第一個版本就算完成啦,第二個版本會有什麼新功能呢

第二版

小莊打算在第二版裡實現狀態的不同顏色。為了實現這個需求,他陷入了深深的沉思:

  • 數據類中肯定不可能耦合顏色這種UI實現,所以需要一個由狀態獲取顏色的辦法
  • 由於畫一個item還需要知道上一個item的顏色,乾脆直接把整個數據源列表data傳入ItemDecoration好了
  • 結合以上兩點,我們可以定義一個函數類型的屬性var color: (item: T) -> Int,實現這個屬性就可以讓使用者通過數據狀態設置想要的顏色了

函數類型是kotlin(或者說函數式編程)的特性之一。如果是Java的話可以考慮用模板模式實現,即定義一個抽象方法讓子類去實現

class SecondVerTimeline<T> : RecyclerView.ItemDecoration() {
// 其他屬性...
var data: List<T> = ArrayList()  //-->這裡有更新,定義了數據源
var color: (item: T) -> Int = { _ -> Color.GRAY }  //-->這裡有更新,通過這個屬性設置顏色選擇策略
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)  //-->這裡有更新,獲取當前項的真正位置
val item = data[adapterPosition]  //-->這裡有更新,獲取當前項的數據源
// 畫上線。第一個item不畫
if (adapterPosition != 0) {
paint.color = color(data[adapterPosition - 1])  //-->這裡有更新,設置上線的顏色
c.drawLine(...)
}
paint.color = color(item)  //-->這裡有更新,設置圓和下線的顏色
// 畫下線。最後一個item不畫
if (adapterPosition != data.size - 1) {//-->這裡有更新,改用數據源的大小判斷是否為最後一個item
c.drawLine(...)
}
// 畫圓...
}
}
// getItemOffsets...
}

代碼中可能需要注意的點:

  • 繪製上線前,需要通過data數據源獲取到上一個item,並用color屬性獲得其狀態對應的顏色
  • 繪製圓和下線前,同樣需要改變到這一個item的顏色
  • parent.childCount獲取到的子項數量指的是屏幕中可見的部分,必須要用parent.getChildAdapterPosition獲取到該項在列表中的真正位置,才能確定下線要不要畫。否則會出現【當前屏幕上可見的最後一項不是真正的最後一項,但它卻沒有下線,但向下滑動後它又有下線了】的尷尬場景
  • 注意到此時用於判斷是否為最後一個item的方法,從count - 1變為了data.size - 1,用數據源的大小判斷,比count更加正確

使用時也需要有一些變化:

  • data設置給ItemDecoration
  • 通過color屬性設置顏色策略
val secondVerTimeline = SecondVerTimeline<Record>()
secondVerTimeline.data = records
secondVerTimeline.color = { item ->
when (item.status) {
1 -> color1
2 -> color2
...
}
}
rv_timeline2.addItemDecoration(secondVerTimeline)

然後就可以運行看一下效果了:

又來新需求了,急,Android怎麼實現時間線效果(上)?

哇哦,鵝妹子嚶,這樣就已經實現根據狀態轉變顏色的功能了!第二版的功能也圓滿實現!

後話

後來小莊又根據UI一頓修修改改,很快就完成了這個需求~但是小莊是一個有追求的程序員,他開始思考起了這個代碼的擴展性和通用性如何。不想不要緊,一想發現根本沒有鴨!如果產品想要把圓點變成圖片怎麼辦?或者產品想要更隨風飛翔自由是方向呢?

於是他想找個時間完善改進一下這個ItemDecoration,最好能應對產品的所有需求!具體升級內容請期待下集~

然而沒有什麼設計能做到一勞永逸,軟件工作中唯一不變的就是變化,同時我們也不應該為了應對所謂的“未來可能發生的改動”而過度設計

完整示例代碼請移步這裡,感謝您的觀看和時間

相關文章

JDK的sql設計不合理導致的驅動類初始化死鎖問題

有了這款GitHubAction,碼雲的付費服務也能免費用!

centos7安裝ElasticSearch配置外網訪問

Chrome請求報錯net::ERR_CERT_AUTHORITY_INVALID