一、需求分析
我们经常会看到这种按钮:
特点就是:
①圆角;
②有边框;
③有图标,并且图标+文字一起居中。
前两个需求很容易实现,写个shape文件就行了,第三个需求就没那么容易实现了,当然原生的Button和TextView都有drawableXXX属性,再组合上padding属性,也可以实现这种效果,例如:
<TextView
android:layout_width="260dp"
android:layout_height="60dp"
android:drawableStart="@mipmap/wechat"
android:paddingStart="80dp"
android:gravity="center_vertical"
android:text="微信登录"
android:textColor="@color/black"
android:textSize="18sp"/>
利用 paddingStart=“80dp” 将图标移到中间位置,这样效果实现了,但未免也太low了一点,还得自己去计算位置。
此外还有一些其他的需求,例如想要按钮更加生动一点,希望按下去的时候会使得按钮发生变化,放大或者缩小一点,Q弹的感觉,这种需求利用原生Button就比较难实现了。
因此针对以上需求,设计了一款名为 TextButton 的控件,通过简单设置属性满足以上需求:
<TextButton
android:id="@+id/tb_login"
android:layout_width="260dp"
android:layout_height="60dp"
android:layout_centerInParent="true"
app:backgroundColor="#ECDB4B"
app:angleRadius="15dp"
app:strokeWidth="2dp"
app:strokeColor="@color/black"
app:textColor="@color/black"
app:textLocation="center"
app:isBold="true"
app:text="微信登录"
app:textSize="16sp"
app:onClickStrokeWidth="1dp"
app:iconCenter="@mipmap/wechat"
app:onClickStyle="narrow"/>
二、准备属性
需要一下满足这么多需求,就得自定义很多属性:
<declare-styleable name="TextButton">
<attr name="iconCenter" format="reference"/>
<attr name="iconLeft" format="reference"/>
<attr name="iconRight" format="reference"/>
<attr name="iconTop" format="reference"/>
<attr name="iconBottom" format="reference"/>
<attr name="text" format="string"/>
<attr name="angleRadius" format="dimension"/>
<attr name="backgroundColor" format="color"/>
<attr name="textSize" format="dimension"/>
<attr name="textColor" format="color"/>
<attr name="isBold" format="boolean"/>
<attr name="onClickStyle" format="string"/>
<attr name="onClickBackground" format="color"/>
<attr name="strokeWidth" format="dimension"/>
<attr name="strokeColor" format="color"/>
<attr name="onClickStrokeWidth" format="dimension"/>
<attr name="onClickStrokeColor" format="color"/>
<attr name="onClickTextColor" format="color"/>
<attr name="onClickText" format="string"/>
<attr name="afterClickText" format="string"/>
<attr name="textLocation" format="string"/>
<attr name="changeSizeSpan" format="dimension"/>
<attr name="changeTextSizeSpan" format="dimension"/>
</declare-styleable>
解释一下:
iconXxx:图标属性,上下左右中,能够同时显示
text:要显示的文字
angleRadius:圆角半径
backgroundColor:背景颜色
textSize:文字大小
textColor:文字颜色
isBold:是否加粗
onClickStyle:点击那一瞬间的样式,有很多种:expand–放大 narrow–缩小 change–改变颜色
onClickBackground:选择改变颜色时,改变后的颜色就是来自这个属性
strokeWidth:边框宽度
strokeColor:边框颜色
onClickStrokeWidth:点击时的边框宽度
onClickStrokeColor:点击时的边框颜色
onClickTextColor:点击时的文字颜色
onClickText:点击时的文字
afterClickText:点击后的文字,有这种需求,比如点击登录后现在正在登录
textLocation:文字的位置,默认居中,还有top、left、bottom和right
changeSizeSpan:控件放大或缩小的跨度
changeTextSizeSpan:文字放大或缩小的跨度
三、获取属性
先声明全局变量,在初始化的时候拿到属性值:
val theme = context.theme.obtainStyledAttributes(attrs, R.styleable.TextButton,0,0)
text = theme.getString(R.styleable.TextButton_text)
angleRadius = theme.getDimension(R.styleable.TextButton_angleRadius,0f)
iconCenter = theme.getDrawable(R.styleable.TextButton_iconCenter)
.......
theme.recycle()
太多了只复制了部分过来。
然后初始化画笔,设置一些属性,比如描边还是填充、抗锯齿、画笔宽度等:
strokePaint.color = strokeColor
strokePaint.style = Paint.Style.STROKE
strokePaint.strokeWidth = strokeWidth
strokePaint.isDither = true
strokePaint.isAntiAlias = true
四、计算位置
提前把位置计算好,这样绘制的时候直接拿计算结果进行绘制。
- 如果用户选择点击时放大,那么最初绘制的时候需要在上下左右留一点间隔,用来做放大的区域,这个区域的大小是由属性changeSizeSpan决定,默认是2dp,同样文字也会默认放大1dp,可以通过changeTextSizeSpan设置文字放大多少。
- 如果用户选择点击时缩小,那么就不用考虑留间隙了,缩小的范围同样是上面两个属性决定的。
- 除了上面的考虑点,还得考虑边框宽度,绘制的宽高度要再减去边框,不然边框可能显示不下,因为正常边框和点击时的边框都可以定制,所以选这里面的最大值作为减掉的空间。
val m = max(strokeWidth,onClickStrokeWidth)//获取初始边框和点击边框的最大值
when(onClickStyle){
OnClickStyle.EXPAND,OnClickStyle.EXPAND_CHANGE ->{
dWidth = mWidth - changeSizeSpan - m * 2 //初始宽度 ,m*2是因为边框左右共两个,上下也共两个
dHeight = mHeight - changeSizeSpan - m * 2//初始高度
onClickHeight = dHeight + changeSizeSpan //扩大高度
onClickWidth = dWidth + changeSizeSpan //扩大宽度
onClickTextSize = textSize + changeTextSizeSpan //扩大字体大小
}
OnClickStyle.NARROW,OnClickStyle.NARROW_CHANGE -> {
dWidth = mWidth - m * 2 //上面一样 对应缩小
dHeight = mHeight - m * 2
onClickHeight = dHeight - changeSizeSpan
onClickWidth = dWidth - changeSizeSpan
onClickTextSize = textSize - changeTextSizeSpan
}
else -> { //其他情况只用减掉边框就行
dWidth = mWidth - m * 2
dHeight = mHeight - m * 2
onClickHeight = dHeight
onClickWidth = dWidth
onClickTextSize = normalTextSize
}
}
//正常的高度宽度字体大小
normalHeight = dHeight
normalWidth = dWidth
normalTextSize = textSize
上面只是计算控件的尺寸,还得计算文字绘制的起始位置:
if(onclickState){//如果是点击状态计算点击文字的宽度
drawText?.let {
textW = textPaint.measureText(it)
}
}else{
text?.let {//否则计算普通文字的宽度
textW = textPaint.measureText(it)
}
}
when(textLocation){
Location.CENTER->{
drawTextStartX = pointX - textW / 2 + pdL - pdR //文字的起始x坐标
if(drawTextStartX<0) //如果算出来已经越界了 调整到从紧贴边框开始
drawTextStartX = strokeWidth
iconCenter?.let { d ->
drawTextStartX += d.intrinsicWidth / 2//如果中间还有图标 继续右移动图标一半的距离
}
drawTextStartY = pointY + textH + pdT - pdB //文字的起始y坐标,跟padding属性有关
if(drawTextStartY<textSize)//越界处理,但y坐标是指文字的底部,所以还得加上文字的大小,这样文字上面正好贴着边框
drawTextStartY = strokeWidth + textSize
}
.........//计算其他情况的位置
}
同样还有图标的位置计算:
when(location){
Location.CENTER-> {
iconCenter?.let {
if (textLocation == Location.CENTER) { //如果是中间图标和中间文字
drawIconCenterLeft = drawTextStartX - it.intrinsicWidth //文字的起始位置 - 图标的宽度
if (drawIconCenterLeft < 0)//越界处理
drawIconCenterLeft = strokeWidth
drawIconCenterRight = drawIconCenterLeft + it.intrinsicWidth//图标的右边
} else {
drawIconCenterLeft = pointX - it.intrinsicWidth / 2//文字没在中间,就绘制在正中间,中心点 - 图标宽度一半
if (drawIconCenterLeft < 0)//越界处理
drawIconCenterLeft = strokeWidth
drawIconCenterRight = drawIconCenterLeft + it.intrinsicWidth//图标的右边
}
drawIconCenterTop = pointY - it.intrinsicHeight / 2 + pdT - pdB//图标的上面中心位置减图标高度一半 再处理padding属性
if (drawIconCenterTop < 0)
drawIconCenterTop = strokeWidth//越界处理
drawIconCenterBottom = drawIconCenterTop + it.intrinsicHeight//图标下面
}
}
........//计算其他位置的坐标
五、判断点击
实现点击和长按监听,我一般是重写onTouchEven方法,在里面做判断:
override fun onTouchEvent(event: MotionEvent?): Boolean {
when(event?.action){
MotionEvent.ACTION_DOWN->{
downTime = System.currentTimeMillis()//记录按下的时间
when(onClickStyle){ //根据用户设置的属性改变参数
OnClickStyle.EXPAND->
changeState(false,OnClickStyle.EXPAND)
}
.........
}
postInvalidate()//重新绘制
return true
}
MotionEvent.ACTION_UP->{
if(System.currentTimeMillis() - downTime < 200){//判断与按下的时间差
onClickListener?.onClick(this) //回调监听方法
}else{
onLongClickListener?.onLongClick(this) //回调长按监听方法
}
when(onClickStyle){
OnClickStyle.EXPAND->{
changeState(true,OnClickStyle.EXPAND) //根据用户设置的属性改变参数
}
.........
}
postInvalidate()//重新绘制
return true
}
}
return false
}
六、改变参数
就是拿前面计算好的值进行赋值:
private fun changeState(onClick:Boolean,state:OnClickStyle){
if(onClick){//点击时赋值点击的数据
dHeight = onClickHeight
dWidth = onClickWidth
textSize = onClickTextSize
textPaint.textSize = textSize
strokePaint.color = onClickStrokeColor
strokePaint.strokeWidth = onClickStrokeWidth
}else{//否则复制正常的数据
dHeight = normalHeight
dWidth = normalWidth
textSize = normalTextSize
textPaint.textSize = textSize
strokePaint.color = strokeColor
strokePaint.strokeWidth = strokeWidth
}
}
七、绘制
这才是核心啊:
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
//先画背景
canvas?.drawRoundRect((mWidth - dWidth)/2,(mHeight - dHeight)/2,(mWidth - dWidth)/2+dWidth,(mHeight - dHeight)/2+dHeight,angleRadius,angleRadius,bgPaint)
//再描边
if(strokeWidth!=0f){
canvas?.drawRoundRect((mWidth - dWidth)/2,(mHeight - dHeight)/2,(width - dWidth)/2+dWidth,(mHeight - dHeight)/2+dHeight,angleRadius,angleRadius,strokePaint)
}
getTextAddress()//计算文字的绘制位置
//根据不同状态绘制不同文字
if(onclickState){
drawText?.let {
textPaint.color = onClickTextColor
canvas?.drawText(it, drawTextStartX, drawTextStartY, textPaint)
}
}else {
text?.let {
textPaint.color = textColor
canvas?.drawText(it, drawTextStartX, drawTextStartY, textPaint)
}
}
iconCenter?.let {
getIconAddress(Location.CENTER)//计算图片的绘制位置
val rect = Rect(drawIconCenterLeft.toInt(),drawIconCenterTop.toInt(),drawIconCenterRight.toInt(),drawIconCenterBottom.toInt())
iconCenter?.bounds = rect
canvas?.let {
its->
iconCenter?.draw(its)
}
}
......//其他图片
}
八、源码
源码非常多,放到了GitHub上,有需要的自己复制一下拷贝的项目里:链接
别忘了还有属性文件!