仿实现尺子滑动

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

尺子.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的变化规律,蓝色阴影部分为内容,绿色的边框是布局。
坐标滑动变化.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的左下角的位置。


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的连接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,039评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,223评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,916评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,009评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,030评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,011评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,934评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,754评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,202评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,433评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,590评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,321评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,917评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,568评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,738评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,583评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,482评论 2 352

推荐阅读更多精彩内容