在之前的博客中,我們曾經(jīng)討論設計過一個通用組件:CommonShapeButton 。主要用來移除項目中大量的 shape 文件,提高我們項目的可維護性韧涨。有興趣的朋友可以點擊下方鏈接進行閱讀:
Android - Kotlin 是時候跟 shape 標簽說拜拜了
這篇博客發(fā)布以后,得到了大家的廣泛關注侮繁,可能大家也切身感受到了 CommonShapeButton 給我們帶來的便利虑粥。而今天在這里,筆者想要討論的是這個通用組件不能解決的應用場景宪哩,以及給出新的解決方案舀奶。
我們先來看看 CommonShapeButton 不能解決的應用場景是什么?這里我們需要回顧下這個通用組件斋射,它本身是用來解決 shape 文件泛濫的問題育勺,支持 shape 的各種特性,同時也支持文本樣式和按鈕樣式罗岖。但是歸根結底 CommonShapeButton 只是一個 View 涧至,它沒有辦法解決 ViewGroup 的應用場景。而在實際開發(fā)過程中桑包,在 ViewGroup 這一層去設置 shape 樣式的背景是一個常見的需求南蓬。分析到這里,我們得出結論哑了,我們還需要一個通用組件 CommonShapeViewGroup 來協(xié)助我們項目開發(fā)赘方。
正當筆者準備著手設計這個新的通用組件的時候,腦中突然閃過一個官方提供的組件 CardView 弱左,這個位于 support-v7 下面的谷歌親兒子窄陡,好像已經(jīng)解決了我們的問題?于是筆者又去啃了一下官方文檔拆火,對這個 CardView 做了一個全面的梳理跳夭,發(fā)現(xiàn)了它的局限性:
- CardView 繼承自 FrameLayout 涂圆,而現(xiàn)在主流的 ViewGroup 應該是 ConstraintLayout 和 RelativeLayout。
- CardView 支持設置背景顏色币叹,但是只能設置純色润歉,無法設置漸變顏色。
- CardView 支持設置圓角大小颈抚,但是只能同時設置四個角的圓角大小踩衩,無法單一設置左側圓角或者右側圓角。
- CardView 只支持矩形一種形狀贩汉。
- CardView 不支持設置描邊顏色和描邊寬度驱富。
沒辦法,看來谷歌親兒子也不頂用雾鬼,還是自己擼吧萌朱。
Talk is cheap. Show me the code
第一步宴树,我們需要確定支持的 ViewGroup 有哪些策菜。還是那句話,現(xiàn)在主流的 ViewGroup 應該是 ConstraintLayout 和 RelativeLayout 酒贬,這里需要重點推一波 ConstraintLayout 又憨,自從用了它以后,腰也不酸了锭吨,腿也不疼了蠢莺,媽媽再也不用擔心我寫布局了。但是考慮到我們程序猿都是重感情的人零如,之前最愛的 RelativeLayout 也不能說有了新歡就不管了是吧躏将,好吧,把 RelativeLayout 加上考蕾,就支持這兩兄弟了祸憋。
第二步,繼續(xù)思考如何來設計這個通用組件肖卧,主要是從以下幾個方面進行了考慮:
- ViewGroup 的設計要比 View 更簡單蚯窥,因為它是純展示的,沒有交互也不需要動效塞帐。
- 直接繼承 ConstraintLayout 和 RelativeLayout 拦赠,進行背景的動態(tài)設置是最為簡單有效的方式。
- 自定義屬性方面葵姥,完全可以參照 CommonShapeButton 荷鼠,去掉一些不需要的屬性即可。
- 新增一個陰影屬性榔幸,提升一下逼格颊咬∥裆控件陰影這個問題,在 5.0 以上也就一行代碼的事喳篇。在 5.0 以下敞临,筆者花了不少時間,用各種方案做出來的效果都不盡人意麸澜。本著寧缺毋濫的原則挺尿,最終還是選擇了放棄。其實主要原因還是 5.0 以下的用戶確實越來越少炊邦,花費過多的精力去做一些收效甚微的工作也不符合軟件工程的思想编矾。當然這方面有興趣的朋友,可以在文章的后面拿到源碼以后進行自己的擴展和修改馁害。
第三步窄俏,思路已經(jīng)梳理清楚了,那就開擼吧碘菜。這里就以 ConstraintLayout 為例凹蜈,
class ShapeConstraintLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
這里選擇了直接繼承 ConstraintLayout 進行擴展。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
// 初始化shape
with(mGradientDrawable) {
// 漸變色
if (mStartColor != Color.parseColor("#FFFFFF") && mEndColor != Color.parseColor("#FFFFFF")) {
colors = intArrayOf(mStartColor, mEndColor)
when (mOrientation) {
0 -> orientation = GradientDrawable.Orientation.TOP_BOTTOM
1 -> orientation = GradientDrawable.Orientation.LEFT_RIGHT
}
}
// 填充色
else {
setColor(mFillColor)
}
when (mShapeMode) {
0 -> shape = GradientDrawable.RECTANGLE
1 -> shape = GradientDrawable.OVAL
2 -> shape = GradientDrawable.LINE
3 -> shape = GradientDrawable.RING
}
// 統(tǒng)一設置圓角半徑
if (mCornerPosition == -1) {
cornerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, mCornerRadius.toFloat(), resources.displayMetrics)
}
// 根據(jù)圓角位置設置圓角半徑
else {
cornerRadii = getCornerRadiusByPosition()
}
// 默認的透明邊框不繪制
if (mStrokeColor != Color.parseColor("#00000000")) {
setStroke(mStrokeWidth, mStrokeColor)
}
}
// 設置背景
background = mGradientDrawable
// 5.0以上設置陰影
if (mWithElevation && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
elevation = DEFAULT_ELEVATION
}
}
核心代碼依然選擇在 onMeasure 方法中實現(xiàn)忍啸,我們做一個簡單的分析:
- 首先對 mGradientDrawable 設置當前是漸變色渲染還是填充色渲染仰坦,漸變色渲染還需要單獨控制渲染的方向。
- 然后對 mGradientDrawable 設置 shape 模式计雌、圓角以及描邊悄晃。這里的圓角設置區(qū)分了統(tǒng)一設置四個角還是根據(jù)圓角位置設置。
- 然后設置 ViewGroup 的背景凿滤。
- 最后在 5.0 以上設置控件陰影妈橄。
到這里,就完成了核心實現(xiàn)翁脆。下面我們看一下根據(jù)圓角位置設置圓角半徑的具體實現(xiàn):
/**
* 根據(jù)圓角位置獲取圓角半徑
*/
private fun getCornerRadiusByPosition(): FloatArray {
val result = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f)
val cornerRadius = mCornerRadius.toFloat()
if (containsFlag(mCornerPosition, TOP_LEFT)) {
result[0] = cornerRadius
result[1] = cornerRadius
}
if (containsFlag(mCornerPosition, TOP_RIGHT)) {
result[2] = cornerRadius
result[3] = cornerRadius
}
if (containsFlag(mCornerPosition, BOTTOM_RIGHT)) {
result[4] = cornerRadius
result[5] = cornerRadius
}
if (containsFlag(mCornerPosition, BOTTOM_LEFT)) {
result[6] = cornerRadius
result[7] = cornerRadius
}
return result
}
/**
* 是否包含對應flag
*/
private fun containsFlag(flagSet: Int, flag: Int): Boolean {
return flagSet or flag == flagSet
}
簡單分析一下:
- 自定義圓角位置支持四個方位的:TOP_LEFT眷蚓、TOP_RIGHT、BOTTOM_RIGHT鹃祖、BOTTOM_LEFT溪椎。
- 通過自定義屬性中的 flag 標簽設置了圓角方位支持按位或運算。
- 生成四個角對應的8位數(shù)組恬口,解析 xml 屬性根據(jù)按位或運算設置對應方位的圓角半徑校读。
到這里,也就是 CommonShapeViewGroup 的全部實現(xiàn)了祖能。其實筆者寫到這里的時候歉秫,陷入了一個思考,我們到現(xiàn)在實現(xiàn)了 CommonShapeButton 和 CommonShapeViewGroup 养铸,其實這兩者的本質都是用代碼去實現(xiàn) shape 效果雁芙,也就是對 GradientDrawable 的二次封裝轧膘,那么我們是不是實現(xiàn)一個封裝以后的 CommonShapeDrawable 就可以解決所有問題呢?TextView 兔甘、Button 谎碍、ConstraintLayout 、RelativeLayout等等以及其他的應用場景都可以適配洞焙。筆者產(chǎn)生了這個想法以后蟆淀,就馬上去實現(xiàn)了一個。但是實際開發(fā)用起來以后澡匪,發(fā)現(xiàn)它并不像我們想象的那么方便熔任,需要創(chuàng)建一個 CommonShapeDrawable 對象,然后逐一調用對應的方法去設置 shape 效果唁情,最后還要在一個恰當?shù)臅r機設置成控件的背景疑苔。這跟我們通過 xml 自定義屬性就能實現(xiàn)效果來比,繁瑣了不少甸鸟,最終還是選擇了放棄惦费。有興趣的朋友也可以通過這兩篇博客的學習,自己去擼一個出來哀墓。
題外話說了這么多趁餐,這里還是回到 CommonShapeViewGroup 喷兼,照例貼上全部的自定義屬性:
<declare-styleable name="CommonShapeViewGroup">
<attr name="csvg_shapeMode" format="enum">
<enum name="rectangle" value="0" />
<enum name="oval" value="1" />
<enum name="line" value="2" />
<enum name="ring" value="3" />
</attr>
<attr name="csvg_fillColor" format="color" />
<attr name="csvg_strokeColor" format="color" />
<attr name="csvg_strokeWidth" format="dimension" />
<attr name="csvg_cornerRadius" format="dimension" />
<attr name="csvg_cornerPosition">
<flag name="topLeft" value="1" />
<flag name="topRight" value="2" />
<flag name="bottomRight" value="4" />
<flag name="bottomLeft" value="8" />
</attr>
<attr name="csvg_startColor" format="color" />
<attr name="csvg_endColor" format="color" />
<attr name="csvg_orientation" format="enum">
<enum name="TOP_BOTTOM" value="0" />
<enum name="LEFT_RIGHT" value="1" />
</attr>
<attr name="csvg_withElevation" format="boolean" />
</declare-styleable>
以下是效果圖:
最后再附上:github地址傳送門 喜歡就 star 一下唄篮绰。