高仿京东金融的数值滚动尺

以前博客讲的大部分都是静态的自定义View的编写,其实无非就是“画画”画出一个好看的效果,而这篇博客写的是写一个动态的自定义控价,这里不仅需要"画",还要各种事件和计算,废话不说现在就讲讲自定义数值滚动尺,这个用的还是满广的,例如京东金融的通过滚动尺选择金额等,而这次就是高仿京东金融的数值滚动尺。首先看看下效果图,如下


show.gif
  • 首先先给你们各个变量的含义,以免在后面的讲解中不知变量的意思,代码如下:
//最小值
private int minValue;
//最大值
private int maxValue;
//当前值
private int currentValue;
//最小单位值
private int minUnitValue;
//最小当前值
private int minCurrentValue;
//字体大小
private int textSize;
//字体颜色
private int textColor;
//线颜色
private int dividerColor;
//指示线颜色
private int indicatrixColor;
//画线的画笔
private Paint linePaint;
//控价的宽度
private int slideRulerWidth=0;
//滑动的宽度
private int rollingWidth;
//屏幕的宽
private int wrapcontentWidth;
//屏幕的高
private int wrapcontentHeight;
//一屏显示Item
private int showItemSize;
//刻度和数值的间距
private int marginCursorData;
//长刻度的大小
private int longCursor;
//短刻度的大小
private int shortCursor;
//计算每个刻度的间距
private int marginWidth=0;
//数据回调接口
private SlideRulerDataInterface slideRulerDataInterface;
//正在滑动状态
private int isScrollingState=1;
//快速一滑
private int fastScrollState=2;
//结束滑动
private int finishScrollState=3;

private GestureDetector mDetector;
private Display display =null;
private Scroller scroller;

public SlideRuler(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context,attrs,defStyleAttr);
        display=((WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
        //屏幕宽高
        wrapcontentWidth=display.getWidth();
        wrapcontentHeight=display.getHeight();
        //初始化自定义的参数
        TypedArray typedArray=context.getTheme().obtainStyledAttributes(attrs,R.styleable.slideruler,defStyleAttr,0);
        textSize = typedArray.getDimensionPixelSize(R.styleable.slideruler_textSize,(int) TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_SP,15,getResources().getDisplayMetrics()));
        textColor=typedArray.getColor(R.styleable.slideruler_textColor,Color.DKGRAY);
        dividerColor=typedArray.getColor(R.styleable.slideruler_dividerColor,Color.BLACK);
        indicatrixColor=typedArray.getColor(R.styleable.slideruler_indicatrixColor,Color.BLACK);
        minValue=typedArray.getInteger(R.styleable.slideruler_min_value,0);
        maxValue=typedArray.getInteger(R.styleable.slideruler_max_value,199000);
        currentValue=typedArray.getInteger(R.styleable.slideruler_current_value,10000);
        minUnitValue=typedArray.getInteger(R.styleable.slideruler_min_unitValue,1000);
        minCurrentValue=typedArray.getInteger(R.styleable.slideruler_min_currentValue,1000);
        showItemSize=typedArray.getInteger(R.styleable.slideruler_show_itemSize,30);
        marginCursorData=typedArray.getDimensionPixelSize(R.styleable.slideruler_margin_cursor_data,(int)TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_SP,10,getResources().getDisplayMetrics()));
        longCursor=typedArray.getDimensionPixelSize(R.styleable.slideruler_longCursor,(int)TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_SP,25,getResources().getDisplayMetrics()));
        shortCursor=typedArray.getDimensionPixelSize(R.styleable.slideruler_shortCursor,(int)TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_SP,15,getResources().getDisplayMetrics()));

        scroller=new Scroller(context);
        mDetector=new GestureDetector(context,myGestureListener);

        //初始化Paint
        linePaint=new Paint();
        linePaint.setAntiAlias(true);
        linePaint.setTextAlign(Paint.Align.CENTER);
        linePaint.setStyle(Paint.Style.STROKE);
        linePaint.setTextSize(textSize);
        //检查当前值是不是正确值
        checkCurrentValue();
    }

  • 其次自定义View也好自定义控价也好
protected void onMeasure(int widthMeasureSpec, int heigh)

