做者:咕咚移動技術團隊-Bluephp
在 Android 開發中,使用 shape 標籤能夠很方便的幫咱們構建資源文件,跟傳統的 png 圖片相比:android
關於 shape 標籤如何使用,在網上一搜一大把,筆者就不在這裏贅述了,今天咱們要討論的是 shape 標籤氾濫成災之後帶來的後果。這裏先給你們看一個維護超過了 5 年的項目的 drawable 目錄 git
請注意右側標紅的滾動條,有沒有感受很酸爽,在這個目錄下的文件如今已經超過了 500 個,而且還在不停的增長。咱們分析這個目錄下的 xml 構成,發現主要由兩種類型構成:selector 和 shape。selector 這裏略過不提,重點關注 shape,發現 shape 文件已經超過了 200 個而且還在不停的增長。咱們再帶着好奇的心態隨便點開幾個 shape 看一看<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#66000000" />
<corners android:radius="15dp" />
</shape>
複製代碼
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient android:startColor="#0f000000" android:endColor="#00000000" android:angle="270" />
</shape>
複製代碼
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<solid android:color="#fbfbfd" />
<stroke android:width="1px" android:color="#dad9de" />
<corners android:radius="10dp" />
</shape>
複製代碼
真的是不看不知道,一看嚇一跳。原來咱們項目中大量存在的 shape 文件其實都是大同小異的,涉及到最多見的 shape 變化:圓角,描邊,填充以及漸變。 進一步分析,咱們又發現:github
等等一些狀況,讓咱們陷入了 shape 文件的無限新增與維護中。咱們不由要思考,有沒有辦法能夠把這些 shape 統一塊兒來管理呢?xml 書寫出來的代碼最終不都是會對應一個內存中的對象嗎?咱們能不能從管理 shape 文件過分到管理一個對象呢?canvas
Talk is cheap. Show me the codeapp
第一步,咱們須要肯定 shape 標籤對應的類究竟是哪個?第一反應就是 ShapeDrawable,顧名思義嘛。而後殘酷的事實告訴咱們實際上是 GradientDrawable 這兄弟。瀏覽 GradientDrawable 類的方法結構,從中咱們也找到了setColor()、setCornerRadius()、setStroke() 等目標方法。好吧,無論怎樣,先找到正主了。ide
第二步,繼續思考如何來設計這個通用控件,主要從如下幾個方面進行了考慮:ui
第三步,思路已經梳理清楚了,那就開擼。spa
class CommonShapeButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatButton(context, attrs, defStyleAttr) {
複製代碼
這裏實現了繼承 AppCompatButton 進行擴展,默認樣式 defStyleAttr 傳遞的是 0,那麼 CommonShapeButton 的默認表現形式就是文本樣式。設計
若是想要採用按鈕樣式,則須要先自定義一個按鈕樣式,緣由是系統按鈕的樣式自帶了 minWidth、minHeight 以及 padding,在具體業務中會影響到咱們的按鈕顯示,因此在自定義按鈕樣式中重置了這三個屬性:
<!-- 自定義按鈕樣式 -->
<style name="CommonShapeButtonStyle" parent="@style/Widget.AppCompat.Button"> <item name="android:minWidth">0dp</item> <item name="android:minHeight">0dp</item> <item name="android:padding">0dp</item> </style>
複製代碼
有了自定義按鈕樣式,那麼想要 CommonShapeButton 採用按鈕樣式,則採用以下形式:
<com.blue.view.CommonShapeButton style="@style/CommonShapeButtonStyle" android:layout_width="300dp" android:layout_height="50dp"/>
複製代碼
到這裏就能夠實現簡單的文本樣式和按鈕樣式的切換了。 接下來咱們就要進行關鍵的 shape 渲染了:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
// 初始化normal狀態
with(normalGradientDrawable) {
// 漸變色
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
}
cornerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, mCornerRadius.toFloat(), resources.displayMetrics)
// 默認的透明邊框不繪製,不然會致使沒有陰影
if (mStrokeColor != Color.parseColor("#00000000")) {
setStroke(mStrokeWidth, mStrokeColor)
}
}
// 是否開啓點擊動效
background = if (mActiveEnable) {
// 5.0以上水波紋效果
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
RippleDrawable(ColorStateList.valueOf(mPressedColor), normalGradientDrawable, null)
}
// 5.0如下變色效果
else {
// 初始化pressed狀態
with(pressedGradientDrawable) {
setColor(mPressedColor)
when (mShapeMode) {
0 -> shape = GradientDrawable.RECTANGLE
1 -> shape = GradientDrawable.OVAL
2 -> shape = GradientDrawable.LINE
3 -> shape = GradientDrawable.RING
}
cornerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, mCornerRadius.toFloat(), resources.displayMetrics)
setStroke(mStrokeWidth, mStrokeColor)
}
// 注意此處的add順序,normal必須在最後一個,不然其餘狀態無效
// 設置pressed狀態
stateListDrawable.apply {
addState(intArrayOf(android.R.attr.state_pressed), pressedGradientDrawable)
// 設置normal狀態
addState(intArrayOf(), normalGradientDrawable)
}
}
} else {
normalGradientDrawable
}
}
複製代碼
這裏的代碼有點長,彆着急,咱們來慢慢分析一下:
到這裏就能夠實現了用自定義屬性控制shape渲染顯示 CommonShapeButton 的背景了,這裏貼上所有的屬性:
<declare-styleable name="CommonShapeButton">
<attr name="csb_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="csb_fillColor" format="color" />
<attr name="csb_pressedColor" format="color" />
<attr name="csb_strokeColor" format="color" />
<attr name="csb_strokeWidth" format="dimension" />
<attr name="csb_cornerRadius" format="dimension" />
<attr name="csb_activeEnable" format="boolean" />
<attr name="csb_drawablePosition" format="enum">
<enum name="left" value="0" />
<enum name="top" value="1" />
<enum name="right" value="2" />
<enum name="bottom" value="3" />
</attr>
<attr name="csb_startColor" format="color" />
<attr name="csb_endColor" format="color" />
<attr name="csb_orientation" format="enum">
<enum name="TOP_BOTTOM" value="0" />
<enum name="LEFT_RIGHT" value="1" />
</attr>
</declare-styleable>
複製代碼
接下來咱們還須要進行最後的工做,解決在一個 button 中添加 drawable 不居中顯示的問題
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
// 若是xml中配置了drawable則設置padding讓文字移動到邊緣與drawable靠在一塊兒
// button中配置的drawable默認貼着邊緣
if (mDrawablePosition > -1) {
compoundDrawables?.let {
val drawable: Drawable? = compoundDrawables[mDrawablePosition]
drawable?.let {
// 圖片間距
val drawablePadding = compoundDrawablePadding
when (mDrawablePosition) {
// 左右drawable
0, 2 -> {
// 圖片寬度
val drawableWidth = it.intrinsicWidth
// 獲取文字寬度
val textWidth = paint.measureText(text.toString())
// 內容總寬度
contentWidth = textWidth + drawableWidth + drawablePadding
val rightPadding = (width - contentWidth).toInt()
// 圖片和文字所有靠在左側
setPadding(0, 0, rightPadding, 0)
}
// 上下drawable
1, 3 -> {
// 圖片高度
val drawableHeight = it.intrinsicHeight
// 獲取文字高度
val fm = paint.fontMetrics
// 單行高度
val singeLineHeight = Math.ceil(fm.descent.toDouble() - fm.ascent.toDouble()).toFloat()
// 總的行間距
val totalLineSpaceHeight = (lineCount - 1) * lineSpacingExtra
val textHeight = singeLineHeight * lineCount + totalLineSpaceHeight
// 內容總高度
contentHeight = textHeight + drawableHeight + drawablePadding
// 圖片和文字所有靠在上側
val bottomPadding = (height - contentHeight).toInt()
setPadding(0, 0, 0, bottomPadding)
}
}
}
}
}
// 內容居中
gravity = Gravity.CENTER
// 可點擊
isClickable = true
}
複製代碼
咱們繼續來分析這裏的代碼:
到這裏就作好了讓 drawable 居中顯示的準備工做,咱們繼續往下走:
override fun onDraw(canvas: Canvas) {
// 讓圖片和文字居中
when {
contentWidth > 0 && (mDrawablePosition == 0 || mDrawablePosition == 2) -> canvas.translate((width - contentWidth) / 2, 0f)
contentHeight > 0 && (mDrawablePosition == 1 || mDrawablePosition == 3) -> canvas.translate(0f, (height - contentHeight) / 2)
}
super.onDraw(canvas)
}
複製代碼
接下來咱們就是在 onDraw 方法中,利用在 onLayout 方法中計算的數值,平移 button 的內容,從而實現讓 drawable 和文字一塊兒居中顯示。
到這裏咱們就完成了 CommonShapeButton 的所有設計和實現,如下是效果圖:
最後再附上: github地址傳送門 喜歡就 star 一下唄。