系列文章目录
自定义view实现
文章目录
前言
在模仿荣耀手机上的天气App时,天气界面底部的指示器小圆点用原生TabLayout控件实现较为复杂,于是自己用自定义view的方式简单实现了一个。
提示:以下是本篇文章正文内容,下面案例可供参考
一、实现方案
1. 原理
确定ViewPager2包含fragment的总数量和fragment当前的位置,切换页面时重新绘制指示器样式。
2. 直接继承 view的方式,复杂度不高,只需要重写onMeasure方法和onDraw方法。
onMeasure方法中实现了根据控件宽高和指示器个数动态计算圆点的直径
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
mIndicatorWidth = MeasureSpec.getSize(widthMeasureSpec)
mIndicatorHeight = MeasureSpec.getSize(heightMeasureSpec)
// item宽度 = 指示器宽度 / (item个数 + 间隔个数)
mItemWidth = mIndicatorWidth.div(mIndicatorItemCount + mIndicatorItemCount - 1)
// item高度 = item宽度和指示器高度中的小值,避免绘制不全
mItemHeight = mItemWidth.coerceAtMost(mIndicatorHeight)
// 绘制item的起始位置 = 指示器宽度/2 - 绘制区域/2,保持绘制区域居中显示
mStartPos =
mIndicatorWidth.div(2f) - ((mIndicatorItemCount + mIndicatorItemCount - 1) * mItemHeight).div(
2f
)
// 不需要改变原控件大小,此处不需要重绘
// setMeasuredDimension(mIndicatorWidth, mIndicatorHeight)
}
这里绘制的是圆点指示器,其余形状的大家有兴趣可以自己实现。
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val dy = mIndicatorHeight.div(2f)
// 圆半径
val cr = mItemHeight.div(2f)
for (i in 0 until mIndicatorItemCount) {
// 指示器为圆形
mIndicatorItemDistance = mItemHeight
// 动态计算每个item的起始绘制位置
val dx = mStartPos + i * mItemHeight + i * mIndicatorItemDistance + cr
// item选中态在大小和颜色上有所不同
canvas.drawCircle(
dx,
dy,
if (i == mCurrentSelectedPosition) cr else cr.div(1.5f),
if (i == mCurrentSelectedPosition) mSelectedPaint else mUnSelectedPaint
)
}
}
3. 添加自定义属性attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="IndicatorView">
<!--指示器选中颜色-->
<attr name="colorSelected" format="color|reference"/>
<!--指示器未选中颜色-->
<attr name="colorUnSelected" format="color|reference"/>
</declare-styleable>
</resources>
二、使用步骤
1.在xml定义控件
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/weather_vp2"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/vp2_indicator"
app:layout_constraintTop_toBottomOf="@id/main_toolbar"
android:layout_marginBottom="12dp" />
<com.kkw.smallweather.view.IndicatorView
android:id="@+id/vp2_indicator"
android:layout_width="match_parent"
android:layout_height="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:colorUnSelected="#60efefef"
android:layout_marginBottom="12dp" />
2.在代码中调用
// 设置指示器个数
mBinding.vp2Indicator.setIndicatorItemCount(vp2Adapter.itemCount)
// 监听vp2界面变化
mBinding.weatherVp2.registerOnPageChangeCallback(object :
ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
// 显示在哪个页面就重绘对应的指示器
mBinding.vp2Indicator.setCurrentSelectedPosition(mBinding.weatherVp2.currentItem)
mBinding.vp2Indicator.postInvalidate()
}
})
三、附上完整代码
/**
* 自定义指示器圆点样式
* @author kkw
*/
class IndicatorView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
// 指示器容器 宽/高
private var mIndicatorWidth = 0
private var mIndicatorHeight = 0
// 指示器item 宽/高
private var mItemWidth = 0
private var mItemHeight = 0
// 指示器item的间隔
private var mIndicatorItemDistance = 0
// 指示器item的个数
private var mIndicatorItemCount = 0
// 首个item的起点
private var mStartPos = 0f
// item画笔 选中态/未选中态
private val mSelectedPaint: Paint = Paint()
private val mUnSelectedPaint: Paint = Paint()
// item画笔颜色 选中态/未选中态
private var mColorSelected = Color.WHITE
private var mColorUnSelected = Color.GRAY
// 当前选中的位置
private var mCurrentSelectedPosition = 0
// item是否为圆点
private var isCircle = true
init {
// 自定义属性
val a = context.obtainStyledAttributes(attrs, R.styleable.IndicatorView)
mColorSelected = a.getColor(R.styleable.IndicatorView_colorSelected, Color.WHITE)
mColorUnSelected = a.getColor(R.styleable.IndicatorView_colorUnSelected, Color.GRAY)
a.recycle()
// 配置paint画笔
mSelectedPaint.apply {
style = Paint.Style.FILL
isAntiAlias = true
color = mColorSelected
}
mUnSelectedPaint.apply {
style = Paint.Style.FILL
isAntiAlias = true
color = mColorUnSelected
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
mIndicatorWidth = MeasureSpec.getSize(widthMeasureSpec)
mIndicatorHeight = MeasureSpec.getSize(heightMeasureSpec)
// item宽度 = 指示器宽度 / (item个数 + 间隔个数)
mItemWidth = mIndicatorWidth.div(mIndicatorItemCount + mIndicatorItemCount - 1)
// item高度 = item宽度和指示器高度中的小值,避免绘制不全
mItemHeight = mItemWidth.coerceAtMost(mIndicatorHeight)
// 绘制item的起始位置 = 指示器宽度/2 - 绘制区域/2,保持绘制区域居中显示
mStartPos =
mIndicatorWidth.div(2f) - ((mIndicatorItemCount + mIndicatorItemCount - 1) * mItemHeight).div(
2f
)
// 不需要改变原控件大小,此处不需要重绘
// setMeasuredDimension(mIndicatorWidth, mIndicatorHeight)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val dy = mIndicatorHeight.div(2f)
// 圆半径
val cr = mItemHeight.div(2f)
for (i in 0 until mIndicatorItemCount) {
// 指示器为圆形
mIndicatorItemDistance = mItemHeight
// 动态计算每个item的起始绘制位置
val dx = mStartPos + i * mItemHeight + i * mIndicatorItemDistance + cr
// item选中态在大小和颜色上有所不同
canvas.drawCircle(
dx,
dy,
if (i == mCurrentSelectedPosition) cr else cr.div(1.5f),
if (i == mCurrentSelectedPosition) mSelectedPaint else mUnSelectedPaint
)
}
}
/**
* 控制指示器显示隐藏
*/
private fun indicatorVisibility() {
if (mCurrentSelectedPosition >= mIndicatorItemCount) {
mCurrentSelectedPosition = mIndicatorItemCount - 1
}
// 小于1个不显示
visibility = if (mIndicatorItemCount <= 1) GONE else VISIBLE
}
/**
* 设置指示器item个数
*/
fun setIndicatorItemCount(count: Int) {
mIndicatorItemCount = count
indicatorVisibility()
}
/**
* 设置当前位置
*/
fun setCurrentSelectedPosition(pos: Int) {
this.mCurrentSelectedPosition = pos
}
}
四、实现效果
上面是荣耀天气,下面是实现效果
总结
本文只是根据需求简单实现了一个圆形的方案,大家可自行扩展。
IndicatorView不止可搭配ViewPager2使用,水平切换的场景下都可以。