在Android开发中,Indicator(指示器)是一个常见的UI元素,用于指示当前页面或内容的位置。它通常被用于轮播图、导航栏、引导页面等场景。Indicator通过视觉上的变化来表示当前所处的位置,为用户提供了导航和反馈的服务。
日常开发时,我们会经常遇到编写轮播图代码的需求,而且会对轮播图下面的小点点,也就是指示器进行各种各样的调整,位置上的,视觉上的等等,下面我们就来一探究竟。
首先,在目标module的build.gradle文件中添加com.youth.banner:banner库的依赖项。
dependencies {
implementation 'com.youth.banner:banner:2.1.0'
}
添加比较流行的youth banner库,可以比较快速构建我们的轮播图。
在我们的布局文件中,添加Banner组件。它是一个自定义的ViewPager,负责显示轮播图和管理Indicator。
<com.youth.banner.Banner
android:id="@+id/banner"
android:layout_width="match_parent"
android:layout_height="200dp" />
根据我们的需求设置适当的布局宽度和高度。
在代码中,准备轮播图数据。通常,它是一个包含图片URL或资源ID的列表。
val images = listOf(
"https://example.com/image1.jpg",
"https://example.com/image2.jpg",
"https://example.com/image3.jpg"
)
在代码中,找到Banner组件的实例,并设置轮播图数据、Indicator以及其他相关属性。
viewBinding.banner.adapter = object : BannerImageAdapter<String>(data.titleBanner) {
override fun onBindView(
holder: BannerImageHolder?,
data: String?,
position: Int,
size: Int
) {
holder?.let {
Glide.with(holder.itemView)
.load(data)
.apply(RequestOptions.bitmapTransform(RoundedCorners(20)))
.into(holder.imageView)
}
}
}
上面这段代码的作用是在轮播图中显示一组图片,通过Glide库加载图片并应用圆角效果,然后将其绑定到每个项的视图上。
适配器的作用是将数据源中的数据绑定到轮播图的每个项上。在这个例子中,适配器的泛型参数为String,表示数据源中的每个项是字符串类型。也就是说banner接收的数据就是我们上面定义的images列表,当然这个我们也可以自定义,可以是任意类型的,只要后面在glide图片加载那里写好逻辑就好了。(毕竟glide图片加载框架可以加载本地的图片,也可以加载网络的图片嘛)
下面开始设置一些参数:这里不配置指示器的话默认使用自带的指示器。
viewBinding.banner.apply {
// 添加生命周期管理,确保在适当的生命周期内开始和停止轮播
addBannerLifecycleObserver(owner)
setLoopTime(3000)
// 设置轮播图的点击事件监听器,点了哪张图执行自定义逻辑
setOnBannerListener { data, _ ->
when (data) {
"https://example.com/image1.jpg" -> {
}
"https://example.com/image2.jpg" -> {
}
"https://example.com/image3.jpg" -> {
}
}
}
}
com.youth.banner:banner库提供了一些默认的Indicator样式,例如圆点指示器。如果我们想要自定义Indicator的样式,可以使用setIndicator()方法来设置。可以设置圆点的大小、间距、颜色等属性,以满足我们的设计需求。
自定义MyCircleIndicator : 其实就是com.youth.banner:banner库下的CircleIndicator的kotlin版本,只不过我们可以在此基础上进行自定义,但前提是我们得先看懂youth作者写的代码
class MyCircleIndicator @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : BaseIndicator(context, attrs, defStyleAttr) {
private var mNormalRadius = 0
private var mSelectedRadius = 0
private var maxRadius = 0
init {
mNormalRadius = config.normalWidth / 2
mSelectedRadius = config.selectedWidth / 2
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val count = config.indicatorSize
if (count <= 1) {
return
}
mNormalRadius = config.normalWidth / 2
mSelectedRadius = config.selectedWidth / 2
//考虑当 选中和默认 的大小不一样的情况
maxRadius = Math.max(mSelectedRadius, mNormalRadius)
//间距*(总数-1)+选中宽度+默认宽度*(总数-1)
val width =
(count - 1) * config.indicatorSpace + config.selectedWidth + config.normalWidth * (count - 1)
setMeasuredDimension(width, Math.max(config.normalWidth, config.selectedWidth))
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val count = config.indicatorSize
if (count <= 1) {
return
}
var left = 0f
for (i in 0 until count) {
mPaint.apply {
color = if (config.currentPosition == i) config.selectedColor else config.normalColor
alpha = if (config.currentPosition == i) (0.2 * 256).toInt() else (0.5 * 256).toInt()
}
val indicatorWidth =
if (config.currentPosition == i) config.selectedWidth else config.normalWidth
val radius = if (config.currentPosition == i) mSelectedRadius else mNormalRadius
canvas.drawCircle(left + radius, maxRadius.toFloat(), radius.toFloat(), mPaint)
left += (indicatorWidth + config.indicatorSpace).toFloat()
}
}
}
在自定义 View 中重写 onMeasure() 方法的主要目的是测量视图的宽度和高度。在 MyCircleIndicator 类中,onMeasure() 方法用于计算并设置指示器视图的尺寸。
具体来说,onMeasure() 方法中的计算逻辑如下:
首先,获取指示器的数量 (config.indicatorSize)。如果数量小于等于1,表示没有需要绘制的指示器,直接返回,不进行后续计算。
然后,根据配置对象中的默认宽度和选中宽度计算出默认半径和选中半径 (mNormalRadius 和 mSelectedRadius)。这里假设宽度值是直径,通过除以 2 可以得到半径。
接下来,计算默认半径和选中半径中的最大值 (maxRadius),以便在绘制时可以确定绘制圆形的中心位置。
计算整个指示器视图的宽度 (width)。通过间距和宽度参数的组合,以及指示器数量来计算宽度。公式为:间距 * (总数-1) + 选中宽度 + 默认宽度 * (总数-1)。这样可以确保所有指示器都能适应在视图中显示。
最后,调用 setMeasuredDimension() 方法设置测量的视图宽度和高度。宽度为计算出的 width,高度为默认宽度和选中宽度中的最大值 (Math.max(config.normalWidth, config.selectedWidth))。
通过执行这些计算,onMeasure() 方法确保了指示器视图在布局过程中得到正确的测量尺寸,从而在绘制时能够正确地显示和布局指示器的位置。
onDraw 方法就不展开说了,无非就是用画笔画几个圆点点。canvas.drawCircle
布局好我们要放置的指示器的位置:
<com.youth.banner.Banner
android:id="@+id/banner"
android:layout_width="match_parent"
android:layout_height="100dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.example.view.MyCircleIndicator
android:id="@+id/indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginBottom="9dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="@id/banner" />
这里再来解释说明一下MyCircleIndicator的onMeasure方法:因为我们提前不知道banner具体有多少内容,也就不知道有多少个点,从而也无法判断MyCircleIndicator的具体的宽高。当我们设置宽度为wrap_content的时候就表示说,我们的view(MyCircleIndicator)根据子view(onDraw画的)来判定大小,也就是根据我们在MyCircleIndicator的onDraw方法里画的大小来判断,所以,如果我们不调用onMeasure对MyCircleIndicator进行测量,那么就好造成显示不全,布局错位等千奇百怪的问题。
因此通过重写 onMeasure() 方法,可以自定义指示器视图的测量逻辑,确保其宽度正确适应父视图的布局要求。
tips⚡:override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)
在 Android 中,widthMeasureSpec 和 heightMeasureSpec 是由系统传递给 onMeasure() 方法的参数,用于指定视图的宽度和高度的测量规格。
widthMeasureSpec 和 heightMeasureSpec 都是由测量规格和测量模式组成的整数值。
测量规格 (MeasureSpec):测量规格是一个 32 位整数,其中高 2 位表示测量模式,低 30 位表示测量的尺寸大小。
测量模式 (MeasureSpec Mode):测量模式定义了测量尺寸的规则。它有三种可能的值:
MeasureSpec.EXACTLY:精确测量模式,表示视图的尺寸已经明确指定,通常是固定的数值(android:layout_height=“100dp”)或 match_parent。
MeasureSpec.AT_MOST:最大值测量模式,表示视图的尺寸可以根据需要进行调整,但不能超过指定的最大值,通常是 wrap_content。
MeasureSpec.UNSPECIFIED:未指定测量模式,表示视图的尺寸可以任意大小,常用于特殊情况下的测量,不常用。
最后代码里设置一下参数:
这样MyCircleIndicator所继承的BaseIndicator才有config.normalWidth和config.selectedWidth的值,这些我们都可以自定义的。
viewBinding.banner.apply {
// 添加生命周期管理,确保在适当的生命周期内开始和停止轮播
addBannerLifecycleObserver(owner)
// 设置指示器,false 代表我们可以将指示器通过布局放在任何位置
// 这里的Indiacator就是我们自定义的指示器了
setIndicator(viewBinding.indicator, false)
setIndicatorWidth(10.dpToPx(), 10.dpToPx())
setIndicatorNormalColor(Color.parseColor("#FFFFFF"))
setIndicatorSelectedColor(Color.parseColor("#000000"))
setIndicatorSpace(10.dpToPx())
setLoopTime(3000)
// 设置轮播图的点击事件监听器,点了哪张图执行自定义逻辑
setOnBannerListener { data, _ ->
when (data) {
"https://example.com/image1.jpg" -> {
}
"https://example.com/image2.jpg" -> {
}
"https://example.com/image3.jpg" -> {
}
}
}
}
都自定义view了,想加的逻辑和样式,码上代码就ok啦。如果想自己写出banner控件,那还得好好沉淀呀。