登录 立即注册
安币:

安卓巴士 - 安卓开发 - Android开发 - 安卓 - 移动互联网门户

查看: 228|回复: 0

Android 滚轮选择器的实现详解

[复制链接]

357

主题

361

帖子

7286

安币

手工艺人

发表于 2019-4-18 11:05:01 | 显示全部楼层 |阅读模式
如果对本篇文章感兴趣,请前往,原文地址:http://www.apkbus.com/blog-822721-79902.html

## 简介最近用一个日期选择控件,感觉官方的DatePicker操作有点复杂,而且不同的Android版本样式也都不一样。后来发现小米日历的日期选择控件蛮好看的,于是自己尝试仿写一个,感觉效果还不错。效果图:![Picture](//upload-images.jianshu.io/upload_images/2782988-93ef07ed359c3677.gif)![Picture](//upload-images.jianshu.io/upload_images/2782988-3f583ea8075adac8.gif)## 功能分析- 滚轮:首先绘制一列文本,然后添加一个偏移量,在onDraw中根据手指滑动,改变偏移量并重新绘制这一列文本,这样就实现了滑动的效果。- Fling:这个应该很常见了,用```VelocityTracker```和```Scroller```来实现。- 循环滚动:当滚动超过数据集的大小后,从头继续获取数据即可。- 幕布效果:在中心区域绘制一个矩形。- 字体颜色渐变:- 从中心到两边,逐渐将Paint的透明度变小。- 从中心相邻项到中心,字体颜色渐变。- 中心选项文字变大: 从中心相邻项到中心,字体大小渐变。- 指示器文字,在中间的Item后边绘制一个文字。到这里,所有的功能点的思路大概就清晰了。## 实现方法### 测量控件大小这里主要是测量wrap_content模式的大小。首先,要确定单个item的文字的宽高。代码如下:```public void computeTextSize() {    mTextMaxWidth = mTextMaxHeight = 0;    if (mDataList.size() == 0) {            return;    }    //这里使用最大的,防止文字大小超过布局大小。    mPaint.setTextSize(mSelectedItemTextSize > mTextSize ? mSelectedItemTextSize : mTextSize);    if (!TextUtils.isEmpty(mItemMaximumWidthText)) {        mTextMaxWidth = (int) mPaint.measureText(mItemMaximumWidthText);    } else {        mTextMaxWidth = (int) mPaint.measureText(mDataList.get(0).toString());    }    Paint.FontMetrics metrics = mPaint.getFontMetrics();    mTextMaxHeight = (int) (metrics.bottom - metrics.top);}```然后确定布局的大小,布局的宽度就等于测量的mTextMaxWidth,高度为测量的```mTextMaxHeight * itemCount```。这里宽高中可以加入一个额外的Space,要不然文字就会挤到一起,比较难看。```    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        int specWidthSize = MeasureSpec.getSize(widthMeasureSpec);        int specWidthMode = MeasureSpec.getMode(widthMeasureSpec);        int specHeightSize = MeasureSpec.getSize(heightMeasureSpec);        int specHeightMode = MeasureSpec.getMode(heightMeasureSpec);            int width = mTextMaxWidth   mItemWidthSpace;        int height = (mTextMaxHeight   mItemHeightSpace) * getVisibleItemCount();            width  = getPaddingLeft()   getPaddingRight();        height  = getPaddingTop()   getPaddingBottom();        setMeasuredDimension(measureSize(specWidthMode, specWidthSize, width),                measureSize(specHeightMode, specHeightSize, height));    }        private int measureSize(int specMode, int specSize, int size) {        if (specMode == MeasureSpec.EXACTLY) {            return specSize;        } else {            return Math.min(specSize, size);        }    }```### 滚轮的绘制一般滚轮被选中的位置都是在中间,所以显示的设置为奇数比较合适。**为了方便,定义```mHalfVisibleItemCount```作为显示的个数的一半,总显示个数为```mHalfVisibleItemCount * 2   1```,这样就可以保证选中的item在正中间**。首先最简单的,不考虑滚动的情况,直接绘制一列文字,这里为了方便计算,设置中间的为数据集的第0个,第一个item就为```0-mHalfVisibleItemCount```,最后一个则为```mHalfVisibleItemCount```,代码如下:```@Overrideprotected void onDraw(Canvas canvas) {    for (int drawDataPos = -mHalfVisibleItemCount; drawDataPos  mDataList.size() - 1) {            continue;        }        int itemDrawY = mFirstItemDrawY   (drawDataPos   mHalfVisibleItemCount) * mItemHeight;        T data = mDataList.get(pos);        canvas.drawText(data.toString(), mFirstItemDrawX, itemDrawY, mPaint);    }}```接下来加入滚动,获取手指滑动的值, 然后绘制的时候添加偏移量就好了。手指滑动,这个应该都懂,这里就不废话了,直接上代码:```@Overridepublic boolean onTouchEvent(MotionEvent event) {    switch (event.getAction()) {        case MotionEvent.ACTION_DOWN:            mLastDownY = (int) event.getY();            break;        case MotionEvent.ACTION_MOVE:            float move = event.getY() - mLastDownY;            mScrollOffsetY  = move;     //滑动的偏移量            mLastDownY = (int) event.getY();            invalidate();            break;        case MotionEvent.ACTION_UP:            break;    }```更改```onDraw()```的代码。这边有两个问题:- 上下两边要多绘制一个出来,因为在滚动的时候,实际在容器内的item要比原定的item要多一个。- 定位中间向位于数据集的位置,用偏移量 / item的高度即可。由于手指的坐标是以左上角为原点的,这里要注意坐标正负问题。代码如下:```@Overrideprotected void onDraw(Canvas canvas) {    int drawnSelectedPos = - mScrollOffsetY / mItemHeight;    for (int drawDataPos = drawnSelectedPos - mHalfVisibleItemCount - 1;drawDataPos  mDataList.size() - 1) {            continue;        }        int itemDrawY = mFirstItemDrawY   (drawDataPos   mHalfVisibleItemCount) * mItemHeight   mScrollOffsetY;        T data = mDataList.get(drawDataPos);        canvas.drawText(data.toString(), mFirstItemDrawX, itemDrawY, mPaint);    }}```### Fling的效果先上主要代码:```@Overridepublic boolean onTouchEvent(MotionEvent event) {    if (mTracker == null) {        mTracker = VelocityTracker.obtain();    }    mTracker.addMovement(event);    switch (event.getAction()) {        case MotionEvent.ACTION_DOWN:            mTracker.clear();            mTouchDownY = mLastDownY = (int) event.getY();            break;        case MotionEvent.ACTION_MOVE:            mTouchSlopFlag = false;            float move = event.getY() - mLastDownY;            mScrollOffsetY  = move;            mLastDownY = (int) event.getY();            invalidate();            break;        case MotionEvent.ACTION_UP:            mTracker.computeCurrentVelocity(1000, mMaximumVelocity);            int velocity = (int) mTracker.getYVelocity();            mScroller.fling(0, mScrollOffsetY, 0, velocity,                    0, 0, mMinFlingY, mMaxFlingY);            mScroller.setFinalY(mScroller.getFinalY()                      computeDistanceToEndPoint(mScroller.getFinalY() % mItemHeight));            mHandler.post(mScrollerRunnable);            mTracker.recycle();            mTracker = null;            break;    }    return true;}private int computeDistanceToEndPoint(int remainder) {    if (Math.abs(remainder) > mItemHeight / 2) {        if (mScrollOffsetY < 0) {            return -mItemHeight - remainder;        } else {            return mItemHeight - remainder;        }    } else {        return -remainder;    }}private Runnable mScrollerRunnable = new Runnable() {    @Override    public void run() {        if (mScroller.computeScrollOffset()) {            int scrollerCurrY = mScroller.getCurrY();            mScrollOffsetY = scrollerCurrY;            postInvalidate();            mHandler.postDelayed(this, 16);        }    }}```Fling效果的实现主要是用的```VelocityTracker```和```Scroller```,网上已经有很多资料了,这里就不再说明了。这里主要就是获取Scroller当前滚动的值,然后加入到偏移量```mScrollOffsetY```后请求重新绘制。这里有一个finalY的修正计算.当动画停止的时候,停止的位置如果随缘的话,就会经常在停在这种位置:![Picture](//upload-images.jianshu.io/upload_images/2782988-b99e5238b21aee84.jpg)因为要保证当滑动停止的时候,要保证item正好在中间。也就是这样:![Picture](//upload-images.jianshu.io/upload_images/2782988-25554bf82f2cd377.jpg)修正的方法就是:滚动的值只能为item高度的整数。这样就能保证,滚动结束中间的item只能在正中间。利用这个原理,上边的手指滑动,也能在手指离开屏幕后来修正位置。这里还要考虑一个问题,滚动的时候可能会超过给定的数据集的大小,就是当滚动的值超过最后一个数据后,当手指松开后返回到最后一个数据的位置,即下图的效果:![Picture](//upload-images.jianshu.io/upload_images/2782988-b460ee254bbff992.gif)方法还是在ACTION_UP的时候判断是否超过数据集的大小即可。代码如下:```@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {    ...    mMinFlingY = - mItemHeight * (mDataList.size() - 1);    mMaxFlingY = 0;}@Overridepublic boolean onTouchEvent(MotionEvent event) {    ...        case MotionEvent.ACTION_UP:        mTracker.computeCurrentVelocity(1000, mMaximumVelocity);        int velocity = (int) mTracker.getYVelocity();        mScroller.fling(0, mScrollOffsetY, 0, velocity,                0, 0, mMinFlingY, mMaxFlingY);        mScroller.setFinalY(mScroller.getFinalY()                  computeDistanceToEndPoint(mScroller.getFinalY() % mItemHeight));                        if (mScroller.getFinalY() > mMaxFlingY) {            mScroller.setFinalY(mMaxFlingY);        } else if (mScroller.getFinalY() < mMinFlingY) {            mScroller.setFinalY(mMinFlingY);        }        ...}```### 循环滚动接下来要考虑循环滚动的问题。首先 上边的```mMinFlingY```和```mMaxFlingY```在循环滚动的时候就要设置成Integer的极限值,如下```mMinFlingY = mIsCyclic ? Integer.MIN_VALUE : - mItemHeight * (mDataList.size() - 1);mMaxFlingY = mIsCyclic ? Integer.MAX_VALUE : 0;```然后考虑绘制的时候取值问题,在上边onDraw()方法的里面有一个安全值判断,如果超过数据集的大小就跳过此条item绘制,代码如下:```@Overrideprotected void onDraw(Canvas canvas) {    int drawnSelectedPos = - mScrollOffsetY / mItemHeight;    for (int drawDataPos = drawnSelectedPos - mHalfVisibleItemCount - 1;drawDataPos  mDataList.size() - 1) {            continue;        }        ...    }}```我们要改动的就是这里。当循环滚动的时候上下滚动的极限都为无穷,所以```- mScrollOffsetY / mItemHeight;```得到的值应该也在正负无穷之间,我们要把值都映射到数据集中。假设数据集有10条数据,当前滚动的位置为pos:- 当pos>10时,假设当pos = 10时,我们想让其回到第一个数据,对其取余即可:```pos % 10```。- 当pos mDataList.size() - 1) {                continue;            }        }        ...    }}```### 幕布这个就比较简单了,在中间画一个矩形就好了,不过要在绘制滚轮之前绘制,否则会遮盖住文字,代码如下:```@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {    ...    mDrawnRect.set(getPaddingLeft(), getPaddingTop(),        getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());    mSelectedItemRect.set(getPaddingLeft(), mItemHeight * mHalfVisibleItemCount,getWidth() - getPaddingRight(), mItemHeight   mItemHeight * mHalfVisibleItemCount);}@Overrideprotected void onDraw(Canvas canvas) {    super.onDraw(canvas);    mPaint.setTextAlign(Paint.Align.CENTER);    //是否绘制幕布    if (mIsShowCurtain) {        mPaint.setStyle(Paint.Style.FILL);        mPaint.setColor(mCurtainColor);        canvas.drawRect(mSelectedItemRect, mPaint);    }    //是否绘制幕布边框    if (mIsShowCurtainBorder) {        mPaint.setStyle(Paint.Style.STROKE);        mPaint.setColor(mCurtainBorderColor);        canvas.drawRect(mSelectedItemRect, mPaint);        canvas.drawRect(mDrawnRect, mPaint);    }    ...}```### 字体颜色渐变这个分三个部分:### 1. 透明度渐变:Paint在绘制文字的时候可以设置Alpha来设置透明度,Alpha的比例计算方法:$$ \frac{绘制点到端点距离}{中心绘制点到端点距离} $$如下图所示,计算“03”的比例:![Picture](//upload-images.jianshu.io/upload_images/2782988-16bbf74c7416992d.jpg)主要代码如下:```@Overrideprotected void onDraw(Canvas canvas) {    ...    int drawnSelectedPos = - mScrollOffsetY / mItemHeight;    for (int drawDataPos = drawnSelectedPos - mHalfVisibleItemCount - 1;drawDataPos  mDataList.size() - 1) {                continue;            }        }                T data = mDataList.get(pos);        int itemDrawY = mFirstItemDrawY   (drawDataPos   mHalfVisibleItemCount) * mItemHeight   mScrollOffsetY;        //距离中心的Y轴距离        int distanceY = Math.abs(mCenterItemDrawnY - itemDrawY);                float alphaRatio;        if (itemDrawY > mCenterItemDrawnY) {            alphaRatio = (mDrawnRect.height() - itemDrawY) /                    (float) (mDrawnRect.height() - (mCenterItemDrawnY));        } else {            alphaRatio = itemDrawY / (float) mCenterItemDrawnY;        }        mPaint.setAlpha((int) (alphaRatio * 255));        canvas.drawText(data.toString(), mFirstItemDrawX, itemDrawY, mPaint);        ...    }}```### 2. 文字颜色渐变当距离中心绘制点的距离小于一个ItemHeight时,进行文字颜色渐变。首先需要一个线性颜色渐变的工具,思路就是指定一个开始颜色和结束颜色,传入比例获取颜色。比较简单,直接看代码:```public class LinearGradient {    private int mStartColor;    private int mEndColor;    private int mRedStart;    private int mBlueStart;    private int mGreenStart;    private int mRedEnd;    private int mBlueEnd;    private int mGreenEnd;        public LinearGradient(@ColorInt int startColor, @ColorInt int endColor) {        mStartColor = startColor;        mEndColor = endColor;        updateColor();    }            public void setStartColor(@ColorInt int startColor) {        mStartColor = startColor;        updateColor();    }        public void setEndColor(@ColorInt int endColor) {        mEndColor = endColor;        updateColor();    }        private void updateColor() {        mRedStart = Color.red(mStartColor);        mBlueStart = Color.blue(mStartColor);        mGreenStart = Color.green(mStartColor);        mRedEnd = Color.red(mEndColor);        mBlueEnd = Color.blue(mEndColor);        mGreenEnd = Color.green(mEndColor);    }        public int getColor(float ratio) {        int red = (int) (mRedStart   ((mRedEnd - mRedStart) * ratio   0.5));        int greed = (int) (mGreenStart   ((mGreenEnd - mGreenStart) * ratio   0.5));        int blue = (int) (mBlueStart   ((mBlueEnd - mBlueStart) * ratio   0.5));        return Color.rgb(red, greed, blue);    }}```接下来就是计算文字颜色渐变了,和上边的计算透明度的类似,代码如下:```@Overrideprotected void onDraw(Canvas canvas) {    ...    int drawnSelectedPos = - mScrollOffsetY / mItemHeight;    for (int drawDataPos = drawnSelectedPos - mHalfVisibleItemCount - 1;drawDataPos  mCenterItemDrawnY) {            alphaRatio = (mDrawnRect.height() - itemDrawY) /                    (float) (mDrawnRect.height() - (mCenterItemDrawnY));        } else {            alphaRatio = itemDrawY / (float) mCenterItemDrawnY;        }        mPaint.setAlpha((int) (alphaRatio * 255));        canvas.drawText(data.toString(), mFirstItemDrawX, itemDrawY, mPaint);        ...    }}```### 3.文字大小渐变:这个和文字颜色渐变实现思路一模一样,变化的项由文字的颜色变为大小。也是在中心位置一个ItemHeigh的距离进行计算,直接上代码:```@Overrideprotected void onDraw(Canvas canvas) {    ...    int drawnSelectedPos = - mScrollOffsetY / mItemHeight;    for (int drawDataPos = drawnSelectedPos - mHalfVisibleItemCount - 1;drawDataPos  mCenterItemDrawnY) {            alphaRatio = (mDrawnRect.height() - itemDrawY) /                    (float) (mDrawnRect.height() - (mCenterItemDrawnY));        } else {            alphaRatio = itemDrawY / (float) mCenterItemDrawnY;        }        mPaint.setAlpha((int) (alphaRatio * 255));                //靠近中心的Item字体放大        if (distanceY < mItemHeight) {            float addedSize = (mItemHeight - distanceY) / (float) mItemHeight * (mSelectedItemTextSize - mTextSize);            mPaint.setTextSize(mTextSize   addedSize);        } else {            mPaint.setTextSize(mTextSize);        }        canvas.drawText(data.toString(), mFirstItemDrawX, itemDrawY, mPaint);        ...    }}```### 指示器文字实现指示器文字就是直接在中间的item后边绘制一个文字,只需要计算一下要绘制的坐标即可,直接在```onDraw()```方法的的最后加入就好了。代码如下:```@Overrideprotected void onDraw(Canvas canvas) {    ...    mPaint.setTextAlign(Paint.Align.CENTER);    int drawnSelectedPos = - mScrollOffsetY / mItemHeight;    for (int drawDataPos = drawnSelectedPos - mHalfVisibleItemCount - 1;drawDataPos
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

站长推荐

通过邮件订阅最新安卓weekly信息
上一条 /4 下一条

下载安卓巴士客户端

全国最大的安卓开发者社区

广告投放| 广东互联网违法和不良信息举报中心|中国互联网举报中心|下载客户端|申请友链|手机版|站点统计|安卓巴士 ( 粤ICP备15117877号 )

快速回复 返回顶部 返回列表