1、简述
Today,写一个像尺子一样的滑动选择控件,效果如下:
控件可随手指左右滑动,手指抬起后,若手指力量不足以使它惯性滑动,则会自动将选中的刻度线滑到与中间对齐;若可以惯性滑动,则先加速后减速滑动,停止时选中的刻度线也是与中间对齐,有点类似给 RecyclerView 设置 LinearSnapHelper 后的效果。这种可滑动选择控件比较常见,比如app中的日期、地区选择等等。因为它长的像尺子,所以叫它RulerSelectView。
2、实现方法
第一种方法是使用RecyclerView。RecyclerView这么强大,是肯定可以实现的,而且实现起来并不复杂。一个刻度就是一个ItemView,刻度线就是View加一个带颜色的背景,或者只使用一个空白的View,用ItemDecoration来绘制刻度线。再利用LinearSnapHelper 对齐刻度 ItemView。两边的透明渐变效果通过下面两个属性指定:
//指定渐变方向
android:requiresFadingEdge="horizontal"
//指定渐变长度
android:fadingEdgeLength="100dp"
这样实现起来确实很简单。这种方法不仅仅可以实现这个尺子选择控件,只要是类似的,可以滑动和惯性滑动,需要对齐的控件都可以实现,比如选择生日的控件等等。
第二种方法是自定义View实现。继承View,绘制刻度线,监听触摸事件,处理滑动和惯性滑动,滑动冲突,对齐,边界检查(不能滑出最小/大刻度)等等问题,都需要我们自己来实现。这种方法能运用更多的技能,也并不是很难,但实现过程中可能会遇到各种问题,需要一些时间来不断的调试和修改,最后直到完美。
3、代码实现
我不打算在xml中定义需要的属性,定义这些属性我得考虑会不会和别的控件属性相冲突,而且还需要浪费性能来解析这些个属性。所以我会在代码里面定义一些set方法,通过这些方法来设置和定制该控件。
初始化
重写了三个构造方法,前两个都会调用第三个构造方法。在第三个构造方法中,初始化了一些成员变量,注释对各变量进行了说明。
public RulerSelectView(Context context) {
this(context, null);
}
public RulerSelectView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public RulerSelectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//画笔,绘制刻度线和刻度文字
mPath = new Path();//刻度线的路径
mDecimalNumber = 10;//进制数,表示一个进制单位包含多少个刻度,如:8,10,16等
mDecimal = 10;//精确度,值为10的x次的幂,x为小数位数
mSegmentNumber = 5;//片段(高低刻度线分割的片段),表示一个片段包含多少个刻度
mIsShowSelectedScale = true;//是否显示中间选中刻度线
mHasGradient = true;//是否有渐变效果
final int gap = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, getResources().getDisplayMetrics());
mScaleLineGap = gap;//刻度线之间的间隔
mScaleLineHeight = gap * 3;//刻度线的高度
mSegmentLineHeight = gap * 4;//片段刻度线的高度
mDecimalLineHeight = mSegmentLineHeight;//进制刻度线的高度
mScaleLineColor = Color.GRAY;//刻度线的颜色
mSelectedColor = Color.RED;//中间选中刻度线的颜色
mPaint.setTextAlign(Paint.Align.CENTER);//设置文本居中绘制
mPaint.setTextSize(gap);//设置刻度文字大小
mPaint.setStrokeWidth(gap / 10f);//设置刻度线宽度
//OverScroller是处理滑动和惯性滑动的利器,构造时传入了一个减速插值器
mScroller = new OverScroller(getContext(), new DecelerateInterpolator());
ViewConfiguration vc = ViewConfiguration.get(getContext());
//最小滑动速率,可以触发惯性滑动的最小速率
mScaledMinimumFlingVelocity = vc.getScaledMinimumFlingVelocity();//150
//最大滑动速率,后面使用 VelocityTracker 获取速率是会用到
mScaledMaximumFlingVelocity = vc.getScaledMaximumFlingVelocity();//24000
}
测量大小
为了支持在布局文件中指定宽高为wrap_conent,我强制设置wrap_conent时宽为屏幕宽度,高度为100dp。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
//如果widthMode是AT_MOST或UNSPECIFIED,width为父控件剩余的宽度,否则为布局文件中声明的宽度
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
//如果是heightMode是AT_MOST或UNSPECIFIED,height为父控件剩余的高度,否则为布局文件中声明的高度
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
if (widthMode != MeasureSpec.EXACTLY) {
width = displayMetrics.widthPixels;
}
if (heightMode != MeasureSpec.EXACTLY) {
height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100f, displayMetrics);
}
setMeasuredDimension(width, height);
}
OnSizeChange
测量完成后,这个方法会马上调用,该方法中可以拿到view的宽高。
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
Rect rect = new Rect();
//测量刻度文字的高度,用户计算下面的Y坐标值
mPaint.getTextBounds("1", 0, 1, rect);
int textHeight = rect.height();
//刻度线底部在view中的Y坐标,用于绘制刻度线
mScaleBottomY = h - getPaddingBottom() - textHeight - mScaleLineGap;
if (mHasGradient) {//如果有渐变效果,初始化LinearGradient,用它来实现渐变。
mLinearGradient = new LinearGradient(0f, 0f, w / 2f, 0f,//设置渐变效果从0到中间位置
new int[]{Color.TRANSPARENT, mScaleLineColor},//渐变的颜色
new float[]{0.2f, 0.8f},//渐变颜色占的比重,透明为0.2,不透明为0.8
Shader.TileMode.MIRROR);//设置平铺模式为镜像,即中间位置到width的渐变效果为0到中间位置的镜像(中间位置到 0)
}
}
绘制刻度线
有了刻度线之后,我们才好做下一步处理。考虑到绘制的效率,只绘制屏幕上出现的刻度线,没有出现的刻度线不用绘制,一是因为浪费时间和性能,二是因为使用Path绘制路劲时,如果坐标很大(y坐标超过9000 或 x坐标超过9000),Path 不会绘制出来。
选中的刻度线(mSelectedPosition)始终是在中点,绘制的时候以中点坐标 cx 为基准计算每个刻度线的坐标,先绘制它和大于它(后面)的刻度线,坐标大于控件 width - paddingEnd 的不绘制。再绘制小于它(前面)的刻度线,坐标小于 paddingStart 的不绘制。
mSelectedPosition 默认为 0,在滑动结束后,才会更新为当前选中的刻度,在滑动过程中还是上一次选中的刻度。所以滑动距离很大时,mSelectedPosition 可能并不在屏幕上。比如手指往左滑动了很久,这时绘制后面的刻度线时,其实有很多刻度线小于了paddingStart,这些刻度也是不能绘制的。同样在手指往右滑动了很久,绘制前面的刻度线时,大于 width - paddingEnd 的刻度线不能绘制。
同时在绘制时,计算了mSnapX 的值,该值的绝对值是所有刻度线中与中点距离相差最小的值,它对应的刻度线为 mSnapPosition 。mSnapPosition 记录着滑动过程中选中刻度线的变化,在滑动结束后会赋值给 mSelectedPosition。
@Override
protected void onDraw(Canvas canvas) {
if (mData == null || mData.isEmpty()) return;//mData是刻度线集合,没有刻度线不绘制
final int w = getWidth();
final int h = getHeight();
if (w == 0 || h == 0) return;//宽或高为0不绘制
mPath.reset();//清除上一次的路径
final int size = mData.size();//刻度值的大小
float cx = w / 2f;//中点
float x;//刻度线的x坐标
float snap;//刻度线与中点的距离( cx - x ),最小的snap对应的刻度即为当前选中的刻度线
mSnapX = Integer.MAX_VALUE;//最小的snap,这里是重置,后面会对其赋值
if (mHasGradient) {
mPaint.setShader(mLinearGradient);//有渐变效果,设置渐变渲染
} else {
mPaint.setColor(mScaleLineColor);//没有渐变效果,就刻度线颜色
}
mPaint.setStyle(Paint.Style.FILL);
//两个循环,一个从 mSelectedPosition 往上(++)循环,另一个从 mSelectedPosition 往下(--)开始循环
//选中的刻度线始终在中点,以中点坐标 cx 为基准计算每个刻度线的坐标
for (int i = mSelectedPosition; i < size; i++) {
//x 为当前i(刻度线)的坐标,mOffset为滑动的距离,滑动时会不断改变它
x = cx + (i - mSelectedPosition) * mScaleLineGap + mOffset;
if (x > w - getPaddingEnd()) break;//超过w - getPaddingEnd直接返回
//在往左的fling状态下(很快),由于mSelectedPosition没有及时改变,会有很多大于mSelectedPosition的i的坐标小于getPaddingStart,这样的i也不绘制,但不能break,因为真正要绘制的i(出现在屏幕上的)比这样的i大,所以只能跳过。
if (x < getPaddingStart()) continue;
snap = cx - x;//当前i与中点的距离
forPath(canvas, i, x, snap, h);//该方法用于更新mSnapX ,添加路径,绘制刻度文字
}
if (mSelectedPosition > 0) {
for (int i = mSelectedPosition - 1; i >= 0; i--) {
x = cx - (mSelectedPosition - i) * mScaleLineGap + mOffset;
if (x < getPaddingStart()) break;//小于getPaddingStart直接返回
//在往右的fling状态下(很快),由于mSelectedPosition没有及时改变,会有很多小于mSelectedPosition的i的坐标大于w - getPaddingEnd,这样的i也是不绘制的,但不能break,因为真正要绘制的i(出现在屏幕上的)比这样的i小,所以只能continue;
if (x > w - getPaddingEnd()) continue;
snap = cx - x;//当前i与中点的距离
forPath(canvas, i, x, snap, h);
}
}
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawPath(mPath, mPaint);//绘制刻度线路径
mPath.reset();//清除路径
if (mHasGradient) {
mPaint.setShader(null);//重置
}
if (mIsShowSelectedScale) {//绘制中间选中的刻度线
mPath.moveTo(cx, mScaleBottomY);
mPath.lineTo(cx, mScaleBottomY - mScaleLineHeight - 2 * mScaleLineGap);
mPaint.setColor(mSelectedColor);
canvas.drawPath(mPath, mPaint);
}
//mLastSnapPosition 为滑动过程中上次的选中刻度线
//mSnapPosition 为滑动过程中的选中刻度线
//滑动时,如果上一次与这次的不一样,就通过接口回调来通知选中刻度线的变化
if (mLastSnapPosition != mSnapPosition) {
mLastSnapPosition = mSnapPosition;//更新滑动过程中上次的选中刻度线
mSelectedValue = Math.round(mData.get(mSnapPosition) * mDecimal) / mDecimal;//计算选中刻度线对应的刻度值
if (mRulerSelectChangeListener != null) {//接口回调监听者选中的刻度线变了
mRulerSelectChangeListener.onRulerChange(this, mSnapPosition, mSelectedValue);
}
}
}
//更新mSnapX ,添加路径,绘制刻度文字
private void forPath(Canvas canvas, int i, float x, float snap, int h) {
if (Math.abs(mSnapX) > Math.abs(snap)) {//滑动过程中,与中点距离最短的i发生变化
mSnapX = snap;//更新mSnapX为最小的snap
if (mSnapPosition != i) mSnapPosition = i;//更新选中的i
}
//下面是添加刻度线路径和绘制刻度线文字
mPath.moveTo(x, mScaleBottomY);
if (i % mDecimalNumber == 0) {//进制刻度线
mPath.lineTo(x, mScaleBottomY - mDecimalLineHeight);
float s = Math.round(mData.get(i) * mDecimal) / mDecimal;
String text;
if (s * mDecimalNumber % mDecimalNumber == 0) {
text = String.valueOf((int) s);
} else {
text = String.valueOf(s);
}
canvas.drawText(text, x, h - getPaddingBottom(), mPaint);
} else if (i % mSegmentNumber == 0) {//片段刻度线
mPath.lineTo(x, mScaleBottomY - mSegmentLineHeight);
} else {
mPath.lineTo(x, mScaleBottomY - mScaleLineHeight);
}
}
监听触摸事件处理滑动
要让刻度线滑动起来,就要根据手指的滑动距离来不断的重新绘制,在onDraw中,计算刻度线坐标时已经加上了滑动的距离mOffset,我们只要在 ACTION_MOVE 事件改变它即可。
可以随手指滑动之后,还需要处理 ACTION_UP事件。当手指抬起时我们通过 VelocityTracker 获取手指的速率,判断该速率是否足够惯性滑动,不足够的话就需要调整选中的刻度线到中间位置,在onDraw中已经计算了要调整的距离mSnapX。怎么调整呢,肯定不能从当前刻度线直接到中点,这样像断片了一样,我们使用 Scroller 来平缓调整到中间。如果足够惯性滑动的话,我们使用 Scroller 的 fling方法来惯性滑动。
因为该控件需要消费触摸事件,若放到也可以左右滑动的ViewPager中,会产生滑动冲突导致无法正常滑动,所以需要处理冲突。处理方法是,当该控件可以滑动时(继续滑动不会超过边界),请求父控件不拦截事件,当该控件不能滑动了(继续滑动会超过边界),就默认由父处理事。
Scroller并不直接让 View 滑动,它只是用来计算一个运动过程中不同时间的运动距离。 当调用它的 startScroll 方法时,它并没有做什么特殊操作,只是保存传入的参数和记录运动开始的时间,当调用它的 computeScrollOffset 方法时,会根据开始时间,插值器,和startScroll 方法传入的距离(最终距离)、时间等等计算出此时的运动距离,并返回一个布尔值,true表示运动结束,false表示还没有结束。它的filing方法,会根据传入速率计算出的该速率可以运动多长距离,并在调用 computeScrollOffset 方法时,计算惯性运动此时运动的距离,该距离和真实的运动效果一样。它的 getCurrX/Y 方法用来获取当前时间的距离。
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mData == null || mData.isEmpty()) return super.onTouchEvent(event);
if (mVelocityTracker == null) {//初始化了mVelocityTracker
mVelocityTracker = VelocityTracker.obtain();
}
// 只在MotionEvent.ACTION_UP里面添加是不起作用的
// 翻译:向速率追踪者中加入一个用户的移动事件,你应该最先在ACTION_DOWN调用这个方法,
// 然后在你接受的ACTION_MOVE,最后是ACTION_UP。你可以为任何一个你愿意的事件调用该方法
mVelocityTracker.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//不是在边界,需要消费事件,请求父View请求不要拦截
if (!isRightBound() && !isLeftBound()) {
getParent().requestDisallowInterceptTouchEvent(true);
}
if (mScroller.computeScrollOffset()) {//如果上一次运动没有结束,停止掉
mScroller.forceFinished(true);
}
mIsFling = false;//是否是惯性滑动状态,重置为不是
mLastX = event.getX();//获取手指按下的坐标
//左边可以被滑动的最大距离,正数;手指向右滑时进行比较,mOffset为正
mMaxLeftOffset = mSelectedPosition * mScaleLineGap;
//右边可以被滑动的最大距离,负数;手指向左滑时进行比较,mOffset为负
mMaxRightOffset = (mSelectedPosition - mData.size() + 1) * mScaleLineGap;
break;
case MotionEvent.ACTION_MOVE:
float x = event.getX();//手指的坐标
float dx = x - mLastX;//手指相比上一次的滑动距离,往左为负,往右为正
boolean isLeftBound = isLeftBound();//是否到达左边界
boolean isRightBound = isRightBound();//是否到达右边界
if (isLeftBound || isRightBound) {
//如果到达左边界,手指还往右滑,不处理;如果到达右边界,手指还往左滑,不处理
if (dx == 0 || (isLeftBound && dx > 0) || (isRightBound && dx < 0)) {
return false;
}
}
//如果到达了边界,但滑动方向与边界方向相同,仍然可以滑动,请求不要拦截
getParent().requestDisallowInterceptTouchEvent(true);//请求不要拦截
if (dx == 0) return true;//滑动距离为0不重新绘制
mOffset += dx;//累加滑动的距离
mLastX = x;//更新为当前坐标
if (dx > 0) {//手指往右滑动,被滑动的是左边的距离
if (mOffset > mMaxLeftOffset) {//如果大于左边可滑的最大距离,重新赋值
mOffset = mMaxLeftOffset;
}
} else {//往左滑动,被滑动的是右边的距离
if (mOffset < mMaxRightOffset) {//如果小于右边可滑的最大距离,重新赋值
mOffset = mMaxRightOffset;
}
}
//请求重新绘制,执行onDraw方法,因为mOffset的值已经发生变化,所以绘制的刻度线位置也发生变化,看起来就像在滑动一样
invalidate();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
//参数:units 你想要指定的得到的速度单位,如果值为1,代表1毫秒运动了多少像素。
// 如果值为1000,代表1秒内运动了多少像素。
// 同样的力量,units 的数值越大,滑动的距离越长,数值越小,滑动的距离越小
mVelocityTracker.computeCurrentVelocity(1000, mScaledMaximumFlingVelocity);
//手指往左,velocityX 与 mOffset一样为负数; 手指往右,velocityX 与 mOffset一样为正数
float velocityX = mVelocityTracker.getXVelocity();//获取手指的x轴方向速率
//如果超过最小速率,可以惯性滑动
if (Math.abs(velocityX) > mScaledMinimumFlingVelocity) {
//但是如果已经在左边界,不能向右惯性滑动;如果已经在右边界,不能向左惯性滑动
if ((isLeftBound() && velocityX > 0) || (isRightBound() && velocityX < 0)) {
mLastX = 0;//重置
mVelocityTracker.clear();//清除
break;//返回
}
mIsFling = true;//要开始fling了,设置为true
mScroller.fling(0, 0, (int) velocityX, 0,
Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);//将速率设置给 mScroller 计算可以滑动多长距离
mOffset = Math.round(mOffset);//取个整
} else {//没有触发filing ,滑动到中间位置与其对齐
mScroller.startScroll(0, 0, (int) mSnapX, 0, 300);
}
//必须请求绘制,才会执行computeScroll方法和 onDraw方法
invalidate();
mLastX = 0;
mVelocityTracker.clear();
break;
}
return true;//只有 ACTION_DOWN 被消费,后续的事件才会被接受到
}
private boolean isLeftBound() {//是否到达左边界
//选中的刻度线是第1个,并且与中间距离为0必然是到了左边界
return mSnapPosition == 0 && mSnapX == 0;
}
private boolean isRightBound() {是否到达右边界
//选中的刻度线是最后一个,并且与中间距离为0必然是到了右边界
return mSnapPosition == mData.size() - 1 && mSnapX == 0;
}
computeScroll 处理 fling
onTouchEvent 中只是调用了Scroller 相应的方法,还要在computeScroll 方法中获取新的滑动距离赋值给mOffset,来让刻度线滑动到相应位置,该方法在每次绘制前被调用。
在手指抬起时,我们判断了不能触发 fling 时,就滑动 mSnapX 距离来对齐到中点,因 mSnapX 本身是选中刻度线与中点的距离,且在ACTION_MOVE 中我们已经做了边界检查,所以该滑动自然是不会超过边界。但是,在 fling 时,如果速率很大,fling 的距离会很长,就可能出现超过边界的问题。另外,fling 结束时,刻度线要刚好停在在对齐位置上,但 fling 的距离是由速率来决定,无法控制。我们需要处理好这两个问题。
虽然 fling 的距离我们无法控制,但是我们知道 fling 结束时应该停在哪里,超过边界时,应该刚好停在边界上,不超过边界时,选中刻度线应该刚好是停在中点的。当调用Scroller的 filing 方法后,getFinalX方法可以立马获取到可以 fling 的距离,试想我们可以计算出与该距离最相近的,且是刻度线间隔整数倍的新距离,然后通过Scroller的 startScroll 来滑动该新距离,若不超过边界,这样刻度线会刚好停在中点,但是这样看到的就不是真实的惯性滑动效果了,因此还是要在 fling 过程中处理。
在ACTION_DOWN 中,我们计算了本次滑动可以向左的最大距离mMaxLeftOffset,和向右的最大距离mMaxRightOffset,fling 时,我们可以判断滑动距离 mOffset 是否超过最大距离,如果超过,我们就立马停止 fling 。如果不超过的话,就判断是否小于刻度线的间隔,如果小于,同样也是停止 fling ,但是会调用 startScroll 开启新的滑动,使选中刻度线与中的对齐。这样就解决了上面两个问题点,很自然真实的 fling 到对齐位置上。
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
final int currX = mScroller.getCurrX();//获取当前时间的运动距离
final int finalX = mScroller.getFinalX();//获取终点距离
//往右fling时finalX > 0 , 如果大于左边可滑的最大距离,重新赋值
if (mIsFling && finalX > 0 && mOffset > mMaxLeftOffset) {
mScroller.forceFinished(true);//结束运动
//让选中的刻度线为第一个,并让mOffset = 0 ,这样绘制时就在左边界
mSelectedPosition = 0;
mOffset = 0;
mIsFling = false;
invalidate();
return;//返回
}
//往左fling时finalX < 0 , 如果小于右边可滑的最大距离,重新赋值
if (mIsFling && finalX < 0 && mOffset < mMaxRightOffset) {
mScroller.forceFinished(true);
//让选中的刻度线为最后一个,并让mOffset = 0 ,这样绘制时就在右边界
mSelectedPosition = mData.size() - 1;
mOffset = 0;
mIsFling = false;
invalidate();
return;//返回
}
final int remain = Math.abs(finalX - currX);//还剩多少运动距离
if (mIsFling && remain <= mScaleLineGap) {//不会超过边界,但是剩下的距离小于刻度线的间距,需要调整到中点
int dx = mScaleLineGap;
//mold有正负,与mOffset正负一致
int mold = Math.round(mOffset % dx );//已滑动距离除以间距的余数
//finalX 和 mOffset 可能会不是同一方向的(手指往左滑,mOffset为负,释放时却是往右,finalX为正数)
if (finalX > 0) {//向右filing 时,dx应为正数,mOffset变大
dx -= mold;//mOffset + mScaleLineGap - mode 刚好是间距的整数倍
} else {//向左filing 时,dx应为负数,mOffset变小
dx = -dx - mold; //mOffset - mScaleLineGap - mold 刚好是间距的整数倍
}
//dx正数相当于手指往右滑动,dx负数相当于手指往左滑动
//已经滑动了mOffset 距离,再滑动 dx 距离 ,最终的滑动距离会比 finalX 长
mIsFling = false;//不是在fling状态了
mLastX = 0;
mScroller.forceFinished(true);//停止fling
mScroller.startScroll(0, 0, dx, 0, 180);//开启新的滑动
invalidate();
return;//返回
}
mOffset = mOffset + currX - mLastX;//累加滑动距离
mLastX = currX;//更新
if (mScroller.isFinished()) {
//运动结束,更新选中的刻度线,并让mOffset = 0 ,这样绘制时选中刻度线就在中间
mSelectedPosition = mSnapPosition;
mOffset = 0;
}
//调用Scroller的滑动相关方法后必须请求重绘,不然不会再次computeScroll计算新的运动距离
invalidate();
}
}
onSaveInstanceState 保存数据
完成上面的步骤后,我们的控件基本完成。但是自定义View 还有重要的一点是要处理页面重建过程中View的数据的保存和恢复,以不至于重建后数据丢失。 所以当屏幕旋转过程中,我们要保存当前选中的刻度线 mSelectedPosition的值,代码很简单。
protected Parcelable onSaveInstanceState() {
if (mSelectedPosition != 0) {//不是0才需要保存我们的数据
Bundle bundle = new Bundle();//新建一个Bundle用于保存数据
Parcelable parcelable = super.onSaveInstanceState();//获取View自身的数据
bundle.putParcelable("sys", parcelable);//保存View自身的数据
bundle.putInt("i", mSelectedPosition);//保存我们的数据
return bundle;
} else {
return super.onSaveInstanceState();
}
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {//判断是不是我们保存的数据
Bundle bundle = (Bundle) state;
mSelectedPosition = bundle.getInt("i");//恢复我们的数据
mSelectedValue = mData.get(mSelectedPosition);
super.onRestoreInstanceState(bundle.getParcelable("sys"));//恢复View自身的数据
} else {
super.onRestoreInstanceState(state);
}
}
到这里,RulerSelectView 就完工了,另外还有设置数据源和一些get、set方法没有贴出代码,有兴趣的可以去查看下源码。
总结
最后,总结这个自定义 View 运用了哪些知识:
1、如何让自定义的View可以滑动,以及滑动过程中的高效的绘制,边界处理;
2、处理触摸事件,和滑动冲突;
3、使用Scroller 和 VelocityTracker 处理滑动和惯性滑动;
4、巧妙地先 fling,然后再 startScroll 到终点,实现自然的滑动到对齐位置;
5、View 重建过程中,数据的保存和回复。