Android自定义控件_垂直滚动器PickerView

转载请注明出处:http://blog.csdn.net/ljmingcom304/article/details/50393098
本文出自:【梁敬明的博客】

1.效果展示

  前段时间公司的产品设计了一款三级联动的垂直滚动器PickerView,当时觉得挺简单的,实际开发中还是遇到了点小障碍,于是上网上搜了段代码用,代码是别人写的,直接Copy过来始终也是别人的东西,只有学到手才真正的属于自己,于是稍微研究了下,并通过自己的方式实现。下面先上张效果图,实现了其中一组数据的滚动选择,其他两组联动数据的实现原理是一样的,就是麻烦了点。
  

Android自定义控件_垂直滚动器PickerView_第1张图片

2.实现过程

  初始化画笔和存放数据的集合。

private void init() {
    mDataList = new ArrayList();
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setStyle(Style.FILL);
    mPaint.setAntiAlias(true);
    mPaint.setTextAlign(Align.CENTER);
    mPaint.setColor(Color.BLACK);
}
   在效果图中看以看到,当字体位于View中心时尺寸最大,位于两侧时尺寸最小。因此,需要设置字体的最大尺寸、最小尺寸以及字体间的距离。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    mViewHeight = getMeasuredHeight();
    mViewWidth = getMeasuredWidth();
    // 按照View的高度计算字体大小
    mMaxTextSize = mViewHeight / 6.0f;
    mMinTextSize = mMaxTextSize / 1.5f;
    mDistance = MARGIN_ALPHA * mViewHeight;
}
   初始化默认绘制,首先将当前选中的元素绘制到View的中心位置,索引在当前元素前的绘制到当前元素的上方,索引在当前元素后的绘制到当前元素的下方。
/**
 * @param position:当前元素前后第几个位置,若为当前元素position为0
 * @param direction:绘制的元素相对于当前元素的方向,上方为-1,下方为1,当前元素默认为下方
 */
private void drawText(Canvas canvas, int position, int direction) {
    // 元素距离控件中心的相对距离
    float offset = (float) (mDistance * position + direction * mMoveLen);

    // 缩放倍数:位于控件中心时,缩放倍数是1;控件中心的前一个数据和后一个数据的缩放倍数是0
    float f = (float) (1 - Math.pow(offset / mDistance, 2));// 按抛物线缩放
    // 当数据与控件中心的距离,大于数据间的初始距离时,不再进行缩放
    float scale = f < 0 ? 0 : f;

    // 字体尺寸
    float size = (mMaxTextSize - mMinTextSize) * scale + mMinTextSize;

    // 字体大小与透明度
    mPaint.setTextSize(size);
    mPaint.setAlpha((int) ((mMaxTextAlpha - mMinTextAlpha) * scale + mMinTextAlpha));

    float x = (float) (mViewWidth / 2.0);
    float y = getTextBaseLine((float) (mViewHeight / 2.0 + direction
            * offset));

    canvas.drawText(mDataList.get(mCurrentSelected + direction * position),
            x, y, mPaint);
}
  处理View的触摸滑动事件,包括触摸按下事件、触摸滑动事件和触摸离开事件。通过效果图可以知道,在字体没有滑动到初始化位置时手指离开,字体会自动滑动到初始化的位置,这里通过定时器的方式实现字体的自动滑动。   当手指按下时,首先要初始化定时任务,将上次的定时任务清除,并记录下手指按下的位置。
private void doDown(MotionEvent event) {
    if(mHandler!=null){
        mHandler.removeCallbacks(mRunnable);
    }
    mLastDownY = event.getY();
}
   当手指进行滑动时,获取每次滑动的距离,当滑动的距离超过字体间相对距离的一半时,调整字体在集合中的位置。向上滑动时,第一个元素移动到最后位置,其余元素整体前移,将每个元素的滑动距离整体向下移动一个相对位置。向上滑动时,最后一个元素移动到集合的首位,其余元素整体后移,将每个元素的滑动距离整体向上移动一个相对位置。
