实现的尺子滑动的效果如下:
使用到知识点:view的滑动,以及view的绘画尺子。
view的滑动相关知识介绍
先需要了解如何实现view的滑动,比较常用的有如下几种方式:
通过view本身的scrollTo/scrollBy方法实现滑动;
通过动画的方式实现view的滑动;
通过改变view的LayoutParams的参数,重新布局,实现滑动。
实例中尺子的滑动就是通过第一种方式。scrollTo/scrollBy只能将view的内容滑动(确切的说是只改变view中的mScrollX和mScrollY),不改变view的布局。scrollBy方法会间接调用scrollTo(mScrollX+X,mScrollY+Y);通过getCurrX()和getCurrY()可以获得view中mScrollX和mScrollY的参数。
如图是mScrollX和mScrollY的变化规律,蓝色阴影部分为内容,绿色的边框是布局。
三种滑动方式的比较:
scrollTo/scrollBy的方式:不影响点击事件,只能滑动view的内容,不能滑动view本身。
动画的方式:除缺少交互外,3.0以上的属性动画没有明显确定。3.0以下存在点击事件在原处的问题。
改变布局参数方式:没有明显缺点,就是相对比较麻烦。
因为使用scrollTo/scrollBy的方式来完成,因此需要InnerRulerView类作为view的内容。OutRulerView继承ViewGroup作为包裹InnerRulerView的壳。
解析InnerRulerView类
InnerRulerView的具体作用:
- 绘制尺子的刻度,刻度线,指示线。
- 提供具体的刻度线所在的数值。
- 根据滑动距离,保持指示线居中
InnerRulerView.java代码解析
public class InnerRulerView extends ScrollView implements RulerMoveDistanceListener {
private int mHeight = 80;//尺子的高度
private int mWidth;
private int mCount = 30;//屏幕范围内划分30个,每隔代表0.1
private int mLeftNum = 0;//屏幕最左边的数字
private int mRightNum = 1230;//最右边的数字
private int mMidNum = 615;//当前数字
//整数刻度线长度、宽度,
private int mIntHeight = 36;
private int mIntWidth = 4;
//小数刻度线长度、宽度
private int mDecimalHeight = 18;
private int mDecimalWidth = 2;
//中间指示线长度、宽度
private int mMidNumHeight = 40;
private int mMidNumWidth = 4;
private static final String TICK_MARK_COLOR = "#CCCCCC";
private static final String INDICATOR_COLOR = "#00FF00";
private static final String RULER_BACKGROUND_COLOR = "#F5F5DC";
private Paint paint;
private Rect mTextRect = new Rect();//文字的矩阵
private int mTextSize = 18;
private static final String TAG = "RulerView";
private float mScrollLength;
private boolean mFirstDrawFlag = true;
public static int space;
public InnerRulerView(Context context) {
this(context, null);
}
public InnerRulerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public InnerRulerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.STROKE);
//获得屏幕宽度
WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
mWidth = outMetrics.widthPixels;
Log.i(TAG, "RulerView: 屏幕宽度 = " + mWidth);
}
private int dp2px(int mHeight) {
return (int) (getResources().getDisplayMetrics().scaledDensity * mHeight + 0.5);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
space = mWidth / mCount;//间隔空间
int index = 0;
//尺子背景色
canvas.drawColor(Color.parseColor(RULER_BACKGROUND_COLOR));
for (int i = mLeftNum; i <= mRightNum; i++) {
if (i % 10 == 0) {//整数的情况
//画长刻度线
paint.setColor(Color.parseColor(TICK_MARK_COLOR));
paint.setStrokeWidth(dp2px(mIntWidth));
paint.setStrokeCap(Paint.Cap.ROUND);
canvas.drawLine(space * (index - mLeftNum), 0 , space * (index - mLeftNum), dp2px(mIntHeight), paint);
//画指数
paint.reset();
paint.setColor(Color.BLACK);
paint.setTextSize(dp2px(mTextSize));
String numStr = String.valueOf(i / 10);
//获取整个文字的矩形
paint.getTextBounds(numStr, 0, numStr.length(), mTextRect);
//offsetY = mTextRect的顶部都刻度线底部的距离 , 会在下图1.1中具体指出
int offsetY = dp2px(mHeight - mIntHeight) / 2 - mTextRect.height() / 2;
//刻度值在下半部分的空间中居中显示,文字绘制的起点是左下角的X,Y坐标开始
canvas.drawText(numStr, space * (index - mLeftNum) - mTextRect.width() / 2, dp2px(mIntHeight) + offsetY + mTextRect.height(), paint);
} else {
//画短刻度线
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.parseColor(TICK_MARK_COLOR));
paint.setStrokeWidth(dp2px(mDecimalWidth));
paint.setStrokeCap(Paint.Cap.ROUND);
canvas.drawLine(space * (index - mLeftNum), 0 , space * (index - mLeftNum), dp2px(mDecimalHeight), paint);
}
index++;
}
//画居中的绿色刻度线
paint.setColor(Color.parseColor(INDICATOR_COLOR));
paint.setStrokeWidth(dp2px(mMidNumWidth));
paint.setStrokeCap(Paint.Cap.ROUND);
Log.i(TAG, "onDraw: getMeasuredWidth = " + getMeasuredWidth());
float indicatorDis = mWidth * 20.5f - mScrollLength;
indicatorDis = indicatorDis <= 0 ? 0 : indicatorDis;//不小于0
indicatorDis = indicatorDis >= mWidth*41.0f ? mWidth*41.0f : indicatorDis;//不大于123
canvas.drawLine(indicatorDis, 0 ,indicatorDis,dp2px(mMidNumHeight),paint);
//计算出具体指示
mMidNum = (int) (((mWidth * 20.5f - mScrollLength))/space + 0.5);
mMidNum = mMidNum > mRightNum ? mRightNum : mMidNum;
mMidNum = mMidNum < 0 ? 0 : mMidNum;
if(mSelectNumListener != null){
mSelectNumListener.selectNum(mMidNum);
}
}
@Override
public void rulerMove(float distance) {
mScrollLength = distance;
invalidate();
mFirstDrawFlag = false;
}
interface OnSelectNumListener{
void selectNum(int num);
}
private OnSelectNumListener mSelectNumListener;
public void setOnSelectNumListener(OnSelectNumListener selectNumListener){
this.mSelectNumListener = selectNumListener;
}
public int getmMidNum() {
return mMidNum;
}
public void setmMidNum(int mMidNum) {
this.mMidNum = mMidNum;
}
}
上述代码中offsetY的位置(mTextRect的顶部都刻度线底部的距离)。canvas.drawText开始的起点在61的左下角的位置。
解析OutRulerView类
OutRulerView的具体作用:
- 实现innerRulerView的滑动(MotionEvent.ACTION_MOVE情况下的滑动,以及抬起手指后的惯性滑动)
- 当滑动停止后,当指示线不在刻度线上,需要滑动到最近的刻度线边上。
- 记录下滑动距离,保证不滑出刻度范围,保证指示线始终保持居中。通过mMoveDistanceListener.rulerMove(int distance)方法中的distance,让InnerRulerView绘画指示线保持居中
OutRulerView类代码解析
public class OutRulerView extends ViewGroup {
private InnerRulerView mRulerView;
private int mHeight = 80;
private int mWidth;
private static final String TAG = "OutRulerView";
private OverScroller mOverScroller;
private VelocityTracker mVelocityTracker;//速度追踪
private int mScaledMaxFlingVelocity,mScaledMinFlingVelocity;
private float mStartX,mCurrentX;
private float mFlingStartX,mFlingCurrentX;//滑动时x坐标变化
private Paint paint;
private static final String INDICATOR_COLOR = "#00FF00";
private float mTotalDis = 0;//总的变化距离,向右滑动距离为+,向左滑动距离为-
private boolean mSmoothScrollFinish = false;
private boolean mIsMoveFinish = false;
private int mTextSize = 22;
private boolean isActionDown = false;//标记手指是否按下,作为最后取整滑动的标志之一
private float mMaxMoveDistance;//单方向最大的移动距离
private int mLeftWidthNum = 20,mRightWidthNum = 21;
public OutRulerView(Context context) {
this(context,null);
}
public OutRulerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public OutRulerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//添加InnerRulerView到OutRulerView
mRulerView = new InnerRulerView(context);
addView(mRulerView);
setInnerRulerView(mRulerView);
// 设置ViewGroup可画
setWillNotDraw(false);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.parseColor(INDICATOR_COLOR));
paint.setStrokeWidth(2);
paint.setTextSize(dp2px(mTextSize));
// 具体RulerView宽高的px值
WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
mWidth = outMetrics.widthPixels;
mHeight = (int) (getResources().getDisplayMetrics().scaledDensity * mHeight + 0.5f);
//初始化滑动相关内容
mOverScroller = new OverScroller(context);
mVelocityTracker = VelocityTracker.obtain();
mScaledMaxFlingVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
mScaledMinFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//确定子view位置,就是InnerRulerView的位置
mRulerView.layout(-mWidth*mLeftWidthNum,0,mWidth*mRightWidthNum, mHeight );
mMaxMoveDistance = mWidth*(mRightWidthNum+mLeftWidthNum)/2.0f;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
mCurrentX = ev.getX();
//开始速度检测
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(ev);
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
if(!mOverScroller.isFinished()){
mOverScroller.abortAnimation();
}
mStartX = ev.getX();
Log.i(TAG, "onTouchEvent: ACTION_DOWN mStartX = " + mStartX );
isActionDown = true;
break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "onTouchEvent: Math.abs(mTotalDis) = " + Math.abs(mTotalDis) + " , mMaxMoveDistance = " + mMaxMoveDistance + " , Math.abs(mTotalDis) < mMaxMoveDistance " +(Math.abs(mTotalDis) < mMaxMoveDistance));
if(Math.abs(mTotalDis) < mMaxMoveDistance){//未超过边界
//在这里需要搞清楚disX的正负,以及scrollBy参数中左滑右滑的正负符号。
float disX = mCurrentX - mStartX;//右滑disX>0,左滑disX<0
mTotalDis += disX;
scrollBy((int) -disX,0);//scrollBy右滑 第一个参数<0 , 左滑 第一个参数>0
mMoveDistanceListener.rulerMove(mTotalDis);
Log.i(TAG, "onTouchEvent: ACTION_MOVE mStartX = " + mStartX + " , mCurrentX = " + mCurrentX +" , disX = " + disX + ",mTotalDis = " + mTotalDis);
mStartX = mCurrentX;
}else{//超过边界
mTotalDis = mTotalDis < 0 ? -mMaxMoveDistance: mMaxMoveDistance;
}
break;
case MotionEvent.ACTION_UP:
isActionDown = false;
if(!mOverScroller.isFinished()){
mOverScroller.abortAnimation();
}
mVelocityTracker.computeCurrentVelocity(1000);
float Vx = mVelocityTracker.getXVelocity();//右滑>0 , 左滑<0
mFlingStartX = getScrollX();//就是view中mScrollX
if(Math.abs(mTotalDis) < mMaxMoveDistance){//未超过边界
if(Vx >= mScaledMaxFlingVelocity){
mIsMoveFinish = false;
smoothScrollTo( getScrollX() - mScaledMaxFlingVelocity / 5,0);
Log.i(TAG, "onTouchEvent: ACTION_UP Vx = " + Vx + ",mFlingStartX = " + mFlingStartX + ", scrollTo = " + (getScrollX() - mScaledMaxFlingVelocity / 5));
}else{
mIsMoveFinish = false;
smoothScrollTo((int) ( getScrollX() - Vx / 5),0);
Log.i(TAG, "onTouchEvent: ACTION_UP Vx = " + Vx + ",mFlingStartX = " + mFlingStartX + ", scrollTo = " + (getScrollX() - Vx/5));
}
}else{//未超过边界 0.1f的目的是为到了边界后,下次能进入MotionEvent.ACTION_MOVE的滑动
mTotalDis = mTotalDis < 0 ? -mMaxMoveDistance+0.1f : mMaxMoveDistance-0.1f;
}
//VelocityTracker回收
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
case MotionEvent.ACTION_CANCEL:
if(!mOverScroller.isFinished()){
mOverScroller.abortAnimation();
}
//VelocityTracker回收
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
return true;
}
private int dp2px(int mHeight) {
return (int) (getResources().getDisplayMetrics().scaledDensity * mHeight + 0.5);
}
//缓慢滑动
private void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int deltaX = destX - scrollX;
mOverScroller.startScroll(scrollX,0,deltaX,0,1000);
invalidate();
Log.i(TAG, "smoothScrollTo: scrollX = " + scrollX + " , destX = " + destX);
}
@Override
public void computeScroll() {
if(mOverScroller.computeScrollOffset()){
if(Math.abs(mTotalDis) < mMaxMoveDistance){//滑动前未超出边界
mFlingCurrentX = mOverScroller.getCurrX();
Log.i(TAG, "computeScroll: mOverScroller.getCurrX() = " + mOverScroller.getCurrX() + " ,getScrollX " + getScrollX());
float dis = mFlingStartX - mFlingCurrentX;
mTotalDis += dis;
if(Math.abs(mTotalDis) < mMaxMoveDistance){
scrollTo(mOverScroller.getCurrX(),mOverScroller.getCurrY());
// Log.i(TAG, "computeScroll_边缘滑动 : mTotalDis = " + mTotalDis + ",小于mMaxMoveDistance = " + mMaxMoveDistance);
mMoveDistanceListener.rulerMove(mTotalDis);
}else{
mTotalDis = mTotalDis < 0 ? -mMaxMoveDistance : mMaxMoveDistance;
scrollTo((int) (-mTotalDis),mOverScroller.getCurrY());//边缘滑动时位置变动
// Log.i(TAG, "computeScroll_边缘滑动 : mTotalDis = " + mTotalDis + ",大于mMaxMoveDistance = " + (Math.abs(mTotalDis) - mMaxMoveDistance));
mMoveDistanceListener.rulerMove( mTotalDis < 0 ? -mMaxMoveDistance : mMaxMoveDistance);
}
mFlingStartX = mFlingCurrentX;
postInvalidate();
mSmoothScrollFinish = true;
}else{//滑动前超出边界
mTotalDis = mTotalDis < 0 ? -mMaxMoveDistance+0.1f : mMaxMoveDistance-0.1f;
}
}else if(mSmoothScrollFinish && !mOverScroller.computeScrollOffset() && !mIsMoveFinish && !isActionDown && Math.abs(mTotalDis) < (mMaxMoveDistance-0.1f)){//最后滑动取整
mFlingCurrentX = getScrollX();
if(mTotalDis > 0){//右滑
int mWantDis = ( (int)(mTotalDis / (float)InnerRulerView.space + 0.5 )) * InnerRulerView.space;//正数的四舍五入
if(mTotalDis != mWantDis){
smoothScrollTo((int) (mFlingCurrentX + (mTotalDis - mWantDis)),0);
}
mIsMoveFinish = true;
}else{//左滑
int mWantDis = ( (int)(mTotalDis / (float)InnerRulerView.space - 0.5 )) * InnerRulerView.space;//负数的四舍五入
if(mTotalDis != mWantDis){
smoothScrollTo((int) (mFlingCurrentX + (mTotalDis - mWantDis)),0);
}
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
}
private RulerMoveDistanceListener mMoveDistanceListener;
interface RulerMoveDistanceListener{
void rulerMove(float distance);
}
public void setInnerRulerView(RulerMoveDistanceListener mMoveDistanceListener){
this.mMoveDistanceListener = mMoveDistanceListener;
}
public InnerRulerView getmRulerView() {
return mRulerView;
}
public void setmRulerView(InnerRulerView mRulerView) {
this.mRulerView = mRulerView;
}
}
总结
主要用到view滑动的知识,以及指示线保持居中。还是会用一些小问题在我的代码中,后期我会修改,并给给出github的连接