也是蛮重要的所以照例也讲讲,用来确定控件的大小,代码如下:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthModel=MeasureSpec.getMode(widthMeasureSpec);
        int heightModel=MeasureSpec.getMode(heightMeasureSpec);
        int widthSize=MeasureSpec.getSize(widthMeasureSpec);    
        int heightSize=MeasureSpec.getSize(heightMeasureSpec);
        int width;
        int height;
        if(widthModel==MeasureSpec.EXACTLY){
            width=widthSize;
        }else{
            width=wrapcontentWidth;
        }
        if(heightModel==MeasureSpec.EXACTLY){
            height=heightSize;
        }else{
            height=(getPaddingBottom()+getPaddingTop()+(wrapcontentHeight/4));
        }
        setMeasuredDimension(width,height);
    }

代码的意思也很简单,当MeasureSpec里的specMode类型是EXACTLY时,即设置了明确的值或者是MATCH_PARENT时,就直接把MeasureSpec.getSize()的值赋进去,如果不是即为WARP_CONTENT时,就直接赋给屏幕的宽高。控件的宽高都是同一样的做法。

  • 当控件大小确定之后,我们再利用
protected void onSizeChanged(int w, int h, int oldw, int oldh)

进行一些变量的赋值,代码如下:

 @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        //计算每个刻度的间距
        marginWidth=getWidth()/showItemSize;
        //开始时的距离
        rollingWidth=(int)(marginWidth*cursorNum());
        //整个控件的宽度
        slideRulerWidth=(maxValue/minUnitValue)*marginWidth;
        super.onSizeChanged(w, h, oldw, oldh);
    }
  • 到此我们就可以在onDraw(Canvas canvas)方法里画出初始的界面,而以后的动态只是通过不断的改变数值再进行绘画而已,代码如下:
@Override
protected void onDraw(Canvas canvas){
    //画最基础的两条线
    drawBaseView(canvas);
    //画初始的界面
    drawBaseLine(canvas);
}
    
//画最基础的两条线
public void drawBaseLine(Canvas canvas){
    //画中间的线
    linePaint.setColor(indicatrixColor);
    canvas.drawLine(getWidth()/2,0,getWidth()/2,getHeight(),linePaint);
    //画底部的直线
    linePaint.setColor(dividerColor);
    canvas.drawLine(0,getHeight(),slideRulerWidth,getHeight(),linePaint);
}

//画初始的界面
public void drawBaseView(Canvas canvas){
    //整数刻度的个数
    int integerWidth= (int)Math.rint((currentValue-minValue)/minUnitValue);
    //剩余不整一个刻度的数值
    int residueWidth=(currentValue-minValue)%minUnitValue;
    //开始画图的X轴位置
    int startCursor=(getWidth()/2)-(marginWidth*integerWidth)-(int)(marginWidth*(float)residueWidth/minUnitValue);
        for(int i=0;i<(maxValue/minUnitValue)+1;i++){
            float xValue=startCursor+(marginWidth*i);
            if(i%10==0){
                //画长刻度
                linePaint.setColor(textColor);
                canvas.drawText((minCurrentValue*i)+"",xValue,getHeight()-longCursor-marginCursorData,linePaint);
                linePaint.setColor(dividerColor);
                canvas.drawLine(xValue,getHeight(),xValue,getHeight()-longCursor,linePaint);
            }else{
                //画短刻度
                canvas.drawLine(xValue,getHeight(),xValue,getHeight()-shortCursor,linePaint);
            }
        }
    }    
    

在drawBaseView()方法里,也很简单,就是在二分之一宽度,画一条直线,然后在控价的底部画出宽度为整个控件的宽度的底线。接着在drawBaseView(Canvas canvas)方法里

  1. 首先用当前值(currentValue)-最小值(minValue)之后再除于最小单位值(minUnitValue)以获取整数刻度的个数
  2. 因为有余数的情况,我们再当前值(currentValue)-最小值(minValue)之后求余与最小单位值(minUnitValue)以获取余数
  3. 接着我们要获取我们画图的X轴开始的位置,因为最小值只能滑到中间,所以开始的位置为控件一半的宽度(getWidth()/2)
    减去计算每个刻度的间距(marginWidth)乘以整数刻度的个数(integerWidth)即marginWidth*integerWidth再减去余数对应所产生的X轴距离即 (int)(marginWidth*(float)residueWidth/minUnitValue)
  4. 再通过For循环刻度的个数,不同的进行刻度的绘画,当i%10==0时即为一个大的单位刻度否者为一个小的单位刻度,具体代码我上面已有注释,原理和画中间线一直就不在赘述。

