Android 自定义 View -- 双向范围选择器,新手踏坑

风起

最近的项目需要用到一个双向范围选择器,遂自己操刀并做下记录

介绍

范围选择器要实现的功能就是进行范围选择,并提供接口向调用者暴露所选最小最大值,由于项目只是需要一个普通的范围选择器,所以并没有其他的花哨的动画特效 duang ~(为自己的技穷找一个借口)

实现

  1. 确定范围选择器需要哪些自定义属性,并在 res/values 目录下新建一个资源文件 attrs.xml (随意) 来声明我们这些属性
    <resources>
    <declare-styleable name="LcRangeBar">
    <attr name="minMark" format="integer" />
    <attr name="maxMark" format="integer" />
    <attr name="markBallRadius" format="dimension" />
    <attr name="markBallColor" format="color" />
    <attr name="unMarkLineSize" format="dimension" />
    <attr name="markLineSize" format="dimension" />
    <attr name="unMarkLineColor" format="color" />
    <attr name="markLineColor" format="color" />
    </declare-styleable>
    </resources>

  2. 接下来接是创建范围选择器,LcRangeView 继承自 View ,并实现 LcRangeView 的三个构造方法
    public LcRangeBar(Context context) {
    super(context);
    initAttrs(null);
    }
    public LcRangeBar(Context context, AttributeSet attrs) {
    super(context, attrs);
    initAttrs(attrs);
    }
    public LcRangeBar(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    initAttrs(attrs);
    }

  3. 之前我们定义选择器所需要的属性,那么现在我就要在 View 中拿到这些属性的赋值并处理,当然为了避免调用者没有给这之中的哪个属性赋值而产生绘图显示异常,我们也默认得给这些属性默认值
    private void initAttrs(AttributeSet attrs) {
    if (attrs != null) {
    TypedArray ta = getContext().obtainStyledAttributes(attrs,
    R.styleable.LcRangeBar, 0, 0);
    minMark = ta.getInt(R.styleable.LcRangeBar_minMark,
    DEFAULT_MIN_MARK);
    maxMark = ta.getInt(R.styleable.LcRangeBar_maxMark,
    DEFAULT_MAX_MARK);
    markBallColor = ta.getColor(R.styleable.LcRangeBar_markBallColor,
    DEFAULT_MARK_BALL_COLOR);
    markLineColor = ta.getColor(R.styleable.LcRangeBar_markLineColor,
    DEFAULT_MARK_LINE_COLOR);
    unMarkLineColor = ta.getColor(
    R.styleable.LcRangeBar_unMarkLineColor,
    DEFAULT_UNMARK_LINE_COLOR);
    markBallRadius = (int) ta.getDimension(
    R.styleable.LcRangeBar_markBallRadius,
    dp2px(DEFAULT_MARK_BALL_RADIUS));
    markLineSize = (int) ta.getDimension(
    R.styleable.LcRangeBar_markLineSize,
    dp2px(DEFAULT_MARK_LINE_SIZE));
    unMarkLineSize = (int) ta.getDimension(
    R.styleable.LcRangeBar_unMarkLineSize,
    dp2px(DEFAULT_UNMARK_LINE_SIZE));
    ta.recycle();
    }
    markRange = maxMark - minMark;
    }

  4. 拿到了绘图所需要的数据,接下来就是测量选择器的大小,重写 onMeasure() 方法。首先试想一下,自适应情况控件的宽高应该是多大,宽的话我们就填充完屏幕,高呢,选择球的高度,外部大小决定好了就该考虑一下内部的测量,标刻线应该为控件正中间位置,即两个球心的连接线,宽则为控件左右边各空出一个球的半径位置以保证球在最左或最右显示不完整。
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    int expectedWidth = dp2px(200);
    int expectedHeight = dp2px(30);
    int finalWidth = expectedWidth;
    int finalHeight = expectedHeight;

         if (widthMode == MeasureSpec.EXACTLY) {
             finalWidth = widthSize; 
         } else if (widthMode == MeasureSpec.AT_MOST) {
             finalWidth = expectedWidth;
         }
         if (heightMode == MeasureSpec.EXACTLY) {
             finalHeight = heightSize;
         } else if (heightMode == MeasureSpec.AT_MOST) {
             finalHeight = markBallRadius;
         }
         
         mLineLength = (finalWidth - markBallRadius * 2);
         mMidY = finalHeight / 2;
         Log.d("测试", "看看y"+mMidY);
         mLineStartX = markBallRadius;
         mLineEndX = mLineLength + markBallRadius;
         mMinPosition = mLineStartX;
         mMaxPosition = mLineEndX;
     }
    
  5. 测量好了就该绘图了,重写 onDraw() 方法,我们要明确的画图的顺序,标准刻度线 -> 选择刻度线 -> 选择球,想好了怎么画就该准备笔 (paint) 和 (canvas) ,绘制所需的参数在前面已经定义过了,形状一出立马感觉成功了一半,

     protected void onDraw(Canvas canvas) {
         super.onDraw(canvas);
         drawUnMarkLine(canvas);
         drawMarkLine(canvas);
         drawMarkBalls(canvas);  
     }
    
     private void drawMarkBalls(Canvas canvas) {
         mPaint.setColor(markBallColor);
         canvas.drawCircle(mMinPosition, mMidY, markBallRadius, mPaint);
         canvas.drawCircle(mMaxPosition, mMidY, markBallRadius, mPaint);
     }
    
     private void drawMarkLine(Canvas canvas) {
         mPaint.setColor(markLineColor);
         mPaint.setStrokeWidth(markLineSize);
         canvas.drawLine(mMinPosition, mMidY, mMaxPosition, mMidY, mPaint);
     }
    
     private void drawUnMarkLine(Canvas canvas) {
         mPaint.setColor(unMarkLineColor);
         mPaint.setStrokeWidth(unMarkLineSize);
         canvas.drawLine(mLineStartX, mMidY, mLineEndX, mMidY, mPaint);
     }
    
  6. 图形已经出现,我们目前要操作的是两个球,那么我们就得判断球是否被触摸到,我这里触摸的范围是刚好装下球的正方形,你也适当得增大触控面积(如果你的球需要绘制很小的话)

     private boolean isTouchingMaxBall(MotionEvent event) {
         return event.getX() > mMaxPosition - markBallRadius
                 && event.getX() < mMaxPosition + markBallRadius
                 && event.getY() > mMidY - markBallRadius
                 && event.getY() < mMidY + markBallRadius;
     }
    
     private boolean isTouchingMinBall(MotionEvent event) {
         return event.getX() > mMinPosition - markBallRadius
                 && event.getX() < mMinPosition + markBallRadius
                 && event.getY() > mMidY - markBallRadius
                 && event.getY() < mMidY + markBallRadius;
     }
    
  7. 写好了判断,接下来就是实现拖动效果了,当手指按下时,就判断是否触摸到了球,触摸了那个球,记录下状态;当手指抬起时,都将触摸状态置为 false ;当手指滑动时,根据触摸状态执行相应的的滑动

     @Override
     public boolean onTouchEvent(MotionEvent event) {
         switch (event.getAction()) {
         case MotionEvent.ACTION_DOWN:
             if (isTouchingMinBall(event)) {
                 isOnMinBall = true;
             } else if (isTouchingMaxBall(event)) {
                 isOnMaxBall = true;
             }
             break;
         case MotionEvent.ACTION_MOVE:
             if (isOnMinBall) {
                 jumpToMin(event);
             }
             if (isOnMaxBall) {
                 jumpToMax(event);
             }
             break;
    
         case MotionEvent.ACTION_UP:
             if (isOnMinBall) {
                 isOnMinBall = false;
             }
             if (isOnMaxBall) {
                 isOnMaxBall = false;
             }
             break;
         }
    
         return true;
     }
    
  8. 继续来处理滑动逻辑,我们要先知道球的滑动范围,
    minBall 的滑动范围为 标准线的起点 -- maxBall 的球心位置,
    maxBall 的滑动位置为 maxBall 的球心位置 -- 标准线的终点。
    (如果需要让两个球不重叠,可以边界增加一个球的宽度)
    确定球的新位置后,调用 invalidate() 进行重绘
    当确定为正在移动球的时候,即使脱离本控件的的范围一样可以更新视图

     private void moveToMinPosition(MotionEvent event) {
         if (event.getX() < mMaxPosition && event.getX() >= mLineStartX) {
             mMinPosition = (int) event.getX();
             invalidate();
             /** 配合 10 一起看,这个必须判断是否为空,如果调用者不监听会导致空指针异常
             if (mRangeChangeListener != null) {
                 mRangeChangeListener.onMinChange(Math
                         .round((float) (mMinPosition - mLineStartX)
                                 / mLineLength * markRange));
             }
             **/
         }
     }
    
     private void moveToMaxPosition(MotionEvent event) {
         if (event.getX() > mMinPosition && event.getX() <= mLineEndX) {
             mMaxPosition = (int) event.getX();
             invalidate();
             /** 配合 10 一起看
             if (mRangeChangeListener != null) {
                 mRangeChangeListener.onMaxChange(Math
                         .round((float) (mMaxPosition - mLineStartX)
                                 / mLineLength * markRange));
             }
             **/
         }
     }
    
  9. 现在界面的雏形已经出现了,接下来我们要根据滑动来实时更新我们的范围值,一开始我们就拿到了总范围值,然后根据滑动比例获取范围值

    计算公式

    • min 值:(minBall 位置 - 标准线起点)/ 标准线长度 * 总范围值

    • max 值:(maxBall 位置 - 标准线起点)/ 标准线长度 * 总范围值

  10. 范围值我们拿到了,最后一步结束范围值提供给调用者,这个部分大家都很熟悉了,直接贴

    public interface RangeChangeListener {
        void onMinChange(int minValue);
        void onMaxChange(int maxValue);
    }
    
    public void setRangeChangeListener(RangeChangeListener rangeChangeListener) {
        mRangeChangeListener = rangeChangeListener;
    }
    

总结

到此为止一个简单的范围选择器就完成了,由于最近还在赶其他项目,所以目前先这么简陋的吧,如果有其他需要还可以更加完善,如多点操作,在标准线上点击实现球的位置跳转,变化动画等。没什么技术含量,纯粹写写文记录开发经历而已。(有空补上源码图片)

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

推荐阅读更多精彩内容