仿实现尺子滑动

实现的尺子滑动的效果如下:

仿实现尺子滑动_第1张图片
尺子.gif

使用到知识点:view的滑动,以及view的绘画尺子。

view的滑动相关知识介绍

先需要了解如何实现view的滑动,比较常用的有如下几种方式:

  1. 通过view本身的scrollTo/scrollBy方法实现滑动;

  2. 通过动画的方式实现view的滑动;

  3. 通过改变view的LayoutParams的参数,重新布局,实现滑动。

实例中尺子的滑动就是通过第一种方式。scrollTo/scrollBy只能将view的内容滑动(确切的说是只改变view中的mScrollX和mScrollY),不改变view的布局。scrollBy方法会间接调用scrollTo(mScrollX+X,mScrollY+Y);通过getCurrX()和getCurrY()可以获得view中mScrollX和mScrollY的参数。

如图是mScrollX和mScrollY的变化规律,蓝色阴影部分为内容,绿色的边框是布局。
仿实现尺子滑动_第2张图片
坐标滑动变化.png

三种滑动方式的比较:
scrollTo/scrollBy的方式:不影响点击事件,只能滑动view的内容,不能滑动view本身。
动画的方式:除缺少交互外,3.0以上的属性动画没有明显确定。3.0以下存在点击事件在原处的问题。
改变布局参数方式:没有明显缺点,就是相对比较麻烦。

因为使用scrollTo/scrollBy的方式来完成,因此需要InnerRulerView类作为view的内容。OutRulerView继承ViewGroup作为包裹InnerRulerView的壳。

解析InnerRulerView类

InnerRulerView的具体作用:

  1. 绘制尺子的刻度,刻度线,指示线。
  2. 提供具体的刻度线所在的数值。
  3. 根据滑动距离,保持指示线居中

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的左下角的位置。


仿实现尺子滑动_第3张图片
1.1.png

解析OutRulerView类

OutRulerView的具体作用:

  1. 实现innerRulerView的滑动(MotionEvent.ACTION_MOVE情况下的滑动,以及抬起手指后的惯性滑动)
  2. 当滑动停止后,当指示线不在刻度线上,需要滑动到最近的刻度线边上。
  3. 记录下滑动距离,保证不滑出刻度范围,保证指示线始终保持居中。通过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的连接

你可能感兴趣的:(仿实现尺子滑动)