網站首頁 編程語言 正文
1、前言
上回Android如何實現時間線效果 說到,小莊吭哧吭哧的擼完了需求,雖然功能上可以應付過去了,但他總覺得什么地方還可以再優化一下,可以搞一個較為通用的組件,順便還能鍛煉一下自己的編碼設計能力,豈不美哉!一起看看他今天又要搞什么幺蛾子唄?
2、分析
2.1提出功能
這回小莊自己當上產品了,他上上下下的看了幾遍這個時間線,親自提出了幾點不滿可以優化的地方:
- 最好可以根據位置決定顯示什么東西,比如第一個/最后一個一般最重要,要跟別的不一樣!
- 我要把圓點改成圖片,根據狀態展示不同的圖片!
- 不,我要圓點和圖片交錯著來,就像某寶物流信息那樣!
產品小莊被程序員小莊關了小黑屋。程序員小莊覺得這幾點需求就暫時夠了,可以先著手嘗試一下了
2.2需求分析
- 從需求上來看,變的主要都是圓點(以下稱結點)的部分,畫線的部分應該不會有什么改動,可以沿用
- 結點部分不僅會變,還會變的多種多樣,千奇百怪,這里的靈活性要求較高
- 原來只有根據狀態決定結點樣式,現在還要根據位置,也要加入考慮
2.3方案設想
是什么讓我們可以實現代碼復用(線),又可以實現靈活定制(結點)呢?是繼承呀!(當然也可以考慮組合)綜合考慮,畫線的部分可以放在父類中,畫結點的部分則可以設計一個抽象方法,交給子類自由實現
3、編碼
重構的過程很快樂(不是),但是步子太大卻容易拉胯。小莊打算先改動代碼結構,并保持原來效果可以運行
3.1第三版
說干就干,小莊決定抽取一個drawNode
抽象方法,讓子類去畫圓點,而在父類中繼續保留畫線的操作。
- 子類圓點的顏色其實可以復用父類中原本的color屬性,但是以防萬一,我們還是重新定義一個dotColor屬性
- 但是由于父類現在不畫結點了,它怎么知道,現在結點寬度是多少呢?整個軸線的x坐標在哪里呢?不知道這些,沒法畫線呀
- 為解決這個問題,小莊決定再定義一個nodeWidth屬性,用來設置結點的寬度還有一個問題,不知道結點的高度,就不知道上線應該在什么y坐標停,下線應該從什么y坐標開始
嗯?你說一條線從頂部畫到底部?這也太粗暴了,如果圓點是空心的就暴露了呀,為解決這個問題,小莊只能再定義一個nodeHeight
屬性,用來設置結點的高度。現在這個類里面的屬性越來越多了,小莊感覺這離自己追求的優雅相去甚遠
abstract class ThirdVerTimeline<T> : RecyclerView.ItemDecoration() { // 不重要的屬性... var color: (item: T) -> Int = { _ -> Color.GRAY } var nodeWidth = 30 var nodeHeight = 30 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 xPosition = (nodeWidth / 2 + paddingLeft).toFloat() //-->這里有修改,用nodeWidth計算x坐標 // 畫上線和下線...//-->這里有修改,用nodeHeight參與計算 } // 子類畫結點! drawNode(c, parent, state) } // 由子類實現 protected abstract fun drawNode(c: Canvas, parent: RecyclerView, state: RecyclerView.State) override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { super.getItemOffsets(outRect, view, parent, state) outRect.left = paddingLeft + paddingRight + nodeWidth //-->這里有修改,用nodeWidth計算item的偏移量 } } class DotTimeline<T> :ThirdVerTimeline<T>(){ // 這里是子類自己的paint,不復用父類 private val paint = Paint(Paint.ANTI_ALIAS_FLAG) val radius = 10f var dotColor: (item: T) -> Int = { _ -> Color.GRAY } override fun drawNode(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { val count = parent.childCount for (i in 0 until count) { // 【不重要的變量】... // 畫圓點! paint.color = dotColor(item) c.drawCircle(xPosition, itemView.top + offset + radius, radius, paint) } } }
運行起來完全沒問題!不過觀眾姥爺們肯定發現了一個致命問題,那就是:為什么在父類和子類里都要各自做循環呢??這是妥妥的代碼重復呀!哎,不著急,為了解決這個問題,我們需要重新設計一下drawNode
方法。
你可能會想,把drawNode
直接放進循環里不就行了嗎?的確,但是在那之前,我們先來看一下【不重要的變量】里都有哪些變量:
// 獲取當前的itemView val itemView = parent.getChildAt(i) // 當前項的x坐標,整個軸線的x坐標都是相同的 val xPosition = (nodeWidth / 2 + paddingLeft).toFloat() // 當前項的真正位置 val adapterPosition = parent.getChildAdapterPosition(itemView) // 當前項的數據源 val item = data[adapterPosition]
子類中是不是真的都需要父類傳給它這些變量呢?讓我們來研究一下:
-
itemView
:子類中畫結點需要確定自身的位置等信息 --> 需要 -
xPosition
:雖然子類可以自己計算,但是由父類傳給子類豈不是更能保證它們都在同一條直線上?--> 需要 -
adapterPosition
:需求中說到要根據位置繪制結點 --> 需要 -
item
:子類自然要根據數據的狀態來判斷畫什么 --> 需要
其實說來說去,這些變量子類中都可以計算得到,之所以要由父類傳給子類,大致有以下幾個理由:
- 減少重復代碼量,遵循DRY原則(Don't repeat yourself)
- 防止父類和子類的數據獲取有出入,子類不按照父類的規則來計算
- 某一天又要改規則了,只需改動父類就可以了,無需改動多處
至于一個方法這么多個參數真的好嗎?這~我暫時也無能為力啦。修改后的代碼如下:
abstract class ThirdVerTimeline<T> : RecyclerView.ItemDecoration() { // 不重要的屬性... 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) { // 【重要的變量】... // 畫上線和下線... // 子類畫結點! drawNode(c, parent, state, xPosition, item, itemView, adapterPosition) } } // 由子類實現 protected abstract fun drawNode(c: Canvas, parent: RecyclerView, state: RecyclerView.State, xPosition: Float, item: T, itemView: View, adapterPosition: Int) // getItemOffsets... } class DotTimeline<T> :ThirdVerTimeline<T>(){ private val paint = Paint(Paint.ANTI_ALIAS_FLAG) private val radius = 10f override fun drawNode(c: Canvas, parent: RecyclerView, state: RecyclerView.State, xPosition: Float, item: T, itemView: View, adapterPosition: Int) { // 簡簡單單畫圓即可 paint.color = color(item) c.drawCircle(xPosition, itemView.top + offset + radius, radius, paint) } }
搞了好大一圈,才只是實現了原本的功能而已,由于效果都是一模一樣的,我就不貼圖了。但是接下來我們就可以隨心所欲對父類進行擴展啦
不知道有沒有小伙伴有疑問,這不是也類似于Java中的模板模式嗎?你怎么不用上一章說的kotlin的函數類型啦?
的確用函數類型可以幫我們減少這些子類的數量,使用時只要新建一個ItemDecoration,并且設置drawNode屬性怎么畫就可以了,想想就美滋滋。然而我不這么做至少有兩個理由:
- 實現一個DotTimeline,可以復用,不至于每個要用到的地方都寫一遍drawNode的內容(當然也可能目前只有一處用到,但是未來是很長的~)
- 使DotTimeline的使用者關心的事情更少,職責更單一(其實我也是寫一半才想起的,也許并沒有人有這個疑問吧哈哈哈)
3.2第四版
第四版基于第三版的父類,實現個圖片的需求~
- 類似于之前提到的
color
屬性,這里我們也定義一個drawableRes
屬性,用它來設置圖片選擇策略 - 同時我們還可以用
nodeWidth
和nodeHeight
用來控制圖片的大小,畢竟我們并不是總能找到尺寸合適的圖片
class PicTimeline<T>(private val context: Context) : ThirdVerTimeline<T>() { lateinit var drawableRes: (item: T) -> Int override fun drawNode(c: Canvas, parent: RecyclerView, state: RecyclerView.State, xPosition: Float, item: T, itemView: View, adapterPosition: Int) { // 從圖片選擇策略中獲得bitmap val bitmap = BitmapFactory.decodeResource(context.resources, drawableRes(item)) val src = Rect(0, 0, bitmap.width, bitmap.height) val left = xPosition - nodeWidth / 2 val top = (itemView.top + offset).toFloat() val dst = RectF(left, top, left + nodeWidth, top + nodeHeight) // 畫圖片 c.drawBitmap(bitmap, src, dst, Paint() ) } }
在使用的時候,要設置一下drawableRes
屬性(記得也要設置color
屬性,這個屬性現在代表線的顏色)
picTimeline.drawableRes = { item -> when (item.status) { 1 -> R.drawable.ic_checked else -> R.drawable.ic_uncheck } } rv_timeline4.addItemDecoration(picTimeline)
然后就可以運行一下看看效果~
是真的!可以誒!小莊在成功的大路上豬突猛進!
至于圖片和圓點交錯的效果,現在看起來也就比較簡單了,只要把第三版的圓點和第四版的圖片中的代碼混搭在一起就可以了。這邊就不貼代碼了,萬一有感興趣的朋友可以到代碼倉庫中查看。直接來看看效果如何吧
還可以還可以
3.3最終版
其實小莊還有很多功能想實現的,但是好像沒必要再寫下去了,無非就是一些屬性的修修改改罷遼。而且實際項目中可能也用不到這么多功能,沒有實際需求指導,越擴展反而越復雜、越難用。
最后放一張全家福,有需要的朋友可以參考一下我的思路,或者選取合適的部分代碼食用,每一個版本的示例代碼都在這里!
如圖,最后三個是最終的版本。在最終版中,我又實現了:
- 時間線的左右位置
- 圓點的類型:實心和邊框
- 結點的不同大小
- 線的寬度
- item的間距(贈送的)
并且在顏色color/
圖片drawableRes
選擇策略中加入adapterPosition
參數,可以根據位置進行設置策略啦
原文鏈接:https://juejin.cn/post/6844904112849420301
相關推薦
- 2023-03-16 python中split()函數的用法詳解_python
- 2022-06-24 React如何使用refresh_token實現無感刷新頁面_React
- 2022-10-20 Android?Flutter實現自定義下拉刷新組件_Android
- 2023-01-30 C++指針和數組:字符和字符串、字符數組的關聯和區別_C 語言
- 2022-07-17 Python中使用tkFileDialog實現文件選擇、保存和路徑選擇_python
- 2023-01-28 詳解如何利用C#實現漢字轉拼音功能_C#教程
- 2022-05-12 Android 11 適配存儲權限
- 2023-02-01 C語言結構體嵌套與對齊超詳細講解_C 語言
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支