Android自定义View之功能强大的文本、按钮控件

一、需求分析

我们经常会看到这种按钮:
在这里插入图片描述
特点就是:
①圆角;
②有边框;
③有图标,并且图标+文字一起居中。
前两个需求很容易实现,写个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

四、计算位置

提前把位置计算好,这样绘制的时候直接拿计算结果进行绘制。

  1. 如果用户选择点击时放大,那么最初绘制的时候需要在上下左右留一点间隔,用来做放大的区域,这个区域的大小是由属性changeSizeSpan决定,默认是2dp,同样文字也会默认放大1dp,可以通过changeTextSizeSpan设置文字放大多少。
  2. 如果用户选择点击时缩小,那么就不用考虑留间隙了,缩小的范围同样是上面两个属性决定的。
  3. 除了上面的考虑点,还得考虑边框宽度,绘制的宽高度要再减去边框,不然边框可能显示不下,因为正常边框和点击时的边框都可以定制,所以选这里面的最大值作为减掉的空间。
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上,有需要的自己复制一下拷贝的项目里:链接
别忘了还有属性文件!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值