Android自定义view——滚动选择器

滚动选择器主要用于在数据列表中快速选择其中一条数据,比如日期选择.首先给大伙看看最终的实现效果:




实现原理

  • 绘制
代码中定义了几个重要的变量:
private int mVisibleItemCount = 3; // 可见的item数量
private int mSelected; // 当前选中的item下标
private int mItemHeight = 0; // 每个条目的高度 = mMeasureHeight/mVisibleItemCount
private int mCenterY; // 中间item的起始坐标y = mCenterPosition*mItemHeight
private float mMoveLength = 0; // item移动长度,负数表示向上移动,正数表示向下移动

Android自定义view——滚动选择器_第1张图片

通过上图可发现,这几个变量决定了滚动选择器中每个条目的绘制位置

protected void onDraw(Canvas canvas) {
        // 中间item
        mPaint.setColor(mCenterItemBackground);
        float y = mCenterY;
        canvas.drawRect(0, y, getWidth(), y + mItemHeight, mPaint);
        // 传入位置信息,绘制item
        drawItem(canvas, mData, mSelected, 0, mMoveLength, mCenterY + mMoveLength);

        int length = Math.max(mCenterPosition, mVisibleItemCount - mCenterPosition);
        int positon;
        // 上下两边
        for (int i = 1; i <= length && i <= mData.size(); i++) {
            if (i <= mCenterPosition + 1) { // 上面的items,相对位置为 -i
                positon = mSelected - i < 0 ? mData.size() + mSelected - i
                        : mSelected - i;
                // 传入位置信息,绘制item
                if (mIsCirculation) {
                    drawItem(canvas, mData, positon, -i, mMoveLength, mCenterY + mMoveLength - i * mItemHeight);
                } else if (mSelected - i >= 0) { // 非循环滚动
                    drawItem(canvas, mData, positon, -i, mMoveLength, mCenterY + mMoveLength - i * mItemHeight);
                }
            }
            if (i <= mVisibleItemCount - mCenterPosition) { // 下面的items,相对位置为 i
                positon = mSelected + i >= mData.size() ? mSelected + i
                        - mData.size() : mSelected + i;
                // 传入位置信息,绘制item
                if (mIsCirculation) {
                    drawItem(canvas, mData, positon, i, mMoveLength, mCenterY + mMoveLength + i * mItemHeight);
                } else if (mSelected + i < mData.size()) { // 非循环滚动
                    drawItem(canvas, mData, positon, i, mMoveLength, mCenterY + mMoveLength + i * mItemHeight);
                }
            }
        }
}

为了便以拓展,drawItem()负责每个item的绘制.接下来只要改变上面几个重要变量的值就可以实现滚动的效果.

  • Scroller滚动类

系统自带的android.widget.Scroller 类封装了滚动相关的操作。可以使用Scroller获取可以产生滚动效果的数据;例如,在响应一个滑动手势时,Scroller会帮你计算
滚动偏移量,你可以根据获取的偏移量来设置你的view的位置,从而实现滚动效果。 主要方法有:

startScroll(int startX, int startY, int dx, int dy) //开始计算平滑滚动,dx、dy为滚动的距离,dx = finalX - startX;

fling(int startX, int startY, int velocityX, int velocityY,
            int minX, int maxX, int minY, int maxY) //根据滑动的速度,开始计算惯性滚动

computeScrollOffset()//计算当前时间滚动的偏移量,如果返回true,则滚动还未结束
getCurrY() //获取当前Y轴坐标的偏移量
getCurrX() //获取当前X轴坐标的偏移量

mScroller.abortAnimation() //停止滚动

在自定义View中常用的方法为:

// 开始计算平滑滚动
mScroller.startScroll(startX, startY, dx, dy); 

// 调用invalidate()方法刷新view时,在绘制(onDraw)之前会调用view.computeScroll()方法,
// 所以在此方法内计算偏移量
public void computeScroll() {
        if (mScroller.computeScrollOffset()) { // 正在滚动
    int curY = mScroller.getCurrY();
		int curX = mScroller.getCurrX();
		// do something
		invalidate(); // 刷新
	}else{ // 滚动结束
		// do something
	}
}

