一,背景
随着项目的逐步增大,业务逐步的迭代累加,我们的代码当中除了日益增加的业务代码外,一些资源文件例如图片,layout,drawable文件也是越来越多,它们虽然占不了apk多大的体积,但是对于后期维护,尤其是做过资源整理,无用资源删除,或者需要兼容多种主题色样式需求的同学来讲,那简直是一件费体力,脑力,精力的事情,没错,我们可以借助一些工具比如Android Studio里面的lint工具去检索这些文件,但虽然可以把无用资源检索出来,但以防万一,还是得逐个看看每个文件是否真的是无用的
大部分图片我们可以用矢量图去代替,布局文件我们也可以把公共布局抽取出来用include,merge标签来引用,比较头疼的是drawable文件,时常会因为一些细小的元素,诸如背景色,渐变色,圆角大小,边框颜色,点击后的反馈颜色的不同,会生成不同的文件,多人开发中如果规范没有定好,这些文件的命名也是五花八门,造成后面视觉如果要更换ui图,稍微更改一下一些元素,我们又得很无奈的重新在新建一个drawable文件,导致后期drawable文件下的文件越来越多难以维护,就像这个样子
实际情况可能还要多,那有没有办法解决drawable文件频繁创建的问题呢,没错,就是自定义一个布局控件,将这些元素都集中在这个控件的属性中去,我们要用的时候,也就是把这些色值,圆角大小等赋给这些属性就好,后期就算视觉要改设计图,对于我们来讲,动几个属性的事情,一个字,倍儿爽~
二,调研
其实这类控件市面上已经有不少开发者开源了自己的代码,比如ShadowLayout,大家可以在自己的项目配置文件中去远程依赖使用,但我个人还是比较喜欢手写一个,一个原因是不太喜欢去依赖这种控件类第三方插件,还有一个就是项目各异,还是根据实际情况去写控件,方便因需求设计变更可以随时做更改,先罗列下这个控件需要支持那些属性
| 属性名称 | 作用 | 需要支持的格式类型 |
|---|---|---|
| joy_bg_color | 背景颜色 | reference |
| joy_start_color | 渐变色起始颜色 | reference |
| joy_end_color | 渐变色结束颜色 | reference |
| joy_direction | 渐变方向 | string |
| joy_corner_radius | 圆角大小 | reference dimension |
| joy_lt_corner_radius | 左上圆角大小 | reference dimension |
| joy_lb_corner_radius | 左下圆角大小 | reference dimension |
| joy_rt_corner_radius | 右上圆角大小 | reference dimension |
| joy_rb_corner_radius | 右下圆角大小 | reference dimension |
| joy_stroke_width | 线框大小 | reference dimension |
| joy_stroke_color | 线框颜色 | reference |
| joy_shape_style | 形状样式 | string |
| joy_pressed_bg_color | 点击下去的背景颜色 | reference |
| joy_pressed_start_color | 点击下去的渐变色起始颜色 | reference |
| joy_pressed_end_color | 点击下去的渐变色结束颜色 | reference |
| joy_pressed_direction | 点击下去的渐变方向 | string |
| joy_ripple | 点击是否需要水波纹效果 | boolean |
| joy_ripple_color | 水波纹色值 | boolean |
其中像背景色,渐变,圆角等,我们需要用到GradientDrawable,而点击反馈的状态,我们需要用到StateListDrawable,而水波纹效果,就需要RippleDrawable,它们都是Drawable的子类,Drawable还有其他别的子类,这里没用到就不介绍了
三,自定义控件JoySelectorLayout编写
3.1 先声明需要用到的Drawable对象
/**
* 渲染背景,普通正常状态下的样式
*/
private lateinit var normalDrawables: GradientDrawable
/**
* 渲染背景,点击状态下的样式
*/
private lateinit var pressedDrawables: GradientDrawable
/**
* 状态背景,设置普通,点击状态的样式
*/
private lateinit var stateDrawable: StateListDrawable
/**
* 水波纹背景
*/
private lateinit var rippleDrawable: RippleDrawable
3.2 创建背景颜色
背景颜色分纯色与渐变色,优先判断有没有设置纯色(joy_bg_color),如果有直接纯色背景,如果没有设置则看有没有设置渐变起始色(joy_start_color)与渐变终止色(joy_end_color),渐变起始色与渐变终止色默认颜色都为透明
/**
* 创建背景颜色
*/
private fun createColors(typedArray: TypedArray): IntArray {
var bgColor = typedArray.getColor(
R.styleable.JoySelectorLayout_joy_bg_color, -1
)
val colors = if (bgColor != -1) {
intArrayOf(bgColor, bgColor)
} else {
intArrayOf(
typedArray.getColor(
R.styleable.JoySelectorLayout_joy_start_color,
0
),
typedArray.getColor(
R.styleable.JoySelectorLayout_joy_end_color,
0
)
)
}
return colors
}
3.3 确定渐变方向
GradientDrawable有三种渐变方向,线性渐变(LINEAR_GRADIENT),放射性渐变(RADIAL_GRADIENT),扇形渐变(SWEEP_GRADIENT),默认线性渐变,这边也主要以线性渐变为例。创建一个GradientDrawable需要两个参数,一个是颜色,这个我们已经定义好了,另一个是渐变方向,我们通过属性joy_direction来决定用什么方向,GradientDrawable总共有八种方向,分别是从左往右(LEFT_RIGHT),从上往下(TOP_BOTTOM),从右往左(RIGHT_LEFT),从下往上(BOTTOM_TOP),从左上到右下(TL_BR),从左下到右上(BL_TR),从右上到左下(TR_BL),从右下到左上(BR_TL),所以joy_direction声明了八个value一一对应
<attr name="joy_direction" format="string">
<enum name="start_to_end" value="1"/>
<enum name="top_to_bottom" value="2"/>
<enum name="end_to_start" value="3"/>
<enum name="bottom_to_top" value="4"/>
<enum name="starttop_to_endbottom" value="5"/>
<enum name="endtop_to_startbottom" value="6"/>
<enum name="startbottom_to_endtop" value="7"/>
<enum name="endbottom_to_starttop" value="8"/>
</attr>
代码当中默认使用从左往右方向,其余根据输入自行匹配
/**
* 决定用哪个方向的GradientDrawable
*/
private fun createGradientDrawable(
typedArray: TypedArray
): GradientDrawable {
val direction = typedArray.getString(
R.styleable.JoySelectorLayout_joy_direction
)
val colors = createColors(typedArray)
return GradientDrawable(
when (direction) {
"2" -> GradientDrawable.Orientation.TOP_BOTTOM
"3" -> GradientDrawable.Orientation.RIGHT_LEFT
"4" -> GradientDrawable.Orientation.BOTTOM_TOP
"5" -> GradientDrawable.Orientation.TL_BR
"6" -> GradientDrawable.Orientation.TR_BL
"7" -> GradientDrawable.Orientation.BL_TR
"8" -> GradientDrawable.Orientation.BR_TL
else -> GradientDrawable.Orientation.LEFT_RIGHT
}, colors
)
}
3.4 创建圆角
圆角也是先去判断joy_corner_radius有没有值,有的话则表示所有圆角都是这个大小,否则去判断joy_lt_corner_radius,joy_lb_corner_radius,joy_rt_corner_radius,joy_rb_corner_radius有没有值,有则表示某一个角的圆角已被设置
/**
* 创建圆角
*/
private fun createRadius(typedArray: TypedArray): FloatArray {
val radius =
typedArray.getDimension(R.styleable.JoySelectorLayout_joy_corner_radius, 0f)
return if (radius != 0f)
floatArrayOf(radius, radius, radius, radius, radius, radius, radius, radius)
else
createCornerArray(typedArray)
}
/**
* 创建圆弧半径数组
*/
private fun createCornerArray(typedArray: TypedArray): FloatArray {
return floatArrayOf(
typedArray.getDimension(
R.styleable.JoySelectorLayout_joy_lt_corner_radius,
0f
),
typedArray.getDimension(
R.styleable.JoySelectorLayout_joy_lt_corner_radius,
0f
),
typedArray.getDimension(
R.styleable.JoySelectorLayout_joy_rt_corner_radius,
0f
),
typedArray.getDimension(
R.styleable.JoySelectorLayout_joy_rt_corner_radius,
0f
),
typedArray.getDimension(
R.styleable.JoySelectorLayout_joy_rb_corner_radius,
0f
),
typedArray.getDimension(
R.styleable.JoySelectorLayout_joy_rb_corner_radius,
0f
),
typedArray.getDimension(
R.styleable.JoySelectorLayout_joy_lb_corner_radius,
0f
),
typedArray.getDimension(
R.styleable.JoySelectorLayout_joy_lb_corner_radius,
0f
)
)
}
3.5 创建边框颜色与大小
这个很容易,直接读取属性值,代码如下
/**
* 生成边框宽度
*/
private fun createStrokeWith(typedArray: TypedArray): Int {
mStrokeWidth =
typedArray.getDimension(
R.styleable.JoySelectorLayout_joy_stroke_width, 0f
).toInt()
return mStrokeWidth
}
/**
* 生成边框颜色
*/
private fun createStrokeColor(typedArray: TypedArray): Int {
mStrokeColor = typedArray.getColor(
R.styleable.JoySelectorLayout_joy_stroke_color, 0
)
return mStrokeColor
}
3.6 创建形状样式
这里只有圆形跟矩形两种形状,原本连圆形也不想定义,毕竟矩形的宽度的一半当作圆角它不就是个圆吗,后来一想也不合适,如果这个矩形宽度随内容变化而变化,那我岂不是圆角也得跟着变吗,所以不如直接设置个圆形属性,按需选择形状,同样我们读取joy_shape_style属性,1代表矩形,2代码圆形,默认为矩形
/**
* 创建样式
*/
private fun createShapetyle(typedArray: TypedArray): Int {
shapeStyle = typedArray.getString(R.styleable.JoySelectorLayout_joy_shape_style)
return if (TextUtils.equals("2", shapeStyle))
GradientDrawable.RING
else
GradientDrawable.RECTANGLE
}
3.7 生成默认状态下的GradientDrawable背景
normalDrawables = createGradientDrawable(typedArray)
normalDrawables.apply {
gradientType = GradientDrawable.LINEAR_GRADIENT
cornerRadii = createRadius(typedArray)
setStroke(createStrokeWith(typedArray), createStrokeColor(typedArray))
shape = createShapetyle(typedArray)
}
点击状态的pressedDrawables与默认状态的设置完全一样,只是取的属性名称不同,这里不展开说明
3.8 创建水波纹效果
RippleDrawable的构造方法需要传入三个参数,第一个为ColorStateList,第二个content是作为基底的drawable,可以用之前的normalDrawable,也可以自定义一个,第三个mask,可控制水波纹范围的drawable
rippleDrawable = RippleDrawable(
ColorStateList(
arrayOf(
intArrayOf(android.R.attr.state_pressed),
intArrayOf(android.R.attr.state_focused),
intArrayOf(android.R.attr.state_activated)
),
intArrayOf(
typedArray.getColor(R.styleable.JoySelectorLayout_joy_ripple_color, 0),
typedArray.getColor(R.styleable.JoySelectorLayout_joy_ripple_color, 0),
typedArray.getColor(R.styleable.JoySelectorLayout_joy_ripple_color, 0)
)
),
normalDrawables, ShapeDrawable()
)
background = rippleDrawable
xml配置示例
<com.coffee.android.JoySelectorLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="10dp"
android:clickable="true"
android:focusable="true"
app:joy_bg_color="@color/transparent"
app:joy_corner_radius="10dp"
app:joy_ripple="true"
app:joy_ripple_color="@color/color_BBBBBB"
app:joy_stroke_color="@color/teal_200"
app:joy_stroke_width="1dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设置水波纹效果"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.coffee.android.JoySelectorLayout>
3.9 添加点击效果
用StateListDrawable设置点击效果同xml基本很相似,也是每个state都要去设置个drawable属性,最后还要加个默认的drawable,无论哪种都要注意顺序,默认的drawable都要加在最后,这边就用我们之前定义好的pressedDrawable作为点击后的效果背景,normalDrawable作为默认的背景,代码如下
stateDrawable.addState(
intArrayOf(
android.R.attr.state_focused,
android.R.attr.state_pressed,
android.R.attr.state_enabled
), pressedDrawables
)
stateDrawable.addState(
intArrayOf(android.R.attr.state_pressed),
pressedDrawables
)
stateDrawable.addState(
intArrayOf(android.R.attr.state_focused),
pressedDrawables
)
stateDrawable.addState(
intArrayOf(android.R.attr.state_enabled),
normalDrawables
)
stateDrawable.addState(intArrayOf(), normalDrawables)
xml配置示例
<com.coffee.android.JoySelectorLayout
android:layout_width="match_parent"
android:layout_height="100dp"
android:clickable="true"
android:focusable="true"
android:layout_margin="10dp"
app:joy_bg_color="@color/transparent"
app:joy_corner_radius="10dp"
app:joy_pressed_end_color="@color/color_FCFCFC"
app:joy_pressed_start_color="@color/color_BBBBBB"
app:joy_pressed_stroke_color="@color/teal_200"
app:joy_pressed_stroke_width="2dp"
app:joy_stroke_color="@color/teal_200"
app:joy_stroke_width="1dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设置点击事件"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.coffee.android.JoySelectorLayout>
3.10 增加开关,切换水波纹跟点击效果
val needRipple = typedArray.getBoolean(R.styleable.JoySelectorLayout_joy_ripple, false)
background = if(needRipple) rippleDrawable else stateDrawable
3.11 完整代码
/**
* Author: tanglei
*/
class JoySelectorLayout : ConstraintLayout {
/**
* 渲染背景,普通正常状态下的样式
*/
private lateinit var normalDrawables: GradientDrawable
/**
* 渲染背景,点击状态下的样式
*/
private lateinit var pressedDrawables: GradientDrawable
/**
* 状态背景,设置普通,点击状态的样式
*/
private lateinit var stateDrawable: StateListDrawable
/**
* 水波纹背景
*/
private lateinit var rippleDrawable: RippleDrawable
/**
* 样式,圆角矩形还是圆形
*/
private var shapeStyle: String? = null
/**
* 边框大小
*/
private var mStrokeWidth = 0
/**
* 边框颜色
*/
private var mStrokeColor = 0
constructor(context: Context) : super(context, null, 0) {
initView(context, null, 0)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs, 0) {
initView(context, attrs, 0)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
initView(context, attrs, defStyleAttr)
}
private fun initView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
if (attrs != null) {
normalDrawables = GradientDrawable()
stateDrawable = StateListDrawable()
val typedArray = context.obtainStyledAttributes(
attrs, R.styleable.JoySelectorLayout, defStyleAttr, 0
)
normalDrawables = createGradientDrawable(typedArray)
normalDrawables.apply {
gradientType = GradientDrawable.LINEAR_GRADIENT
cornerRadii = createRadius(typedArray)
setStroke(createStrokeWith(typedArray), createStrokeColor(typedArray))
shape = createShapetyle(typedArray)
}
pressedDrawables = createPressedGradientDrawable(typedArray)
pressedDrawables.apply {
gradientType = GradientDrawable.LINEAR_GRADIENT
cornerRadii = createRadius(typedArray)
setStroke(createPressedStrokeWith(typedArray), createPressedStrokeColor(typedArray))
shape = createShapetyle(typedArray)
}
rippleDrawable = RippleDrawable(
ColorStateList(
arrayOf(
intArrayOf(android.R.attr.state_pressed),
intArrayOf(android.R.attr.state_focused),
intArrayOf(android.R.attr.state_activated)
),
intArrayOf(
typedArray.getColor(R.styleable.JoySelectorLayout_joy_ripple_color, 0),
typedArray.getColor(R.styleable.JoySelectorLayout_joy_ripple_color, 0),
typedArray.getColor(R.styleable.JoySelectorLayout_joy_ripple_color, 0)
)
),
normalDrawables, ShapeDrawable()
)
stateDrawable.addState(
intArrayOf(
android.R.attr.state_focused,
android.R.attr.state_pressed,
android.R.attr.state_enabled
), pressedDrawables
)
stateDrawable.addState(
intArrayOf(android.R.attr.state_pressed),
pressedDrawables
)
stateDrawable.addState(
intArrayOf(android.R.attr.state_focused),
pressedDrawables
)
stateDrawable.addState(
intArrayOf(android.R.attr.state_enabled),
normalDrawables
)
stateDrawable.addState(intArrayOf(), normalDrawables)
val needRipple = typedArray.getBoolean(R.styleable.JoySelectorLayout_joy_ripple, false)
background = if(needRipple) rippleDrawable else stateDrawable
typedArray.recycle()
}
}
/**
* 创建圆弧半径数组
*/
private fun createCornerArray(typedArray: TypedArray): FloatArray {
return floatArrayOf(
typedArray.getDimension(
R.styleable.JoySelectorLayout_joy_lt_corner_radius,
0f
),
typedArray.getDimension(
R.styleable.JoySelectorLayout_joy_lt_corner_radius,
0f
),
typedArray.getDimension(
R.styleable.JoySelectorLayout_joy_rt_corner_radius,
0f
),
typedArray.getDimension(
R.styleable.JoySelectorLayout_joy_rt_corner_radius,
0f
),
typedArray.getDimension(
R.styleable.JoySelectorLayout_joy_rb_corner_radius,
0f
),
typedArray.getDimension(
R.styleable.JoySelectorLayout_joy_rb_corner_radius,
0f
),
typedArray.getDimension(
R.styleable.JoySelectorLayout_joy_lb_corner_radius,
0f
),
typedArray.getDimension(
R.styleable.JoySelectorLayout_joy_lb_corner_radius,
0f
)
)
}
/**
* 创建样式
*/
private fun createShapetyle(typedArray: TypedArray): Int {
shapeStyle = typedArray.getString(R.styleable.JoySelectorLayout_joy_shape_style)
return if (TextUtils.equals("2", shapeStyle))
GradientDrawable.RING
else
GradientDrawable.RECTANGLE
}
/**
* 生成边框宽度
*/
private fun createStrokeWith(typedArray: TypedArray): Int {
mStrokeWidth =
typedArray.getDimension(
R.styleable.JoySelectorLayout_joy_stroke_width, 0f
).toInt()
return mStrokeWidth
}
/**
* 生成边框颜色
*/
private fun createStrokeColor(typedArray: TypedArray): Int {
mStrokeColor = typedArray.getColor(
R.styleable.JoySelectorLayout_joy_stroke_color, 0
)
return mStrokeColor
}
/**
* 生成点击状态下边框宽度
*/
private fun createPressedStrokeWith(typedArray: TypedArray): Int {
return typedArray.getDimension(
R.styleable.JoySelectorLayout_joy_pressed_stroke_width, 0f
).toInt()
}
/**
* 生成点击状态下边框颜色
*/
private fun createPressedStrokeColor(typedArray: TypedArray): Int {
return typedArray.getColor(
R.styleable.JoySelectorLayout_joy_pressed_stroke_color, 0
)
}
/**
* 决定用哪个方向的GradientDrawable
*/
private fun createGradientDrawable(
typedArray: TypedArray
): GradientDrawable {
val direction = typedArray.getString(
R.styleable.JoySelectorLayout_joy_direction
)
val colors = createColors(typedArray)
return GradientDrawable(
when (direction) {
"2" -> GradientDrawable.Orientation.TOP_BOTTOM
"3" -> GradientDrawable.Orientation.RIGHT_LEFT
"4" -> GradientDrawable.Orientation.BOTTOM_TOP
"5" -> GradientDrawable.Orientation.TL_BR
"6" -> GradientDrawable.Orientation.TR_BL
"7" -> GradientDrawable.Orientation.BL_TR
"8" -> GradientDrawable.Orientation.BR_TL
else -> GradientDrawable.Orientation.LEFT_RIGHT
}, colors
)
}
/**
* 创建圆角
*/
private fun createRadius(typedArray: TypedArray): FloatArray {
val radius =
typedArray.getDimension(R.styleable.JoySelectorLayout_joy_corner_radius, 0f)
return if (radius != 0f)
floatArrayOf(radius, radius, radius, radius, radius, radius, radius, radius)
else
createCornerArray(typedArray)
}
private fun createPressedGradientDrawable(typedArray: TypedArray): GradientDrawable {
val direction = typedArray.getString(
R.styleable.JoySelectorLayout_joy_pressed_direction
)
val colors = createPressedColors(typedArray)
return when (direction) {
"2" -> GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, colors)
"3" -> GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, colors)
"4" -> GradientDrawable(GradientDrawable.Orientation.BOTTOM_TOP, colors)
"5" -> GradientDrawable(GradientDrawable.Orientation.TL_BR, colors)
"6" -> GradientDrawable(GradientDrawable.Orientation.TR_BL, colors)
"7" -> GradientDrawable(GradientDrawable.Orientation.BL_TR, colors)
"8" -> GradientDrawable(GradientDrawable.Orientation.BR_TL, colors)
else -> GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, colors)
}
}
private fun createPressedColors(typedArray: TypedArray): IntArray {
val bgColor = typedArray.getColor(
R.styleable.JoySelectorLayout_joy_pressed_bg_color, -1
)
val colors = if (bgColor != -1) {
intArrayOf(bgColor, bgColor)
} else {
intArrayOf(
typedArray.getColor(
R.styleable.JoySelectorLayout_joy_pressed_start_color,
0
),
typedArray.getColor(
R.styleable.JoySelectorLayout_joy_pressed_end_color,
0
)
)
}
return colors
}
/**
* 创建背景颜色
*/
private fun createColors(typedArray: TypedArray): IntArray {
var bgColor = typedArray.getColor(
R.styleable.JoySelectorLayout_joy_bg_color, -1
)
val colors = if (bgColor != -1) {
intArrayOf(bgColor, bgColor)
} else {
intArrayOf(
typedArray.getColor(
R.styleable.JoySelectorLayout_joy_start_color,
0
),
typedArray.getColor(
R.styleable.JoySelectorLayout_joy_end_color,
0
)
)
}
return colors
}
/**
* 边框颜色,边框宽度默认为1像素
*
* @param color
*/
fun setStrokeColor(color: Int) {
normalDrawables.setStroke(mStrokeWidth, color)
background = stateDrawable
}
/**
* 边框颜色宽度都可以设置
*
* @param width
* @param color
*/
fun setStrokeColor(width: Int, color: Int) {
mStrokeWidth = width
setStrokeColor(color)
}
/**
* 动态设置背景色,单色
*
* @param color
*/
fun setBgColor(color: Int) {
normalDrawables.setColor(color)
background = stateDrawable
}
/**
* 动态设置背景色,渐变色
*
* @param colors
*/
fun setBgColors(colors: IntArray?) {
normalDrawables.colors = colors
background = stateDrawable
}
}