Android自定义Indicator指示器,可搭配ViewPager2使用

系列文章目录

自定义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使用,水平切换的场景下都可以。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用ViewPager实现轮播图的步骤如下: 1. 在布局文件中添加ViewPager控件和指示器(可选)。 ```xml <androidx.viewpager.widget.ViewPager android:id="@+id/view_pager" android:layout_width="match_parent" android:layout_height="200dp" /> <LinearLayout android:id="@+id/indicator_layout" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:orientation="horizontal" /> ``` 2. 创建一个PagerAdapter,设置数据源和页面布局。 ```java public class MyPagerAdapter extends PagerAdapter { private List<Integer> mData; private LayoutInflater mInflater; public MyPagerAdapter(Context context, List<Integer> data) { mData = data; mInflater = LayoutInflater.from(context); } @Override public int getCount() { return mData.size(); } @Override public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { return view == object; } @NonNull @Override public Object instantiateItem(@NonNull ViewGroup container, int position) { View view = mInflater.inflate(R.layout.item_pager, container, false); ImageView imageView = view.findViewById(R.id.image_view); imageView.setImageResource(mData.get(position)); container.addView(view); return view; } @Override public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { container.removeView((View) object); } } ``` 3. 绑定PagerAdapter和ViewPager,并设置ViewPager的滑动监听,以便更新指示器。 ```java ViewPager viewPager = findViewById(R.id.view_pager); MyPagerAdapter adapter = new MyPagerAdapter(this, mData); viewPager.setAdapter(adapter); LinearLayout indicatorLayout = findViewById(R.id.indicator_layout); for (int i = 0; i < mData.size(); i++) { View indicator = new View(this); int size = getResources().getDimensionPixelSize(R.dimen.indicator_size); LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(size, size); lp.leftMargin = i == 0 ? 0 : getResources().getDimensionPixelSize(R.dimen.indicator_margin); indicator.setLayoutParams(lp); indicator.setBackgroundResource(R.drawable.indicator_bg); indicatorLayout.addView(indicator); } viewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { @Override public void onPageSelected(int position) { super.onPageSelected(position); for (int i = 0; i < mData.size(); i++) { View indicator = indicatorLayout.getChildAt(i); indicator.setSelected(i == position); } } }); ``` 其中,item_pager布局文件中只包含一个ImageView控件,用于显示图片。indicator_bg是指示器的背景,可以自定义。 以上就是使用ViewPager实现轮播图的主要步骤。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值