到此我们就已经把自定义控价静态的部分写完了,效果如下:

img.PNG
接着我们用GestureDetector绑定手势事件,根据回调手势事件的方法来改变数据和刷新页面,在GestureDetector里,我们只会回调public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)(手指在触摸屏上滑动)和public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)(手指在触摸屏上迅速移动,并松开的动作)这两个方法就可以了。具体代码如下:

private GestureDetector.SimpleOnGestureListener myGestureListener =new  GestureDetector.SimpleOnGestureListener(){
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            //滑动刷新UI
            updateView(rollingWidth+(int)distanceX,isScrollingState);
            return true;
        }
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            //快速滑动的动画
            scroller.fling(rollingWidth,0,(int)(-velocityX/1.5),0,0,(maxValue/minUnitValue)*marginWidth,0,0);
            return true;
        }
    };
    
//动态更新滑动View
public void updateView(int srcollWidth,int action){
    if(action==isScrollingState){
        //正在滑动状态(onScroll())
        rollingWidth=srcollWidth;
        float itemNum=(float)srcollWidth/marginWidth;
        currentValue=(int)(minUnitValue*itemNum);
    }else if(action==fastScrollState){
        //快速一滑(onFling())
        rollingWidth=srcollWidth;
        int itemNum=(int)Math.rint((float)rollingWidth/marginWidth);
        currentValue=(minUnitValue*itemNum);
    }else if(action==finishScrollState){
        //结束滑动(ACTION_UP)
        int itemNum=(int)Math.rint((float)rollingWidth/marginWidth);
        currentValue=minUnitValue*itemNum;
    }
    //判断是否在最小选择值
    if(currentValue<=minCurrentValue){
        rollingWidth=(minCurrentValue/minUnitValue)*marginWidth;
        currentValue=minCurrentValue;
    }
    //判断是否在最大值
    if(currentValue>=maxValue){
        rollingWidth=marginWidth*allCursorNum();
        currentValue=maxValue;
    }
    //回调数值
    if(slideRulerDataInterface!=null){
        slideRulerDataInterface.getText(currentValue+"");
    }
    invalidate();
}
    

1.当我们滑动我们的控件是,就会回调GestureDetector里的onScroll()方法,然后rollingWidth+(int)distanceX即当前滑动的宽度(rollingWidth)加上滑动产生的宽度(distanceX)为动态产生的宽度,再除于计算每个刻度的间距(marginWidth)从而得到刻度的数量,有了刻度的数量即可得到当前值currentValue=(int)(minUnitValue*itemNum);有了当前值调用invalidate();刷新onDraw()即可完成连续滑动时动态绘制。

2.当我们快速一划时,就会回调GestureDetector里的onFling()方法,在方法里用scroller.fling(rollingWidth,0,(int)(-velocityX/1.5),0,0,(maxValue/minUnitValue)*marginWidth,0,0);以实现滑动有一个好的动画效果,此时在如下代码里:

  @Override
  public void computeScroll() {
    if(scroller.computeScrollOffset()){
        //快滑刷新UI
        updateView(scroller.getCurrX(),fastScrollState);
    }
  }

的scroller.computeScrollOffset()==true;而scroller.getCurrX()就相当于为动态产生的滑动宽度剩下的也是调用updateView()方法不断的刷新,当scroller.computeScrollOffset()==false就滑动动画结束了。

3.最后当我们滑动结束手指抬起时:

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch(event.getAction()){
            case MotionEvent.ACTION_UP:
                updateView(0,finishScrollState);
            default:
                mDetector.onTouchEvent(event);
                break;
        }
        return true;
    }

我们也要掉updateView(),以保持滑动的最后结构都指在指针上。

最后大家要深入学习的话,请看https://github.com/jack921/SlideRuler

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

推荐阅读更多精彩内容