private void doMove(MotionEvent event) {

    mMoveLen += (event.getY() - mLastDownY);

    // 当向下滑动时,mMoveLen为正;当向上滑动时,mMoveLen为负
    if (mMoveLen > mDistance / 2) {
        // 往下滑超过离开距离
        String tail = mDataList.get(mDataList.size() - 1);
        mDataList.remove(mDataList.size() - 1);
        mDataList.add(0, tail);

        // 重新设置数据的位置,将其整体上移
        mMoveLen -= mDistance;
    } else if (mMoveLen < -mDistance / 2) {
        // 往上滑超过离开距离
        String head = mDataList.get(0);
        mDataList.remove(0);
        mDataList.add(head);

        // 重新设置数据的位置,将其整体下移
        mMoveLen += mDistance;
    }

    mLastDownY = event.getY();
    invalidate();
}

  当手指离开时,开启定时任务,保证字体的初始化相对View的位置不发生改变。

private void doUp(MotionEvent event) {
    // 抬起手后mCurrentSelected的位置由当前位置move到中间选中位置
    if (Math.abs(mMoveLen) < 0.0001) {
        mMoveLen = 0;
        return;
    }

    if(mHandler!=null){
        mHandler.removeCallbacks(mRunnable);
    }

    mHandler.postDelayed(mRunnable, 10);
}

  开启定时任务,判断当前字体已经滑动的距离,每隔指定时间自动滑动指定的距离,直到字体滑动到初始化相对View的位置。

private Runnable mRunnable = new Runnable() {

    @Override
    public void run() {
        if (Math.abs(mMoveLen) < SPEED) {
            mMoveLen = 0;
            if (mHandler != null) {
                mHandler.removeCallbacks(this);
                // 事件监听
                if (mSelectListener != null)
                    mSelectListener.onSelect(mDataList
                            .get(mCurrentSelected));
            }
        } else{
            // 这里mMoveLen / Math.abs(mMoveLen)是为了保有mMoveLen的正负号,以实现上滚或下滚
            // 用于将数据回滚到起始位置或者终点位置
            mMoveLen = mMoveLen - mMoveLen / Math.abs(mMoveLen) * SPEED;
            mHandler.postDelayed(this, 10);
        }
        invalidate();
    }
};

3.示例代码

  下面上完整代码,首先是布局文件。



    

        

    

  然后是Activity。

public class MainActivity extends Activity {

    PickerView pv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        pv = (PickerView) findViewById(R.id.minute_pv);
        List data = new ArrayList();
        for (int i = 0; i < 10; i++) {
            data.add("0" + i);
        }
        pv.setData(data);
        pv.setOnSelectListener(new onSelectListener() {

            @Override
            public void onSelect(String text) {
                Toast.makeText(MainActivity.this, "选择" + text + " 分",
                        Toast.LENGTH_SHORT).show();
            }
        });
    }

}

  最后是实现代码。

public class PickerView extends View {

/**
 * text之间间距和minTextSize之比,手工进行配置
 */
public static final float MARGIN_ALPHA = 0.35f;
/**
 * 自动回滚到中间的速度
 */
public static final float SPEED = 2;

private List<String> mDataList;
/**
 * 选中的位置,这个位置是mDataList的中心位置,一直不变
 */
private int mCurrentSelected;
private Paint mPaint;

private float mMaxTextSize;
private float mMinTextSize;

private float mMaxTextAlpha = 255;
private float mMinTextAlpha = 120;

// 要绘制的数据位于当前数据的上方或下方
private int mUp = -1;
private int mDown = 1;
/** 相邻数据间的距离 */
private float mDistance;

private int mViewHeight;
private int mViewWidth;

private float mLastDownY;
/** 滑动的距离 */
private float mMoveLen = 0;
private Handler mHandler = new Handler();
private onSelectListener mSelectListener;

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

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

public PickerView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    init();
}

interface onSelectListener {
    void onSelect(String text);
}

public void setOnSelectListener(onSelectListener listener) {
    mSelectListener = listener;
}

/** 添加数据 */
public void setData(List<String> datas) {
    mDataList = datas;
    mCurrentSelected = datas.size() / 2;
    invalidate();
}

public void setSelected(int selected) {
    mCurrentSelected = selected;
}

private void init() {
    mDataList = new ArrayList<String>();

    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setStyle(Style.FILL);
    mPaint.setAntiAlias(true);
    mPaint.setTextAlign(Align.CENTER);
    mPaint.setColor(Color.BLACK);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    mViewHeight = getMeasuredHeight();
    mViewWidth = getMeasuredWidth();
    // 按照View的高度计算字体大小
    mMaxTextSize = mViewHeight / 6.0f;
    mMinTextSize = mMaxTextSize / 1.5f;
    mDistance = MARGIN_ALPHA * mViewHeight;
}

