返回

轮播“粘”指器——Android自定义View实战

Android

轮播在当下App设计中已俨然成为标配,与其相伴而生、代表轮播进度的轮播指示器也亟待灵动美观的设计。尽管市面上现有的指示器样式多采用小圆点形式,且相关实现教程也数不胜数,但仅凭这些还不能满足我们对个性化、功能丰富应用的需求。

本文将引入一个集美观与动感为一身的“粘性”轮播指示器,以期能为有兴趣的读者提供更多参考与启发。

想要自定义Android中的View,首要了解View的核心概念及其绘制流程。View即一种应用层抽象类,它将窗口中所要显示的图像呈现出来。欲了解具体流程,我们需剖析View源码中的底层机制:

  • 通过onMeasure()确定View的尺寸。
  • 利用onSizeChanged()设置View的实际尺寸。
  • 在onDraw()方法中绘制View。

有了理论认识后,不妨先来看看“粘性”指示器的布局样貌:

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v4.view.ViewPager
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="200dp" />

    <com.zys.stickyindicator.StickyIndicator
        android:id="@+id/sticky_indicator"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_below="@+id/viewPager" />

</RelativeLayout>

其中的StickyIndicator即为我们定义的自定义指示器。先别着急着手实现,不妨动动脑筋思考一下:要想黏合的效果得以实现,我们势必需要连续获取轮播图的变化,并在指示器中持续更新其状态,该如何实现?

其实很简单,我们只要监听ViewPager页面滑动时触发的OnPageChangeListener,根据回调方法传递的信息,更新自定义指示器的状态即可。

public class StickyIndicator extends View implements ViewPager.OnPageChangeListener {

    private Paint mPaint;
    private List<PositionData> mPositionDataList;
    private int mWidth;
    private int mHeight;
    private int mRadius;
    private int mSelectPosition;

    public StickyIndicator(Context context) {
        this(context, null);
    }

    public StickyIndicator(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public StickyIndicator(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setAntiAlias(true);
        mPositionDataList = new ArrayList<>();
        mRadius = 15;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mWidth = MeasureSpec.getSize(widthMeasureSpec);
        mHeight = MeasureSpec.getSize(heightMeasureSpec);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawCircle(canvas);
        drawCurrentCircle(canvas);
    }

    private void drawCircle(Canvas canvas) {
        for (int i = 0; i < mPositionDataList.size(); i++) {
            canvas.drawCircle(mPositionDataList.get(i).x, mPositionDataList.get(i).y, mRadius, mPaint);
        }
    }

    private void drawCurrentCircle(Canvas canvas) {
        if (mSelectPosition < mPositionDataList.size()) {
            mPaint.setColor(Color.BLUE);
            canvas.drawCircle(mPositionDataList.get(mSelectPosition).x, mPositionDataList.get(mSelectPosition).y, mRadius, mPaint);
        }
    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        float offset = 0;
        if (positionOffset != 0) {
            offset = (positionOffsetPixels - mWidth / mPositionDataList.size()) / positionOffset;
        }
        // 计算移动距离
        float moveDistance = (mPositionDataList.get(position + 1).x - mPositionDataList.get(position).x) * positionOffset + offset;
        mSelectPosition = position;
        invalidate();
    }

    @Override
    public void onPageSelected(int position) {

    }

    @Override
    public void onPageScrollStateChanged(int state) {

    }

    public void setViewPager(ViewPager viewPager) {
        viewPager.addOnPageChangeListener(this);
        initPositionData(viewPager);
        invalidate();
    }

    private void initPositionData(ViewPager viewPager) {
        mPositionDataList.clear();
        for (int i = 0; i < viewPager.getAdapter().getCount(); i++) {
            PositionData positionData = new PositionData();
            positionData.x = mWidth / viewPager.getAdapter().getCount() * i + mWidth / viewPager.getAdapter().getCount() / 2;
            positionData.y = mHeight / 2;
            mPositionDataList.add(positionData);
        }
    }

    private static class PositionData {
        int x;
        int y;
    }
}

如何使指示器的圆形能够黏合变化?看似困难的问题,实则只要利用回调方法不断地从ViewPager获取页面滑动状态,从而更新指示器的位置数据便可实现。下面几个步骤是实现的关键:

  • 通过OnPageChangeListener监听ViewPager的状态。
  • 滑动时,根据回调方法传递的信息计算要移动的距离。
  • 将计算出的距离应用于指示器的x坐标。
  • 更新指示器的x坐标,使之与ViewPager中的当前页面保持一致。

将指示器黏合到指定ViewPager的操作方法也很简单:

stickyIndicator.setViewPager(viewPager);

这样做能够将指示器与ViewPager关联起来,并确保指示器的位置能够根据ViewPager中的页面滑动而相应变化。

有了这些理论准备,现在就可以着手实现这个粘性轮播指示器了。快去试试吧!

轮播指示器是App中常见的一种控件,而“粘性”轮播指示器的实现并不复杂,掌握好本文的技巧,相信你一定能够做出美观又实用的效果。