在滚动选择器中,当手指滑动时,把滑动的距离赋值给mMoveLength,当手指抬起时,则调用Scroller.startScroll()方法,将当前item移动到中间位置;如果是快速滑动后抬起手指,则调用Scroller.fling()方法惯性滑动一段距离,惯性结束后再调用Scroller.startScroll()方法,将当前item移动到中间位置.
public void computeScroll() {
        if (mScroller.computeScrollOffset()) { // 正在滚动
            // 可以把scroller看做模拟的触屏滑动操作,mLastScrollY为上次滑动的坐标
            mMoveLength = mMoveLength + mScroller.getCurrY() - mLastScrollY;
            mLastScrollY = mScroller.getCurrY();
            checkCirculation(); // 检测当前选中的item
            invalidate();
        } else { // 滚动完毕
            if (mIsFling) {
                mIsFling = false;
                moveToCenter(); // 滚动到中间位置
            } else if (mIsMovingCenter) { // 选择完成,回调给监听器
                mMoveLength = 0;
                mIsMovingCenter = false;
                mLastScrollY = 0;
                notifySelected();
            }
        }
    }

// 检测当前选择的item位置
    private void checkCirculation() {
        if (mMoveLength >= mItemHeight) { // 向下滑动
            // 该次滚动距离中越过的item数量
            int span = (int) (mMoveLength / mItemHeight);
            mSelected -= span;
            if (mSelected < 0) {  // 滚动顶部,判断是否循环滚动
                if (mIsCirculation) {
                    do {
                        mSelected = mData.size() + mSelected;
                    } while (mSelected < 0); // 当越过的item数量超过一圈时
                    mMoveLength = (mMoveLength - mItemHeight) % mItemHeight;
                } else { // 非循环滚动
                    mSelected = 0;
                    mMoveLength = mItemHeight;
                    if (mIsFling) { // 停止惯性滑动,根据computeScroll()中的逻辑,下一步将调用moveToCenter()
                        mScroller.forceFinished(true);
                    }
                    if (mIsMovingCenter) { //  移回中间位置
                        scroll(mMoveLength, 0);
                    }
                }
            } else {
                mMoveLength = (mMoveLength - mItemHeight) % mItemHeight;
            }

        } else if (mMoveLength <= -mItemHeight) { // 向上滑动
            // 该次滚动距离中越过的item数量
            int span = (int) (-mMoveLength / mItemHeight);
            mSelected += span;
            if (mSelected >= mData.size()) { // 滚动末尾,判断是否循环滚动
                if (mIsCirculation) {
                    do {
                        mSelected = mSelected - mData.size();
                    } while (mSelected >= mData.size()); // 当越过的item数量超过一圈时
                    mMoveLength = (mMoveLength + mItemHeight) % mItemHeight;
                } else { // 非循环滚动
                    mSelected = mData.size() - 1;
                    mMoveLength = -mItemHeight;
                    if (mIsFling) { // 停止惯性滑动,根据computeScroll()中的逻辑,下一步将调用moveToCenter()
                        mScroller.forceFinished(true);
                    }
                    if (mIsMovingCenter) { //  移回中间位置
                        scroll(mMoveLength, 0);
                    }
                }
            } else {
                mMoveLength = (mMoveLength + mItemHeight) % mItemHeight;
            }
        }
    }


类的定义
为了拓展方便,这里把滚动选择器定义为抽象类,文章最开始的效果图是用继承自ScrollPickerView的字符串滚动选择器实现.

public abstract class ScrollPickerView extends View{
/**
     * 绘制item
     * @param canvas
     * @param data        数据集
     * @param position   在data数据集中的位置
     * @param relative   相对中间item的位置,relative=position-getSelected()
     * @param moveLength 中间item滚动的距离,moveLength<0则表示向上滚动的距离,moveLength>0则表示向上滚动
     * @param top        当前绘制item的顶部坐标
     */
    public abstract void drawItem(Canvas canvas, List data, int position, int relative, float moveLength, float top);
}

文章开头的演示图中的控件为字符串选择器,主要继承了ScrollPickerView,并在drawItem方法中绘制字符串。
/**
 * 字符串滚动选择器
 */
public class StringScrollPicker extends ScrollPickerView;

如果要实现图片滚动选择器,只有继承ScrollPickerView,实现drawItem()方法即可.

完整的代码放在了github上:https://github.com/1993hzw/Androids

       

你可能感兴趣的:(android)