指示器(IndicatorView)
场景:Fragment的指示器,虽然有很多IndicatorView可以使用,但是自给自足还是好的。
国际惯例:效果
思路:
View里通过canvas.drawCircle(x坐标,y坐标,半径,Paint)来达到画圆的目的。
需要解决的问题是按照不同的数量,要计算出圆的具体坐标。
探索:
-
利用canvas的宽度等分,高度在中间。
一只圆的时候:
圆刚好在中间。
结论: x=View的宽度/2,y=View的高度/2
两只圆的时候:
可以得出:x1=View的宽度/3, y1=View的高度/2
x2=2*View的宽度/3,y2=View的高度/2
那么总体的坐标公式为:
canvas?.drawCircle(i * mWidth / (count + 1).toFloat(), mHeight / 2F, radius, mPaint)
实现:
- 定义需要的自定义Styleable属性:
<!-- IndicatorView-->
<!-- IndicatorView-->
<declare-styleable name="IndicatorView">
<!-- 总数-->
<attr name="indicatorCount" format="integer" />
<!-- 指示器位置-->
<attr name="indicatePosition" format="integer" />
<!-- 指示器半径-->
<attr name="indicatorRadius" format="float" />
<!-- 指示器主颜色-->
<attr name="indicateMainColor" format="color" />
<!-- 指示器副颜色-->
<attr name="indicateSecondColor" format="color" />
<!-- view背景-->
<attr name="android:background" format="color" />
</declare-styleable>
2.自定义View主结构:
必须重写的方法类
class IndicatorView : View {
//实例化时
constructor(mContext: Context) : super(mContext) {
initView(mContext, null)
}
//sdk 24以上时调用
constructor(mContext: Context, attributes: AttributeSet) : super(mContext, attributes) {
initView(mContext, attributes)
}
//如果有主题时
constructor(mContext: Context, attributes: AttributeSet, theme: Int) : super(
mContext,
attributes,
theme
) {
initView(mContext, attributes)
}
- 在InitView方法里解析sylable:
/**
* mainColor 主颜色
*/
private var mainColor by Delegates.notNull<Int>()
/**
* Second color 副颜色
*/
private var secondColor by Delegates.notNull<Int>()
/**
* Count 总数
*/
var count by Delegates.notNull<Int>()
/**
*position 当前位置
*/
var position: Int = 0
private lateinit var mPaint: Paint
/**
* Bg color 画布背景颜色
*/
private var bgColor by Delegates.notNull<Int>()
/**
* Radius 需要的圆的背景。
*/
private var radius by Delegates.notNull<Float>()
/**
* M width 所要的宽度
*/
private var mWidth by Delegates.notNull<Int>()
/**
* M height 所需高度
*/
private var mHeight by Delegates.notNull<Int>()
private fun initView(mContext: Context, attributes: AttributeSet?) {
初始化
mPaint = Paint()
val ta = mContext.obtainStyledAttributes(attributes, R.styleable.IndicatorView)
mainColor = ta.getColor(R.styleable.IndicatorView_indicateMainColor, Color.RED)
secondColor = ta.getColor(R.styleable.IndicatorView_indicateSecondColor, Color.GRAY)
position = ta.getInt(R.styleable.IndicatorView_indicatePosition, 1)
bgColor = ta.getColor(R.styleable.IndicatorView_android_background, Color.WHITE)
radius = ta.getFloat(R.styleable.IndicatorView_indicatorRadius, 10F)
count = ta.getInt(R.styleable.IndicatorView_indicatorCount, 3)
/* //判断总数不要0
if (count==0){
throw IllegalArgumentException("Can not set 0 to the Total")
}
//判断position不能大于总数
if (count < position){
throw IllegalArgumentException("Position must be greater than Count")
}*/
//回收
ta.recycle()
}
注意:初始化的时候必须走查逻辑,保证不能出现总数为0,或者当前位置超过总数的情况,也可以Throw Exception来抛出异常。
重写OnDraw方法,按照总数,指示器位置等信息画圆。
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
mPaint.isAntiAlias = true
//画背景先
canvas?.drawColor(bgColor)
//遍历时画图
//保证不能0
if (count != 0) {
for (i: Int in 1..count) {
//如果是当前位置,颜色要MainColor。
if (position == i) {
mPaint.color = mainColor
} else {
//如果普通位置,用副颜色。
mPaint.color = secondColor
}
//画圆,参数中高度要刚好在View中间。
canvas?.drawCircle(i * mWidth / (count + 1).toFloat(), mHeight / 2F, radius, mPaint)
}
}
canvas?.save()
}
需要计算mWidth,mHeight的值,在onMeasure的时候,重写。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//宽度的设定为:全部圆的直径+圆之间的空间距离。可以灵活设定。
mWidth = getMySize(30 * (count + 2), widthMeasureSpec)
//高度就20dp(圆的直径),
mHeight = getMySize(20, heightMeasureSpec)
//使用上面的值作为view的宽高。
setMeasuredDimension(mWidth, mHeight)
}
分别对应View的三个模式(UNSPASSIFIED, EXACTLY, ATMOST)进行不同的设计。
我的是:
private fun getMySize(size: Int, measureSpec: Int): Int {
var result = size
val specMode = MeasureSpec.getMode(measureSpec)
val specSize = MeasureSpec.getSize(measureSpec)
when (specMode) {
//未指定或 wrap_content的时候,使用计算出来的值。
MeasureSpec.UNSPECIFIED,
MeasureSpec.AT_MOST -> {
result = size
return result
}
//Match_parent的时候.
MeasureSpec.EXACTLY -> {
result = specSize
return result
}
}
return result
}
最后写一个public方法,让外部更新View的当前指示器位置(状态)
/**
* Update position
*
* @param mPosition 需要更新的位置
*/
fun updatePosition(mPosition: Int) {
//判断一下,免得超过了总数.
position = if (mPosition <= count) {
mPosition
} else {
1
}
//重绘
this.postInvalidate()
}
Github直通车 https://github.com/Neo-Turak/learning