我们开发Android的时候经常会碰到给按钮或者文本设置背景,圆角,填充颜色,描边,按压状态这些样式,首先想到的就是用shape,selector生成一个xml文件然后通过drawable引用,但是随着项目维护迭代的时间越长,你会发现shape,selector文件的数量会疯狂增加,可能有时候几个人同事开发也会创建一样的样式,很难进行管理,今天我们就通过自定义View来减少shape这歌文件的数量,只要通过设置属性就可以实现想要的效果。
通用shape样式按钮CommonShapeButton:
package com.blue.view
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.content.res.ColorStateList
import android.graphics.Canvas
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.RippleDrawable
import android.graphics.drawable.StateListDrawable
import android.os.Build
import android.support.v7.widget.AppCompatButton
import android.util.AttributeSet
import android.view.Gravity
import com.blue.R
/**
* 通用shape样式按钮
*/
class CommonShapeButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatButton(context, attrs, defStyleAttr) {
private companion object {
val TOP_LEFT = 1
val TOP_RIGHT = 2
val BOTTOM_RIGHT = 4
val BOTTOM_LEFT = 8
}
/**
* shape模式
* 矩形(rectangle)、椭圆形(oval)、线形(line)、环形(ring)
*/
private var mShapeMode = 0
/**
* 填充颜色
*/
private var mFillColor = 0
/**
* 按压颜色
*/
private var mPressedColor = 0
/**
* 描边颜色
*/
private var mStrokeColor = 0
/**
* 描边宽度
*/
private var mStrokeWidth = 0
/**
* 圆角半径
*/
private var mCornerRadius = 0
/**
* 圆角位置
*/
private var mCornerPosition = 0
/**
* 点击动效
*/
private var mActiveEnable = false
/**
* 起始颜色
*/
private var mStartColor = 0
/**
* 结束颜色
*/
private var mEndColor = 0
/**
* 渐变方向
* 0-GradientDrawable.Orientation.TOP_BOTTOM
* 1-GradientDrawable.Orientation.LEFT_RIGHT
*/
private var mOrientation = 0
/**
* drawable位置
* -1-null、0-left、1-top、2-right、3-bottom
*/
private var mDrawablePosition = -1
/**
* 普通shape样式
*/
private val normalGradientDrawable: GradientDrawable by lazy { GradientDrawable() }
/**
* 按压shape样式
*/
private val pressedGradientDrawable: GradientDrawable by lazy { GradientDrawable() }
/**
* shape样式集合
*/
private val stateListDrawable: StateListDrawable by lazy { StateListDrawable() }
// button内容总宽度
private var contentWidth = 0f
// button内容总高度
private var contentHeight = 0f
init {
context.obtainStyledAttributes(attrs, R.styleable.CommonShapeButton).apply {
mShapeMode = getInt(R.styleable.CommonShapeButton_csb_shapeMode, 0)
mFillColor = getColor(R.styleable.CommonShapeButton_csb_fillColor, 0xFFFFFFFF.toInt())
mPressedColor = getColor(R.styleable.CommonShapeButton_csb_pressedColor, 0xFF666666.toInt())
mStrokeColor = getColor(R.styleable.CommonShapeButton_csb_strokeColor, 0)
mStrokeWidth = getDimensionPixelSize(R.styleable.CommonShapeButton_csb_strokeWidth, 0)
mCornerRadius = getDimensionPixelSize(R.styleable.CommonShapeButton_csb_cornerRadius, 0)
mCornerPosition = getInt(R.styleable.CommonShapeButton_csb_cornerPosition, -1)
mActiveEnable = getBoolean(R.styleable.CommonShapeButton_csb_activeEnable, false)
mDrawablePosition = getInt(R.styleable.CommonShapeButton_csb_drawablePosition, -1)
mStartColor = getColor(R.styleable.CommonShapeButton_csb_startColor, 0xFFFFFFFF.toInt())
mEndColor = getColor(R.styleable.CommonShapeButton_csb_endColor, 0xFFFFFFFF.toInt())
mOrientation = getColor(R.styleable.CommonShapeButton_csb_orientation, 0)
recycle()
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
// 初始化normal状态
with(normalGradientDrawable) {
// 渐变色
if (mStartColor != 0xFFFFFFFF.toInt() && mEndColor != 0xFFFFFFFF.toInt()) {
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
}
// 统一设置圆角半径
if (mCornerPosition == -1) {
cornerRadius = mCornerRadius.toFloat()
}
// 根据圆角位置设置圆角半径
else {
cornerRadii = getCornerRadiusByPosition()
}
// 默认的透明边框不绘制,否则会导致没有阴影
if (mStrokeColor != 0) {
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 = mCornerRadius.toFloat()
setStroke(mStrokeWidth, mStrokeColor)
}
// 注意此处的add顺序,normal必须在最后一个,否则其他状态无效
// 设置pressed状态
stateListDrawable.apply {
addState(intArrayOf(android.R.attr.state_pressed), pressedGradientDrawable)
// 设置normal状态
addState(intArrayOf(), normalGradientDrawable)
}
}
} else {
normalGradientDrawable
}
}
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
changeTintContextWrapperToActivity()
}
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)
}
/**
* 从support23.3.0开始View中的getContext方法返回的是TintContextWrapper而不再是Activity
* 如果使用xml注册onClick属性,就会通过反射到Activity中去找对应的方法
* 5.0以下系统会反射到TintContextWrapper中去找对应的方法,程序直接crash
* 所以这里需要针对5.0以下系统单独处理View中的getContext返回值
*/
private fun changeTintContextWrapperToActivity() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
getActivity()?.let {
var clazz: Class<*>? = this::class.java
while (clazz != null) {
try {
val field = clazz.getDeclaredField("mContext")
field.isAccessible = true
field.set(this, it)
break
} catch (e: Exception) {
e.printStackTrace()
}
clazz = clazz.superclass
}
}
}
}
/**
* 从context中得到真正的activity
*/
private fun getActivity(): Activity? {
var context = context
while (context is ContextWrapper) {
if (context is Activity) {
return context
}
context = context.baseContext
}
return null
}
/**
* 根据圆角位置获取圆角半径
*/
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
}
}
attr.xml中的代码:
<resources>
<declare-styleable name="CommonShapeButton">
<!--shape模式-->
<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_cornerPosition">
<flag name="topLeft" value="1" />
<flag name="topRight" value="2" />
<flag name="bottomRight" value="4" />
<flag name="bottomLeft" value="8" />
</attr>
<!--是否开启动效-->
<attr name="csb_activeEnable" format="boolean" />
<!--drawable位置-->
<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>
</resources>
style.xml中的代码:
<resources>
<!-- 自定义按钮样式 -->
<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>
</resources>
在使用的时候只需要像使用自定义View一样在布局文件中设置相应的属性即可实现shape样式。
例如下面的例子:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
</data>
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#fff"
android:orientation="vertical">
<com.blue.view.CommonShapeButton
android:layout_width="300dp"
android:layout_height="50dp"
android:layout_margin="10dp"
android:text="文本样式+填充颜色+圆角"
android:textColor="#fff"
app:csb_cornerRadius="50dp"
app:csb_fillColor="#00bc71" />
<com.blue.view.CommonShapeButton
style="@style/CommonShapeButtonStyle"
android:layout_width="300dp"
android:layout_height="50dp"
android:layout_margin="10dp"
android:text="按钮样式+填充颜色+圆角"
android:textColor="#fff"
app:csb_cornerRadius="50dp"
app:csb_fillColor="#00bc71" />
<com.blue.view.CommonShapeButton
style="@style/CommonShapeButtonStyle"
android:layout_width="300dp"
android:layout_height="50dp"
android:layout_margin="10dp"
android:text="按钮样式+水波纹+填充颜色+圆角"
android:textColor="#fff"
app:csb_activeEnable="true"
app:csb_cornerRadius="50dp"
app:csb_fillColor="#00bc71" />
<com.blue.view.CommonShapeButton
android:layout_width="300dp"
android:layout_height="50dp"
android:layout_margin="10dp"
android:text="文本样式+填充颜色+描边"
android:textColor="#fff"
app:csb_activeEnable="true"
app:csb_fillColor="#00bc71"
app:csb_strokeColor="#000"
app:csb_strokeWidth="1dp" />
<com.blue.view.CommonShapeButton
android:layout_width="300dp"
android:layout_height="50dp"
android:layout_margin="10dp"
android:text="文本样式+水波纹+左右渐变"
android:textColor="#fff"
app:csb_activeEnable="true"
app:csb_endColor="#00bc71"
app:csb_orientation="LEFT_RIGHT"
app:csb_startColor="#8800bc71" />
<com.blue.view.CommonShapeButton
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_margin="10dp"
android:text="椭圆"
android:textColor="#fff"
app:csb_fillColor="#00bc71"
app:csb_shapeMode="oval"
app:csb_strokeColor="#000"
app:csb_strokeWidth="1dp" />
<Button
android:layout_width="180dp"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:drawableLeft="@drawable/ic_changebtn_change"
android:text="默认按钮图标无法居中"
android:textColor="#808080"
android:textSize="11dp" />
<com.blue.view.CommonShapeButton
android:layout_width="150dp"
android:layout_height="30dp"
android:layout_margin="10dp"
android:drawableLeft="@drawable/ic_changebtn_change"
android:text="带图标样式居中"
android:textColor="#808080"
android:textSize="11dp"
app:csb_cornerRadius="50dp"
app:csb_drawablePosition="left"
app:csb_strokeColor="#e6e6e6"
app:csb_strokeWidth="0.5dp" />
<com.blue.view.CommonShapeButton
android:layout_width="150dp"
android:layout_height="30dp"
android:layout_margin="10dp"
android:drawableRight="@drawable/ic_changebtn_change"
android:text="带图标样式居中"
android:textColor="#808080"
android:textSize="11dp"
app:csb_cornerRadius="50dp"
app:csb_drawablePosition="right"
app:csb_strokeColor="#e6e6e6"
app:csb_strokeWidth="0.5dp" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.blue.view.CommonShapeButton
android:id="@+id/btn"
android:layout_width="50dp"
android:layout_height="130dp"
android:layout_margin="10dp"
android:drawableTop="@drawable/ic_changebtn_change"
android:text="带图标样式居中"
android:textColor="#808080"
android:textSize="11dp"
app:csb_cornerRadius="50dp"
app:csb_drawablePosition="top"
app:csb_strokeColor="#e6e6e6"
app:csb_strokeWidth="0.5dp" />
<com.blue.view.CommonShapeButton
android:id="@+id/btn2"
android:layout_width="50dp"
android:layout_height="130dp"
android:layout_margin="10dp"
android:layout_toRightOf="@+id/btn"
android:drawableBottom="@drawable/ic_changebtn_change"
android:text="带图标样式居中"
android:textColor="#808080"
android:textSize="11dp"
app:csb_cornerRadius="50dp"
app:csb_drawablePosition="bottom"
app:csb_strokeColor="#e6e6e6"
app:csb_strokeWidth="0.5dp" />
<com.blue.view.CommonShapeButton
android:id="@+id/btn3"
android:layout_width="130dp"
android:layout_height="50dp"
android:layout_margin="10dp"
android:layout_toRightOf="@+id/btn2"
android:drawableTop="@drawable/ic_changebtn_change"
android:text="带图标样式居中"
android:textColor="#808080"
android:textSize="11dp"
app:csb_cornerRadius="50dp"
app:csb_drawablePosition="top"
app:csb_strokeColor="#e6e6e6"
app:csb_strokeWidth="0.5dp" />
<com.blue.view.CommonShapeButton
android:id="@+id/btn4"
android:layout_width="130dp"
android:layout_height="50dp"
android:layout_below="@+id/btn3"
android:layout_margin="10dp"
android:layout_toRightOf="@+id/btn2"
android:drawableBottom="@drawable/ic_changebtn_change"
android:text="带图标样式居中"
android:textColor="#808080"
android:textSize="11dp"
app:csb_cornerRadius="50dp"
app:csb_drawablePosition="bottom"
app:csb_strokeColor="#e6e6e6"
app:csb_strokeWidth="0.5dp" />
</RelativeLayout>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
</layout>
感兴趣的同学可以在自己的项目中试一试!
build.gradle文件中的设置如下(可根据自己的需要进行设置):
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 26
defaultConfig {
applicationId "com.blue"
minSdkVersion 16
targetSdkVersion 26
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
dataBinding {
enabled = true
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
// databinding
kapt "com.android.databinding:compiler:3.0.0"
}
在github上看到另一种替代drawable.xml的方案,有兴趣的同学可以点击查看https://github.com/whataa/noDrawable;