Android 自定义属性,自定义控件、自定义View以及View的常见Error
View.GONE: 不占据layout,但是对象还是存在,资源还是占用的
View.INVISIBLE: 占据layout,对象还是存在,资源还是占用的,只是不显示出来
自定义属性
1 要使用 系统的属性
可以使用所继承的控件的属性 如继承的TextView 或者 View 则相关的属性都可以不用声明直接使用
2 否则 要styleable中定义
<attr name="android:text" />
但是不用指定 format
3 在attrs.xml 中 编写 styleable和item等标签元素
在xml布局文件中使用该属性
在 java类中 通过TypedArray获取
attributeSet 可以获取 属性的名称和值
但是 若是属性值为 引用(如 @string @dimen)则得到的值为 值的id而非值
这样就需要根据ID再获取值
而通过TypedArray则简化了该步骤
直接用
TypedArray typedArray = mContext.obtainStyledAttributes(attributeSet, R.styleable.xxx);
这里的XXX 用的是 styleable的name + _ + 属性名
如:CircleProgressBar_hint
自定义控件
流程:
1 styleable 定义 xml中自定义组件使用的一些属性 如 颜色 文字等
2 自定义xml文件 为自定义组件的样式
3 自定义java文件 设置组件的 文字 颜色 等
4 组件名称即为包名+java类名
其实3 4 就可以实现控件了
只是要在 xml中就初始化设置值 就得再styleable中定义
实例见:Android自定义TopBar
自定义View
xml中直接用包名+类名使用 (也是会创建对象的)
而java 直接创建对象使用
属性则 用 style.xml设置,stylable的名称建议与java的类名一致
注意:自定义View的构造方法不能少了 attributeSet: AttributeSet 这个参数!!!!!!!!!!!!
不需要new 自定义控件类名然后传入context和attributes
直接写xml控件 (也是会创建对象的) 然后findViewById 自动会将context和attributes(即xml内容)传入
避免 LayoutInflater.from(context).inflate(R.layout.template01, this) 传null
这样会导致没有LayoutParams,最终viewgroup会给你生成一个默认的设置
替代View 为 RelativeLayout 用this
或者 自己制定一个root
View是可以有一个onDraw方法 提供了 画布的 可以在画布上绘制自己想要的内容
尽量别用onDraw方法去 绘制View 除非是自定义的 比较复杂的 drawBitmap drawText直接放在组件上就好
当不需要java类去初始化 操作一些东西 只有xml界面 就可以 用< include> 标签否则就自定义View用 <包名.类名>
问题1:oncreate中获取view.getMeasuredHeight 会为0
解决:手动调用测量方法
问题2:java.lang.ClassCastException: android.widget.RelativeLayout cannot be cast to cn.xxx.xxxxxx.View.Template01
Template01也是 继承 RelativeLayout的 为啥cast不了?
解决: 在 xml的RelativeLayout中 加context tools:context=“cn.sihao.mainfunctionmodule.View.Template01”
即可
问题3:java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child’s parent first.
原因:因为 这个view已经有了 父亲 得把他解除绑定才能 让他去加入新的布局
自定义View经验:
1 initAttrs(attrs)
先定好属性,长宽,颜色,半径,各种值等等 哪些是要xml设置的 哪些是根据 sizeChanged设置的
哪些是xml可以设置 可以不设置的(有默认值的)
2 initPaint()
定好形状 定好画笔 画圆内 还是 圆弧 尾部是否圆角 画笔颜色 宽度 是否抗锯齿等等
如 对于这种:
画两次drawCircle 一个的画笔是STROKE的 一个是FILL的
而要画不是完整360度的圆弧,则用drawArc
3 initObject()
初始化对象 一般如 圆心点 Point 矩形 RectF 等等
并且该调用在 initPaint() 之前
4 onMeasure
要用setMeasuredDimension设置好尺寸
可以在View的onMesure中 获取measureWidth和measureHeight 只有在view可见时,真正计算了长宽,才会回调,并且在super.OnMeasure之后去获取长宽是正确的
部分三星设备 若父view gone或者visible了 但是子view没有显性调用 此时并不会调用onMeasure 要在父view visibility改变的时候 也更新一下子类的visibility
setMeasuredDimension(getMeasureSize(true, widthMeasureSpec), getMeasureSize(false, heightMeasureSpec))
一般是固定的写法 当然可以根据需要更改
/**
* 获取View尺寸
*
* @param isWidth 是否是width,不是的话,是height
*/
private fun getMeasureSize(isWidth: Boolean, measureSpec: Int): Int {
var result = 0
val specSize = View.MeasureSpec.getSize(measureSpec)
val specMode = View.MeasureSpec.getMode(measureSpec)
when (specMode) {
View.MeasureSpec.UNSPECIFIED -> result = if (isWidth) {
suggestedMinimumWidth
} else {
suggestedMinimumHeight
}
View.MeasureSpec.AT_MOST -> result = if (isWidth)
Math.min(specSize, mWidth)
else
Math.min(specSize, mHeight)
View.MeasureSpec.EXACTLY -> result = specSize
}
return result
}
5 onSizeChanged
定实际的大小 半径 这个方法在第一次初始化时会调用一次 并且在 View的大小更改了也会再调用
尽量通过 控件之间的固定比例来设置大小 而不是 让用户去设置太多的大小(除非需要)
这里设置的大小是会随View大小变化而变化的 在xml中设置的值这是固定的
6 onDraw 进行View的绘制
canvas.drawArc .drawCircle .drawLine 等等
7 加载定时器 或者属性动画 调用 invalidate或者postInvalidate 进行onDraw方法的调用 进行View的更新
如:mScheduledCalTime.scheduleAtFixedRate
8 onDetachedFromWindow (重要) 一定别漏了
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
removeCallbacks(mScheduledCalTimeRunnable)
}
进行 定时器或者属性动画的关闭
取图片的色值
用android-studio 导入图片 左上角有个取色器的 可以获取到 #ARGB的值
只刷新指定的部分
invalidate或者postInvalidate中设置参数可以设置一个矩形区域指定要刷新的地方
不写参数则默认刷新整个自定义View 当然 若View没有显示或者没有改变 也是不会刷新的
layout inspector
在Tool ->layout inspector中 可以查看view的层级 占用的大小 位置等等 但是得是本地编译出来的包
实践:
贝塞尔曲线
// 画线
canvas.drawLine(起点x,起点y,终点x,终点y)
// 将画笔移至某点
mpath.moveTo(x,y)
// 结合 moveTo 画出一条直线 若无moveTo则默认从 0,0点开始
mPath.lineTo(x,y)
// x1,y1 为控制点,x2,y2为结束数据点
// 结合 moveTo设置数据点起点 用于绘制二阶贝塞尔曲线
mpath.quadTo(x1,y1,x2,y2)
//x1,y1 , x2,y2 为控制点 x3,y3 为数据点结束点
//也是结合 moveTo设置数据点起点 ,用于绘制 三阶贝塞尔曲线
mPath.cubicTo(x1,y1,x2,y2,x3,y3)
//x1,y1起点坐标 宽度 高度 画矩形
mRecF = new RectF(x1,y1,width,height)
//ovalRectF 为圆或者椭圆的矩形 startAngle 开始角度 sweepAngle 为结束角度
//手机坐标 起点是 左上角 x轴为横向 指向右侧 y轴为纵向 指向下侧
//起点 是从 x轴正方向开始 然后绕着 顺时针方向 即如果 是0,90 则所绘制的圆弧为右下角
mPath.arcTo(ovalRectF,startAngle,sweepAngle)
// 最后 绘制出路径
canvas.drawPath(mpath,mPaint);
实例之 自定义时钟:
主要就是利用三角函数 进行 时分秒针的位置定位
遇到的问题:
1 圆四个顶点有缺角:
因为 半径是不包括圆弧宽度的,因此 若圆弧宽度比较大,则会显示不全,因此在计算半径时要减去圆弧宽度*2
<declare-styleable name="MyCustomClockView">
<attr name="outerMostArcColor" />
<attr name="innerMostArcColor" />
<attr name="outerMostArcWidth" format="dimension" />
<attr name="innerMostCircleColor" />
<attr name="hourPointerWidth" format="dimension" />
<attr name="minutePointerWidth" format="dimension" />
<attr name="secondPointerWidth" format="dimension" />
<attr name="hourPointerColor" />
<attr name="minutePointerColor" />
<attr name="secondPointerColor" />
</declare-styleable>
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import com.orhanobut.logger.Logger
import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
/**
* 自定义时钟View
*/
class MyCustomClockView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private var mContext: Context? = null
/** 控件宽高 默认120dp */
private var mWidth: Int = dp2Px(120)
private var mHeight: Int = dp2Px(120)
/** 圆心位置 */
private var mCenterPoint: Point? = null
/** 外圆的内接矩形 */
private var mOutermostRectF: RectF? = null
/** 内圆的内接矩形 */
private var mInnermostRectF: RectF? = null
/** 最外圈的圆的半径 */
private var mOutermostCircleRadius: Float = 0f
/** 最内圈的圆的半径 */
private var mInnermostCircleRadius: Float = 0f
/** 最外圈的圆弧宽度 */
private var mOutermostArcWidth: Float? = null
/** 最内圈的圆弧宽度 */
private var mInnermostArcWidth: Float? = null
/** 最外圈的圆弧的颜色 */
private var mOutermostArcColor: Int? = null
/** 最内圈的圆弧的颜色 */
private var mInnermostArcColor: Int? = null
/** 最内圈的圆内的填充颜色 */
private var mInnermostCircleColor: Int? = null
/** 时针,分针,秒针的长度 */
private var mHourPointerLength: Float = 0f
private var mMinutePointerLength: Float = 0f
private var mSecondPointerLength: Float = 0f
/** 时针,分针,秒针的宽度 */
private var mHourPointerWidth: Float = 0f
private var mMinutePointerWidth: Float = 0f
private var mSecondPointerWidth: Float = 0f
/** 时针,分针,秒针的颜色 */
private var mHourPointerColor: Int? = null
private var mMinutePointerColor: Int? = null
private var mSecondPointerColor: Int? = null
/** 内圆的画笔 */
private var mInnermostCirclePaint: Paint? = null
/** 内圆弧的画笔 */
private var mInnermostArcPaint: Paint? = null
/** 外圆弧的画笔 */
private var mOutermostArcPaint: Paint? = null
/** 时针,分针,秒针的画笔 */
private var mHourPointerPaint: Paint? = null
private var mMinutePointerPaint: Paint? = null
private var mSecondPointerPaint: Paint? = null
/** 当前系统的时分秒时刻 */
private var mCurrentHour: Int = 0
private var mCurrentMinute: Int = 0
private var mCurrentSecond: Int = 0
private val mScheduledCalTime: ScheduledExecutorService = Executors.newScheduledThreadPool(1)
private val mScheduledCalTimeRunnable: Runnable = Runnable { getCurrentTime() }
companion object {
private const val TAG = "MyCustomClockView"
}
init {
mContext = context
initAttrs(attrs)
initObject()
initPaint()
mScheduledCalTime.scheduleAtFixedRate(mScheduledCalTimeRunnable, 0, 1, TimeUnit.SECONDS)
}
/**
* 加载属性
*/
private fun initAttrs(attributeSet: AttributeSet?) {
try {
if (attributeSet != null) {
val typedArray = mContext?.obtainStyledAttributes(attributeSet, R.styleable.MyCustomClockView)
val defColor = Color.BLACK
mOutermostArcColor = typedArray?.getColor(R.styleable.MyCustomClockView_outerMostArcColor, defColor)
mInnermostArcColor = typedArray?.getColor(R.styleable.MyCustomClockView_innerMostArcColor, defColor)
mInnermostCircleColor = typedArray?.getColor(R.styleable.MyCustomClockView_innerMostCircleColor, defColor)
mOutermostArcWidth = typedArray?.getDimension(R.styleable.MyCustomClockView_outerMostArcWidth, dp2Px(2).toFloat())
mHourPointerWidth = typedArray?.getDimension(R.styleable.MyCustomClockView_hourPointerWidth, dp2Px(6).toFloat())!!
mMinutePointerWidth = typedArray.getDimension(R.styleable.MyCustomClockView_minutePointerWidth, dp2Px(4).toFloat())
mSecondPointerWidth = typedArray.getDimension(R.styleable.MyCustomClockView_secondPointerWidth, dp2Px(2).toFloat())
mHourPointerColor = typedArray.getColor(R.styleable.MyCustomClockView_hourPointerColor, defColor)
mMinutePointerColor = typedArray.getColor(R.styleable.MyCustomClockView_minutePointerColor, defColor)
mSecondPointerColor = typedArray.getColor(R.styleable.MyCustomClockView_secondPointerColor, defColor)
Logger.t(TAG).d("外圆弧宽度为: $mOutermostArcWidth 时针宽度为: $mHourPointerWidth " +
"分针宽度为: $mMinutePointerWidth 秒针宽度为: $mSecondPointerWidth")
typedArray.recycle()
} else {
Logger.t(TAG).d("attributeSet为空")
}
} catch (npe: NullPointerException) {
Logger.t(TAG).e("typedArray为空: $npe")
} catch (e: Exception) {
Logger.t(TAG).e("exception: $e")
}
}
/** 初始化对象 */
private fun initObject() {
mCenterPoint = Point()
mOutermostRectF = RectF()
mInnermostRectF = RectF()
}
/** 加载画笔 */
private fun initPaint() {
try {
mInnermostCirclePaint = Paint()
mInnermostCirclePaint?.isAntiAlias = true
mInnermostCirclePaint?.style = Paint.Style.FILL
mInnermostCirclePaint?.color = mInnermostCircleColor!!
mInnermostArcPaint = Paint()
mInnermostArcPaint?.isAntiAlias = true
mInnermostArcPaint?.style = Paint.Style.STROKE
mInnermostArcPaint?.color = mInnermostArcColor!!
mOutermostArcPaint = Paint()
mOutermostArcPaint?.isAntiAlias = true
mOutermostArcPaint?.style = Paint.Style.STROKE
mOutermostArcPaint?.color = mOutermostArcColor!!
mOutermostArcPaint?.strokeWidth = mOutermostArcWidth!!
mHourPointerPaint = Paint()
mHourPointerPaint?.isAntiAlias = true
mHourPointerPaint?.style = Paint.Style.FILL_AND_STROKE
mHourPointerPaint?.strokeCap = Paint.Cap.ROUND
mHourPointerPaint?.color = mHourPointerColor!!
mHourPointerPaint?.strokeWidth = mHourPointerWidth
mMinutePointerPaint = Paint()
mMinutePointerPaint?.isAntiAlias = true
mMinutePointerPaint?.style = Paint.Style.FILL_AND_STROKE
mMinutePointerPaint?.strokeCap = Paint.Cap.ROUND
mMinutePointerPaint?.color = mMinutePointerColor!!
mMinutePointerPaint?.strokeWidth = mMinutePointerWidth
mSecondPointerPaint = Paint()
mSecondPointerPaint?.isAntiAlias = true
mSecondPointerPaint?.style = Paint.Style.FILL_AND_STROKE
mSecondPointerPaint?.strokeCap = Paint.Cap.ROUND
mSecondPointerPaint?.color = mSecondPointerColor!!
mSecondPointerPaint?.strokeWidth = mSecondPointerWidth
} catch (npe: NullPointerException) {
Logger.t(TAG).e("画笔颜色或宽度为空: $npe")
} catch (e: Exception) {
Logger.t(TAG).e("exception: $e")
}
}
/**
* View绘制中的测量方法
* @param widthMeasureSpec 宽度规格
* @param heightMeasureSpec 高度规格
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
setMeasuredDimension(getMeasureSize(true, widthMeasureSpec), getMeasureSize(false, heightMeasureSpec))
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
try {
Logger.t(TAG).d("onSizeChanged: w = $w; h = $h; oldw = $oldw; oldh = $oldh")
mWidth = w
mHeight = h
// 圆心坐标
mCenterPoint?.x = w / 2
mCenterPoint?.y = h / 2
//求控件宽高的最小值作为外圆半径 并且减去圆弧的宽度(x2),否则会造成部分圆弧绘制在外围
val widgetMinSize = Math.min(w - 2 *mOutermostArcWidth!!, h - 2 *mOutermostArcWidth!!)
// 用最小值作为外圆的半径
mOutermostCircleRadius = (widgetMinSize / 2)
// 内圆半径为外圆的 1/24
mInnermostCircleRadius = (mOutermostCircleRadius / 24)
// 内圆弧宽度为内圆半径的 0.6
mInnermostArcWidth = mInnermostCircleRadius * 0.6.toFloat()
// 时针长度为半径的0.68
mHourPointerLength = mOutermostCircleRadius * 0.68.toFloat()
// 分针长度为半径的0.80
mMinutePointerLength = mOutermostCircleRadius * 0.80.toFloat()
// 秒针长度为半径的0.90
mSecondPointerLength = mOutermostCircleRadius * 0.90.toFloat()
//绘制外圆弧的矩形边界 即 圆弧边宽的中心
mOutermostRectF?.left = mCenterPoint!!.x.toFloat() - mOutermostCircleRadius - mOutermostArcWidth!!.div(2)
mOutermostRectF?.top = mCenterPoint!!.y.toFloat() - mOutermostCircleRadius - mOutermostArcWidth!!.div(2)
mOutermostRectF?.right = mCenterPoint!!.x.toFloat() + mOutermostCircleRadius + mOutermostArcWidth!!.div(2)
mOutermostRectF?.bottom = mCenterPoint!!.y.toFloat() + mOutermostCircleRadius + mOutermostArcWidth!!.div(2)
//绘制内圆弧的矩形边界
mInnermostRectF?.left = mCenterPoint!!.x.toFloat() - mInnermostCircleRadius - mInnermostArcWidth!!.div(2)
mInnermostRectF?.top = mCenterPoint!!.y.toFloat() - mInnermostCircleRadius - mInnermostArcWidth!!.div(2)
mInnermostRectF?.right = mCenterPoint!!.x.toFloat() + mInnermostCircleRadius + mInnermostArcWidth!!.div(2)
mInnermostRectF?.bottom = mCenterPoint!!.y.toFloat() + mInnermostCircleRadius + mInnermostArcWidth!!.div(2)
Logger.t(TAG).d("控件宽度(mWidth)为: $mWidth" + " 控件高度(mHeight)为: $mHeight" + " 圆心x坐标为: ${mCenterPoint?.x}" + "\n" +
"圆心y坐标为: ${mCenterPoint?.y}" + " 外圆半径为: $mOutermostCircleRadius" + " 内圆半径为: $mInnermostCircleRadius" + "\n" +
"内圆弧宽度为: $mInnermostArcWidth" + " 时针长度为: $mHourPointerLength" + " 分针长度为: $mMinutePointerLength" + "\n" +
"秒针长度为: $mSecondPointerLength")
} catch (npe: NullPointerException) {
Logger.t(TAG).e("存在空值: $npe")
} catch (e: Exception) {
Logger.t(TAG).e("exception: $e")
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
drawOutermostArc(canvas)
drawHourPointer(canvas)
drawMinutePointer(canvas)
drawSecondPointer(canvas)
drawInnermostArc(canvas)
drawInnermostCircle(canvas)
}
/**
* 绘制外圆弧
*/
private fun drawOutermostArc(canvas: Canvas) {
canvas.drawArc(mOutermostRectF, 0f, 360f, false, mOutermostArcPaint)
}
/**
* 绘制内圆弧
*/
private fun drawInnermostArc(canvas: Canvas) {
mInnermostArcPaint?.strokeWidth = mInnermostArcWidth!!
canvas.drawArc(mInnermostRectF, 0f, 360f, false, mInnermostArcPaint)
}
/**
* 绘制内圆
*/
private fun drawInnermostCircle(canvas: Canvas) {
canvas.drawCircle(mCenterPoint!!.x.toFloat(), mCenterPoint!!.y.toFloat(), mInnermostCircleRadius, mInnermostCirclePaint)
}
/**
* 绘制时针
*/
private fun drawHourPointer(canvas: Canvas) {
// 以秒来算 因为不是说到了一小时才更新一次时针 而是一秒更新一次
val curHourPointerAngle = (mCurrentHour * 60 * 60 + mCurrentMinute * 60 + mCurrentSecond).toFloat().div(12 * 60 * 60) * 360 + 270
val curHourPointerX = mCenterPoint!!.x + Math.cos(Math.PI * 2.toFloat() .div(360) * curHourPointerAngle) * mHourPointerLength
val curHourPointerY = mCenterPoint!!.y + Math.sin(Math.PI * 2.toFloat() .div(360) * curHourPointerAngle) * mHourPointerLength
canvas.drawLine(mCenterPoint!!.x.toFloat(), mCenterPoint!!.y.toFloat(), curHourPointerX.toFloat(), curHourPointerY.toFloat(), mHourPointerPaint)
}
/**
* 绘制分针
*/
private fun drawMinutePointer(canvas: Canvas) {
val curMinutePointerAngle = (mCurrentMinute * 60 + mCurrentSecond).toFloat().div(60 * 60) * 360 + 270
val curMinutePointerX = mCenterPoint!!.x + Math.cos(Math.PI * 2.toFloat() .div(360) * curMinutePointerAngle) * mMinutePointerLength
val curMinutePointerY = mCenterPoint!!.y + Math.sin(Math.PI * 2.toFloat() .div(360) * curMinutePointerAngle) * mMinutePointerLength
canvas.drawLine(mCenterPoint!!.x.toFloat(), mCenterPoint!!.y.toFloat(), curMinutePointerX.toFloat(), curMinutePointerY.toFloat(), mMinutePointerPaint)
}
/**
* 绘制秒针
*/
private fun drawSecondPointer(canvas: Canvas) {
// 这里 +270度 因为 android绘制是以三点钟方向为起点 而这里是以 十二点钟方向为起点
val curSecondPointerAngle = mCurrentSecond.toFloat().div(60f).times(360) + 270
Logger.t(TAG).e(curSecondPointerAngle.toString())
// 这里+就好 因为三角函数是会有正负的 不用根据情况进行 + - 操作 并且这里的x用cos y用sin 以数学的坐标系的角度来看
val curSecondPointerX = mCenterPoint!!.x + Math.cos(Math.PI * 2.toFloat() .div(360) * curSecondPointerAngle) * mSecondPointerLength
val curSecondPointerY = mCenterPoint!!.y + Math.sin(Math.PI * 2.toFloat() .div(360) * curSecondPointerAngle) * mSecondPointerLength
canvas.drawLine(mCenterPoint!!.x.toFloat(), mCenterPoint!!.y.toFloat(), curSecondPointerX.toFloat(), curSecondPointerY.toFloat(), mSecondPointerPaint)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
removeCallbacks(mScheduledCalTimeRunnable)
}
/**
* 获取当前系统时间
*/
private fun getCurrentTime() {
val calendar = Calendar.getInstance()
mCurrentHour = calendar.get(Calendar.HOUR).rem(12)
mCurrentMinute = calendar.get(Calendar.MINUTE)
mCurrentSecond = calendar.get(Calendar.SECOND)
Logger.t(TAG).d("当前的小时(12小时制)为:$mCurrentHour 当前的分钟为:$mCurrentMinute 当前的秒为:$mCurrentSecond")
postInvalidate()
}
/**
* 获取View尺寸
*
* @param isWidth 是否是width,不是的话,是height
*/
private fun getMeasureSize(isWidth: Boolean, measureSpec: Int): Int {
var result = 0
val specSize = View.MeasureSpec.getSize(measureSpec)
val specMode = View.MeasureSpec.getMode(measureSpec)
when (specMode) {
View.MeasureSpec.UNSPECIFIED -> result = if (isWidth) {
suggestedMinimumWidth
} else {
suggestedMinimumHeight
}
View.MeasureSpec.AT_MOST -> result = if (isWidth)
Math.min(specSize, mWidth)
else
Math.min(specSize, mHeight)
View.MeasureSpec.EXACTLY -> result = specSize
}
return result
}
private fun dp2Px(dp: Int): Int {
val scale = context.resources.displayMetrics.density
return (dp * scale + 0.5f).toInt()
}
}
<com.xxx.xxx.xxx.MyCustomClockView
android:layout_width="480dp"
android:layout_height="480dp"
android:layout_marginLeft="40dp"
android:layout_marginTop="15dp"
custom:outerMostArcColor="@color/outerMostArcColor"
custom:innerMostArcColor="@color/innerMostArcColor"
custom:innerMostCircleColor="@color/innerMostCircleColor"
custom:hourPointerColor="@color/hourPointerColor"
custom:minutePointerColor="@color/minutePointerColor"
custom:secondPointerColor="@color/secondPointerColor"
custom:outerMostArcWidth="@dimen/outer_most_arc_width"
android:layout_marginStart="40dp" />
实例之 自定义圆环进度条:
效果:
一、在Sytleable中 设置 进度条的属性名 定义 xml中自定义组件使用的一些属性
<!-- 圆形进度条 -->
<declare-styleable name="CircleProgressBar">
<attr name="antiAlias" />
<attr name="startAngle" />
<attr name="sweepAngle" />
<attr name="animTime" />
<attr name="maxValue" />
<attr name="value" />
<attr name="precision" />
<attr name="valueSize" />
<attr name="valueColor" />
<attr name="textOffsetPercentInRadius" />
<!-- 绘制内容相应的提示语 -->
<attr name="hint" />
<attr name="hintSize" />
<attr name="hintColor" />
<!-- 绘制内容的单位 -->
<attr name="unit" />
<attr name="unitSize" />
<attr name="unitColor" />
<!-- 圆弧宽度 -->
<attr name="arcWidth" />
<attr name="arcColors" />
<!-- 背景圆弧颜色 -->
<attr name="bgArcColor" />
<!-- 背景圆弧宽度 -->
<attr name="bgArcWidth" format="dimension" />
</declare-styleable>
<!-- colors.xml -->
<color name="green">#00FF00</color>
<color name="blue">#EE9A00</color>
<color name="red">#EE0000</color>
<!-- 渐变颜色数组 -->
<integer-array name="gradient_arc_color">
<item>@color/green</item>
<item>@color/blue</item>
<item>@color/red</item>
</integer-array>
二、不用创建XML文件了 因为是自定义View 自己绘制的
三、建立一个继承 View的子类 命名为 CircleBar
主要设置
1 属性,画笔,测量 layout的onSizeChanged,draw方法
2 init方法 初始化 各种 对象 如 属性动画 如 圆的外接矩形 圆心坐标 画笔加载 属性加载
3 重写onMeasure方法 记得要调用setMeasuredDimension,来存储这个View经过测量得到的measured width and height
4 重写 onSizeChanged 方法 // 在控件大小发生改变时调用
5 调用onDraw()方法 再调用 DrawArc绘制圆弧和DrawText()方法绘制文字
并且这里 调用 了 canvas.save(); 从进度圆弧结束的地方开始重新绘制,既是优化性能又美观
onDraw方法会在收到WM_PAINT时调用
7 如界面初始化 调用 showWindow 或者 updateWindow时 又比如 自己调用invalidate方法
8 用 SweepGradient 去 设置渐变
9 并且 给 进度设置属性动画 调用 invalidate()方法 若视图大小无变化则不用执行layout 只重绘更改的部分 也是可以性能优化
10 记得释放资源 onDetachedFromWindow
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.RectF;
import android.graphics.SweepGradient;
import android.graphics.Typeface;
import android.support.annotation.Nullable;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import cn.sihao.customercirclebar.Util.MeasureUtil;
public class CircleBar extends View {
private static final int mDefaultSize = 150; // 这里默认设置150 而那个demo是要将他转为px 并且 数据写在Constant类中 作为 static final
private static final boolean ANTI_ALIAS = true;
private static final int DEFAULT_HINT_SIZE = 15;
private static final int DEFAULT_UNIT_SIZE = 30;
private static final int DEFAULT_MAX_VALUE = 100;
private static final int DEFAULT_VALUE = 50;
public static final int DEFAULT_START_ANGLE = 270;
public static final int DEFAULT_SWEEP_ANGLE = 360;
public static final int DEFAULT_ANIM_TIME = 1000;
public static final int DEFAULT_ARC_WIDTH = 15;
public static final int DEFAULT_VALUE_SIZE = 15;
private final String TAG = CircleBar.class.getSimpleName();
//绘制提示
private TextPaint mHintPaint;
private CharSequence mHint;
private int mHintColor;
private float mHintSize;
private float mHintOffset;
//绘制单位
private TextPaint mUnitPaint;
private CharSequence mUnit;
private int mUnitColor;
private float mUnitSize;
private float mUnitOffset;
//绘制背景圆弧
private Paint mBgArcPaint;
private int mBgArcColor;
private float mBgArcWidth;
//绘制圆弧
private Paint mArcPaint;
private float mArcWidth;
private float mStartAngle, mSweepAngle;
private RectF mRectF;
private int[] mGradientColors = {Color.GREEN, Color.YELLOW, Color.RED};
//圆心坐标
private Point mCenterPoint;
private float mTextOffsetPercentInRadius;
//当前进度,[0.0f,1.0f]
private float mPercent;
//动画时间
private long mAnimTime;
//属性动画
private ValueAnimator mAnimator;
//绘制数值
private TextPaint mValuePaint;
private float mValue;
private float mMaxValue;
private float mValueOffset;
private int mPrecision;
private String mPrecisionFormat;
private int mValueColor;
private float mValueSize;
//是否开启抗锯齿
private boolean antiAlias;
private Context mContext;
public CircleBar(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context,attrs);
}
// 初始化对象
private void init(Context context,AttributeSet attributeSet){
mContext = context;
// 初始化属性动画
mAnimator = new ValueAnimator();
// 初始化圆的外接矩形
mRectF = new RectF();
// 初始化圆心坐标
mCenterPoint = new Point();
// 加载属性 如抗锯齿 默认字体的样式和值 进度条的值等
initAttrs(attributeSet);
// 加载画笔
initPaint();
setValue(mValue);
}
/**
* 连接 styleable中的属性
* @param attributeSet 属性
*/
private void initAttrs(AttributeSet attributeSet){
TypedArray typedArray = mContext.obtainStyledAttributes(attributeSet, R.styleable.CircleProgressBar);
// 设置是否开启抗锯齿
antiAlias = typedArray.getBoolean(R.styleable.CircleProgressBar_antiAlias, ANTI_ALIAS);
// 无数据时的默认字体样式与内容
mHint = typedArray.getString(R.styleable.CircleProgressBar_hint);
mHintColor = typedArray.getColor(R.styleable.CircleProgressBar_hintColor, Color.BLACK);
mHintSize = typedArray.getDimension(R.styleable.CircleProgressBar_hintSize,DEFAULT_HINT_SIZE);
// 进度条当前值 与 最大值
mValue = typedArray.getFloat(R.styleable.CircleProgressBar_value, DEFAULT_VALUE);
mMaxValue = typedArray.getFloat(R.styleable.CircleProgressBar_maxValue,DEFAULT_MAX_VALUE);
//内容数值精度,格式,颜色和大小
mPrecision = typedArray.getInt(R.styleable.CircleProgressBar_precision, 0);
mPrecisionFormat = MeasureUtil.getPrecisionFormat(mPrecision);
mValueColor = typedArray.getColor(R.styleable.CircleProgressBar_valueColor, Color.BLACK);
mValueSize = typedArray.getDimension(R.styleable.CircleProgressBar_valueSize, DEFAULT_VALUE_SIZE);
// 单位 值 颜色 与大小
mUnit = typedArray.getString(R.styleable.CircleProgressBar_unit);
mUnitColor = typedArray.getColor(R.styleable.CircleProgressBar_unitColor, Color.BLACK);
mUnitSize = typedArray.getDimension(R.styleable.CircleProgressBar_unitSize, DEFAULT_UNIT_SIZE);
// 圆弧的宽度 角度 和 渐变色的角度 后面的是默认值
mArcWidth = typedArray.getDimension(R.styleable.CircleProgressBar_arcWidth, DEFAULT_ARC_WIDTH);
mStartAngle = typedArray.getFloat(R.styleable.CircleProgressBar_startAngle, DEFAULT_START_ANGLE);
mSweepAngle = typedArray.getFloat(R.styleable.CircleProgressBar_sweepAngle, DEFAULT_SWEEP_ANGLE);
// 背景颜色 背景宽度
mBgArcColor = typedArray.getColor(R.styleable.CircleProgressBar_bgArcColor, Color.WHITE);
mBgArcWidth = typedArray.getDimension(R.styleable.CircleProgressBar_bgArcWidth, DEFAULT_ARC_WIDTH);
mTextOffsetPercentInRadius = typedArray.getFloat(R.styleable.CircleProgressBar_textOffsetPercentInRadius, 0.33f);
//mPercent = typedArray.getFloat(R.styleable.CircleProgressBar_percent, 0);
mAnimTime = typedArray.getInt(R.styleable.CircleProgressBar_animTime, DEFAULT_ANIM_TIME);
// 渐变色
int gradientArcColors = typedArray.getResourceId(R.styleable.CircleProgressBar_arcColors, 0);
// 设置渐变色
if (gradientArcColors != 0) {
try {
int[] gradientColors = getResources().getIntArray(gradientArcColors);
if (gradientColors.length == 0) {//如果渐变色为数组为0,则尝试以单色读取色值
int color = getResources().getColor(gradientArcColors);
mGradientColors = new int[2];
mGradientColors[0] = color;
mGradientColors[1] = color;
} else if (gradientColors.length == 1) {//如果渐变数组只有一种颜色,默认设为两种相同颜色
mGradientColors = new int[2];
mGradientColors[0] = gradientColors[0];
mGradientColors[1] = gradientColors[0];
} else { // 否则 颜色数组 就是 渐变数组
mGradientColors = gradientColors;
}
} catch (Resources.NotFoundException e) {
throw new Resources.NotFoundException("the give resource not found.");
}
}
// 记得写这个方法
typedArray.recycle();
}
/**
* 加载画笔
*/
private void initPaint() {
// 创建默认文字画笔
mHintPaint = new TextPaint();
// 设置抗锯齿,会消耗较大资源,绘制图形速度会变慢 但是边界更圆润
mHintPaint.setAntiAlias(antiAlias);
// 设置绘制文字大小
mHintPaint.setTextSize(mHintSize);
// 设置画笔颜色
mHintPaint.setColor(mHintColor);
// 从中间向两边绘制,不需要再次计算文字 注意这里是相对于原点的绘制方向 若是Left则 是从左往右绘制 在原点右侧
mHintPaint.setTextAlign(Paint.Align.CENTER);
// 创建值的文字画笔
mValuePaint = new TextPaint();
// 设置抗锯齿,会消耗较大资源,绘制图形速度会变慢 但是边界更圆润
mValuePaint.setAntiAlias(antiAlias);
mValuePaint.setTextSize(mValueSize);
mValuePaint.setColor(mValueColor);
// 设置Typeface对象,即字体风格,包括粗体,斜体以及衬线体,非衬线体等
mValuePaint.setTypeface(Typeface.DEFAULT_BOLD);
mValuePaint.setTextAlign(Paint.Align.CENTER);
// 创建 单位 (即“步”) 的画笔
mUnitPaint = new TextPaint();
mUnitPaint.setAntiAlias(antiAlias);
mUnitPaint.setTextSize(mUnitSize);
mUnitPaint.setColor(mUnitColor);
mUnitPaint.setTextAlign(Paint.Align.CENTER);
// 创建 角度绘制画笔
mArcPaint = new Paint();
mArcPaint.setAntiAlias(antiAlias);
// 设置画笔的样式,为FILL 填充内部 ,FILL_AND_STROKE,或STROKE 描边
mArcPaint.setStyle(Paint.Style.STROKE);
// 设置画笔粗细
mArcPaint.setStrokeWidth(mArcWidth);
// 当画笔样式为STROKE或FILL_AND_STROKE时,设置油漆的填充样式,如圆形样式Cap.ROUND,方形样式Cap.SQUARE
// 也可以理解为 结尾处的样式 圆角ROUND 方形SQUARE 或者 无样式BUTT
mArcPaint.setStrokeCap(Paint.Cap.ROUND);
// 背景画笔 即当没有值时候的默认圆弧背景颜色
mBgArcPaint = new Paint();
mBgArcPaint.setAntiAlias(antiAlias);
mBgArcPaint.setColor(mBgArcColor);
mBgArcPaint.setStyle(Paint.Style.STROKE);
mBgArcPaint.setStrokeWidth(mBgArcWidth);
mBgArcPaint.setStrokeCap(Paint.Cap.ROUND);
}
/**
* View绘制中的测量方法
* @param widthMeasureSpec 宽度规格
* @param heightMeasureSpec 高度规格
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 记得要调用setMeasuredDimension,来存储这个View经过测量得到的measured width and height
// 这里将 根据测量规格设置宽度/高度 的方法 封装了起来
setMeasuredDimension(MeasureUtil.measure(widthMeasureSpec, mDefaultSize),
MeasureUtil.measure(heightMeasureSpec, mDefaultSize));
}
/**
* 在控件大小发生改变时调用 初始化时会调用一次 该方法在layout中的setFrame中
* @param w 新宽
* @param h 新高
* @param oldw 改变之前的宽
* @param oldh 改变之前的高
* 这里用于 设置圆的半径和 文字的baseLine
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
Log.d(TAG, "onSizeChanged: w = " + w + "; h = " + h + "; oldw = " + oldw + "; oldh = " + oldh);
//求圆弧和背景圆弧的最大宽度
float maxArcWidth = Math.max(mArcWidth, mBgArcWidth);
//求控件宽高的最小值作为实际值 并且减去圆弧的宽度,否则会造成部分圆弧绘制在外围
int minSize = Math.min(w - getPaddingLeft() - getPaddingRight() - 2 * (int) maxArcWidth,
h - getPaddingTop() - getPaddingBottom() - 2 * (int) maxArcWidth);
// 用最小值作为圆的半径
float mRadius = minSize / 2;
//获取圆的横纵坐标
mCenterPoint.x = w / 2;
mCenterPoint.y = h / 2;
//绘制圆弧的矩形边界 即 圆弧边宽的中心
mRectF.left = mCenterPoint.x - mRadius - maxArcWidth / 2;
mRectF.top = mCenterPoint.y - mRadius - maxArcWidth / 2;
mRectF.right = mCenterPoint.x + mRadius + maxArcWidth / 2;
mRectF.bottom = mCenterPoint.y + mRadius + maxArcWidth / 2;
//计算文字绘制时的 baseline
//由于文字的baseline、descent、ascent等属性只与textSize和typeface有关,所以此时可以直接计算
//若value、hint、unit由同一个画笔绘制或者需要动态设置文字的大小,则需要在每次更新后再次计算
mValueOffset = mCenterPoint.y - (mValuePaint.descent() + mValuePaint.ascent()) / 2; // ascent 从baseline线到最高的字母顶点到距离,负值
mHintOffset = mCenterPoint.y * 2 / 3 - (mHintPaint.descent() + mHintPaint.ascent()) / 2; // descent 从baseline线到字母最低点到距离
mUnitOffset = mCenterPoint.y * 4 / 3 - (mUnitPaint.descent() + mUnitPaint.ascent()) / 2;
//由于设置渐变需要每次都创建一个新的 SweepGradient 对象,所以最好不要放到 onDraw 方法中去更新,最好在初始化的时候就设置好,避免频繁创建导致内存抖动
updateArcPaint();
Log.d(TAG, "onSizeChanged: 控件大小 = " + "(" + w + ", " + h + ")"
+ "圆心坐标 = " + mCenterPoint.toString()
+ ";圆半径 = " + mRadius
+ ";圆的外接矩形 = " + mRectF.toString());
}
private float getBaselineOffsetFromY(Paint paint) {
return MeasureUtil.measureTextHeight(paint) / 2;
}
/**
* 绘制文字 和 圆角
* @param canvas 画布
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawTheText(canvas);
drawTheArc(canvas);
}
private void drawTheArc(Canvas canvas) {
// 绘制背景圆弧
// 从进度圆弧结束的地方开始重新绘制,优化性能
canvas.save();
float currentAngle = mSweepAngle * mPercent;
//为了方便计算,绘制圆弧的时候使用了 Canvas 的 rotate() 方法,对坐标系进行了旋转
canvas.rotate(mStartAngle, mCenterPoint.x, mCenterPoint.y);
// +2 是因为绘制的时候出现了圆弧起点有尾巴的问题 drawArc的布尔值为是否填充 一般画圆弧都是false不填充
canvas.drawArc(mRectF, currentAngle, mSweepAngle - currentAngle + 2, false, mBgArcPaint);
canvas.drawArc(mRectF, 2, currentAngle, false, mArcPaint);
canvas.restore();
}
private void drawTheText(Canvas canvas) {
canvas.drawText(String.format(mPrecisionFormat, mValue), mCenterPoint.x, mValueOffset, mValuePaint);
if (mHint != null) {
canvas.drawText(mHint.toString(), mCenterPoint.x, mHintOffset, mHintPaint);
}
if (mUnit != null) {
canvas.drawText(mUnit.toString(), mCenterPoint.x, mUnitOffset, mUnitPaint);
}
}
/**
* 更新圆弧画笔 设置渐变
*/
private void updateArcPaint() {
// 设置渐变
/*
* @param cx 渲染中心点x坐标
* @param cy 渲染中心点y坐标
* @param colors 围绕中心渲染的颜色数组,至少要有两种颜色值
* @param positions 相对位置的颜色数组,可为null, 若为null,可为null,颜色沿渐变线均匀分布。一般不需要设置该参数
*/
//渐变的颜色是360度,如果只显示270,那么则会缺失部分颜色
SweepGradient mSweepGradient = new SweepGradient(mCenterPoint.x, mCenterPoint.y, mGradientColors, null);
mArcPaint.setShader(mSweepGradient);
}
/**
* 设置属性动画 对进度属性设置动画
* @param start 开始百分比 而每次结束了 又把此时的百分比设置为 开始百分比 (从这里开始再变化)
* @param end 结束百分比
* @param animTime 动画的时间
*/
private void startAnimator(float start, float end, long animTime) {
mAnimator = ValueAnimator.ofFloat(start, end);
mAnimator.setDuration(animTime);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mPercent = (float) animation.getAnimatedValue();
mValue = mPercent * mMaxValue;
if (BuildConfig.DEBUG) {
Log.d(TAG, "onAnimationUpdate: percent = " + mPercent
+ ";currentAngle = " + (mSweepAngle * mPercent)
+ ";value = " + mValue);
}
invalidate(); // 去调用onDraw 并且当视图大小未变化不会执行layout
}
});
mAnimator.start();
}
public float getValue() {
return mValue;
}
public float getMaxValue() {
return mMaxValue;
}
/**
* 暴露方法给外部设置最大值
*
* @param maxValue 最大值
*/
public void setMaxValue(float maxValue) {
mMaxValue = maxValue;
}
public int getPrecision() {
return mPrecision;
}
/**
* 暴露方法给外部设置精度
*
* @return 精度
*/
public void setPrecision(int precision) {
mPrecision = precision;
mPrecisionFormat = MeasureUtil.getPrecisionFormat(precision);
}
/**
* 设置当前值
*
* @param value 当前值
*/
public void setValue(float value) {
if (value > mMaxValue) {
value = mMaxValue;
}
float start = mPercent;
float end = value / mMaxValue;
startAnimator(start, end, mAnimTime);
}
public int[] getGradientColors() {
return mGradientColors;
}
/**
* 暴露方法给外部设置渐变
*
* @param gradientColors 渐变颜色数组
*/
public void setGradientColors(int[] gradientColors) {
mGradientColors = gradientColors;
updateArcPaint();
}
public long getAnimTime() {
return mAnimTime;
}
/**
* 暴露方法给外部设置动画时间
* @param animTime 动画时间
*/
public void setAnimTime(long animTime) {
mAnimTime = animTime;
}
/**
* 暴露方法给外部 让百分比重置
*/
public void reset() {
startAnimator(mPercent, 0.0f, 1000L);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
//释放资源
}
}
四、xml 使用 自定义的 View
名称就是 包名+java的类名 然后设置在Styleable中定义的各种参数
自定义的View通过暴露方法 reset 和 setValue 让 主界面去 设置或者重置 进度条的值
<cn.sihao.customercirclebar.CircleBar
android:id="@+id/circle_progress_bar1"
android:layout_width="268dp"
android:layout_height="268dp"
android:layout_gravity="center_horizontal"
android:layout_centerHorizontal="true"
android:layout_marginTop="5dp"
android:layout_below="@+id/btn_reset_all"
app:antiAlias="true"
app:arcWidth="@dimen/small"
app:bgArcColor="@color/colorAccent"
app:bgArcWidth="@dimen/small"
app:hint="今天运动步数:"
app:hintSize="15sp"
app:maxValue="10000"
app:startAngle="135"
app:sweepAngle="270"
app:unit="步"
app:unitSize="15sp"
app:value="10000"
app:valueSize="25sp" />
注意:
1 调用 onMeasure 必须要调用setMeasuredDimension
2 那个圆的问题 是边宽的中间开始作外接矩形的
3 坐标是从左上到右下的
4 文字绘制 baseLine 相当于是文字的x轴
上为负 下为正
5 drawArc的 startAngle 是3点钟方向为起点
6 一定要记得 最后要 DetachedFromWindow 取消视图的订阅
7 CircleProgressBar_antiAlias 即Sytleable名为 CircleProgressBar 的 attr 名为 antiAlias
8 要及时的取消draw 避免内存泄漏
参考:https://github.com/MyLifeMyTravel/CircleProgress/blob/master/circleprogress/src/main/java/com/littlejie/circleprogress/CircleProgress.java
View常见Error:
一、 Caused by: android.view.InflateException: Binary XML file line #17: Binary XML file line #17: Error inflating class com.xx.xx.webview.CustomWebView
即17行这个引用错误
1 可能是这个自定义的View路径或文件名错误 可能是setContentView之前就使用了这个View
2 可能是构造方法的参数错了
3 可能是自定义view的代码错了 比如属性的类型错误 比如用了一些错误的代码(可以看更下面的堆栈)
#0x3040002 一般指的是系统的View 自己的一般是#07开头的
二、 获取view的宽高为0 view.getWidth返回为0
原因: view还没绘制完成 获取的结果为0
解决: 在 override fun onWindowFocusChanged(hasFocus: Boolean) {
这里进行获取