@Override
protected void onDraw(Canvas canvas) {
    // 绘制当前元素
    drawText(canvas, 0, mDown);
    // 绘制上方元素
    for (int i = 1; (mCurrentSelected - i) >= 0; i++) {
        drawText(canvas, i, mUp);
    }
    // 绘制下方元素
    for (int i = 1; (mCurrentSelected + i) < mDataList.size(); i++) {
        drawText(canvas, i, mDown);
    }
}

/**
 * @param position:当前元素前后第几个位置,若为当前元素position为0
 * @param direction:绘制的元素相对于当前元素的方向,上方为-1,下方为1,当前元素默认为下方
 */
private void drawText(Canvas canvas, int position, int direction) {
    // 元素距离控件中心的相对距离
    float offset = (float) (mDistance * position + direction * mMoveLen);

    // 缩放倍数:位于控件中心时,缩放倍数是1;控件中心的前一个数据和后一个数据的缩放倍数是0
    float f = (float) (1 - Math.pow(offset / mDistance, 2));// 按抛物线缩放
    // 当数据与控件中心的距离,大于数据间的初始距离时,不再进行缩放
    float scale = f < 0 ? 0 : f;

    // 字体尺寸
    float size = (mMaxTextSize - mMinTextSize) * scale + mMinTextSize;

    // 字体大小与透明度
    mPaint.setTextSize(size);
    mPaint.setAlpha((int) ((mMaxTextAlpha - mMinTextAlpha) * scale + mMinTextAlpha));

    float x = (float) (mViewWidth / 2.0);
    float y = getTextBaseLine((float) (mViewHeight / 2.0 + direction
            * offset));

    canvas.drawText(mDataList.get(mCurrentSelected + direction * position),
            x, y, mPaint);
}

// 调整字体的位置,使其垂直居中进行绘制
private float getTextBaseLine(float p) {
    FontMetricsInt fmi = mPaint.getFontMetricsInt();
    float baseline = (float) (p - (fmi.bottom / 2.0 + fmi.top / 2.0));
    return baseline;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getActionMasked()) {
    case MotionEvent.ACTION_DOWN:
        doDown(event);
        break;
    case MotionEvent.ACTION_MOVE:
        doMove(event);
        break;
    case MotionEvent.ACTION_UP:
        doUp(event);
        break;
    }
    return true;
}

private void doDown(MotionEvent event) {
    if(mHandler!=null){
        mHandler.removeCallbacks(mRunnable);
    }

    mLastDownY = event.getY();
}

private void doMove(MotionEvent event) {

    mMoveLen += (event.getY() - mLastDownY);

    // 当向下滑动时,mMoveLen为正;当向上滑动时,mMoveLen为负
    if (mMoveLen > mDistance / 2) {
        // 往下滑超过离开距离
        String tail = mDataList.get(mDataList.size() - 1);
        mDataList.remove(mDataList.size() - 1);
        mDataList.add(0, tail);

        // 重新设置数据的位置,将其整体上移
        mMoveLen -= mDistance;
    } else if (mMoveLen < -mDistance / 2) {
        // 往上滑超过离开距离
        String head = mDataList.get(0);
        mDataList.remove(0);
        mDataList.add(head);

        // 重新设置数据的位置,将其整体下移
        mMoveLen += mDistance;
    }

    mLastDownY = event.getY();
    invalidate();
}

private void doUp(MotionEvent event) {
    // 抬起手后mCurrentSelected的位置由当前位置move到中间选中位置
    if (Math.abs(mMoveLen) < 0.0001) {
        mMoveLen = 0;
        return;
    }

    if(mHandler!=null){
        mHandler.removeCallbacks(mRunnable);
    }

    mHandler.postDelayed(mRunnable, 10);
}

private Runnable mRunnable = new Runnable() {

    @Override
    public void run() {
        if (Math.abs(mMoveLen) < SPEED) {
            mMoveLen = 0;
            if (mHandler != null) {
                mHandler.removeCallbacks(this);
                // 事件监听
                if (mSelectListener != null)
                    mSelectListener.onSelect(mDataList
                            .get(mCurrentSelected));
            }
        } else{
            // 这里mMoveLen / Math.abs(mMoveLen)是为了保有mMoveLen的正负号,以实现上滚或下滚
            // 用于将数据回滚到起始位置或者终点位置
            mMoveLen = mMoveLen - mMoveLen / Math.abs(mMoveLen) * SPEED;
            mHandler.postDelayed(this, 10);
        }
        invalidate();
    }
};

}

  

你可能感兴趣的:(Android开发)