当前位置 : 主页 > 手机开发 > android >

Android开发双向滑动选择器范围SeekBar实现

来源:互联网 收集:自由互联 发布时间:2023-02-01
目录 一、概述 二、实现 2.1 首先看我们自定义View的全部代码 2.2 实现流程 初始化 计算宽高 onDraw 绘制进度条 事件监听 三、使用 布局文件 布局文件(有刻度线) 布局文件(无刻度线)
目录
  • 一、概述
  • 二、实现
    • 2.1 首先看我们自定义View的全部代码
    • 2.2 实现流程
      • 初始化
      • 计算宽高
      • onDraw 绘制进度条
      • 事件监听
  • 三、使用 布局文件
    • 布局文件(有刻度线)
      • 布局文件(无刻度线)
      • 四、后记

        一、概述

        之前公司app里面有个功能是一个可以双向滑动的范围选择器,我在网上百度过一些实现方法,感觉各有利弊吧,但是都不太适合我们的需求。所以站在巨人的肩膀上,通过自定义View实现了一个可以适用于绝大多数情况的范围选择器来供大家使用。

        首先,看效果图:

        我对该范围选择器的属性进行了一些封装,例如我们可以自由控制我们的范围选择器是否显示刻度、刻度的长度、我们选择器上每个值的单位、最大值最小值、游标(即那个圆形图片)的样式、大小、选择器内部范围颜色以及外部颜色等等很多属性。更多玩法还请下载我的Demo体验,项目地址在文末。

        二、实现

        2.1 首先看我们自定义View的全部代码

         public class DoubleSlideSeekBar extends View {
            /**
             * 线条(进度条)的宽度
             */
            private int lineWidth;
            /**
             * 线条(进度条)的长度
             */
            private int lineLength = 400;
            /**
             * 字所在的高度 100$
             */
            private int textHeight;
            /**
             * 游标 图片宽度
             */
            private int imageWidth;
            /**
             * 游标 图片高度
             */
            private int imageHeight;
            /**
             * 是否有刻度线
             */
            private boolean hasRule;
            /**
             * 左边的游标是否在动
             */
            private boolean isLowerMoving;
            /**
             * 右边的游标是否在动
             */
            private boolean isUpperMoving;
            /**
             * 字的大小 100$
             */
            private int textSize;
            /**
             * 字的颜色 100$
             */
            private int textColor;
            /**
             * 两个游标内部 线(进度条)的颜色
             */
            private int inColor = Color.BLUE;
            /**
             * 两个游标外部 线(进度条)的颜色
             */
            private int outColor = Color.BLUE;
            /**
             * 刻度的颜色
             */
            private int ruleColor = Color.BLUE;
            /**
             * 刻度上边的字 的颜色
             */
            private int ruleTextColor = Color.BLUE;
            /**
             * 左边图标的图片
             */
            private Bitmap bitmapLow;
            /**
             * 右边图标 的图片
             */
            private Bitmap bitmapBig;
            /**
             * 左边图标所在X轴的位置
             */
            private int slideLowX;
            /**
             * 右边图标所在X轴的位置
             */
            private int slideBigX;
            /**
             * 图标(游标) 高度
             */
            private int bitmapHeight;
            /**
             * 图标(游标) 宽度
             */
            private int bitmapWidth;
            /**
             * 加一些padding 大小酌情考虑 为了我们的自定义view可以显示完整
             */
            private int paddingLeft = 100;
            private int paddingRight = 100;
            private int paddingTop = 50;
            private int paddingBottom = 10;
            /**
             * 线(进度条) 开始的位置
             */
            private int lineStart = paddingLeft;
            /**
             * 线的Y轴位置
             */
            private int lineY;
            /**
             * 线(进度条)的结束位置
             */
            private int lineEnd = lineLength + paddingLeft;
            /**
             * 选择器的最大值
             */
            private int bigValue = 100;
            /**
             * 选择器的最小值
             */
            private int smallValue = 0;
            /**
             * 选择器的当前最小值
             */
            private float smallRange;
            /**
             * 选择器的当前最大值
             */
            private float bigRange;
            /**
             * 单位 元
             */
            private String unit = " ";
            /**
             * 单位份数
             */
            private int equal = 20;
            /**
             * 刻度单位 $
             */
            private String ruleUnit = " ";
            /**
             * 刻度上边文字的size
             */
            private int ruleTextSize = 20;
            /**
             * 刻度线的高度
             */
            private int ruleLineHeight = 20;
            private Paint linePaint;
            private Paint bitmapPaint;
            private Paint textPaint;
            private Paint paintRule;
            public DoubleSlideSeekBar(Context context) {
                this(context, null);
            }
            public DoubleSlideSeekBar(Context context, @Nullable AttributeSet attrs) {
                this(context, attrs, 0);
            }
            public DoubleSlideSeekBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
                super(context, attrs, defStyleAttr);
                TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DoubleSlideSeekBar, defStyleAttr, 0);
                int size = typedArray.getIndexCount();
                for (int i = 0; i < size; i++) {
                    int type = typedArray.getIndex(i);
                    switch (type) {
                        case R.styleable.DoubleSlideSeekBar_inColor:
                            inColor = typedArray.getColor(type, Color.BLACK);
                            break;
                        case R.styleable.DoubleSlideSeekBar_lineHeight:
                            lineWidth = (int) typedArray.getDimension(type, dip2px(getContext(), 10));
                            break;
                        case R.styleable.DoubleSlideSeekBar_outColor:
                            outColor = typedArray.getColor(type, Color.YELLOW);
                            break;
                        case R.styleable.DoubleSlideSeekBar_textColor:
                            textColor = typedArray.getColor(type, Color.BLUE);
                            break;
                        case R.styleable.DoubleSlideSeekBar_textSize:
                            textSize = typedArray.getDimensionPixelSize(type, (int) TypedValue.applyDimension(
                                    TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
                            break;
                        case R.styleable.DoubleSlideSeekBar_imageLow:
                            bitmapLow = BitmapFactory.decodeResource(getResources(), typedArray.getResourceId(type, 0));
                            break;
                        case R.styleable.DoubleSlideSeekBar_imageBig:
                            bitmapBig = BitmapFactory.decodeResource(getResources(), typedArray.getResourceId(type, 0));
                            break;
                        case R.styleable.DoubleSlideSeekBar_imageheight:
                            imageHeight = (int) typedArray.getDimension(type, dip2px(getContext(), 20));
                            break;
                        case R.styleable.DoubleSlideSeekBar_imagewidth:
                            imageWidth = (int) typedArray.getDimension(type, dip2px(getContext(), 20));
                            break;
                        case R.styleable.DoubleSlideSeekBar_hasRule:
                            hasRule = typedArray.getBoolean(type, false);
                            break;
                        case R.styleable.DoubleSlideSeekBar_ruleColor:
                            ruleColor = typedArray.getColor(type, Color.BLUE);
                            break;
                        case R.styleable.DoubleSlideSeekBar_ruleTextColor:
                            ruleTextColor = typedArray.getColor(type, Color.BLUE);
                            break;
                        case R.styleable.DoubleSlideSeekBar_unit:
                            unit = typedArray.getString(type);
                            break;
                        case R.styleable.DoubleSlideSeekBar_equal:
                            equal = typedArray.getInt(type, 10);
                            break;
                        case R.styleable.DoubleSlideSeekBar_ruleUnit:
                            ruleUnit = typedArray.getString(type);
                            break;
                        case R.styleable.DoubleSlideSeekBar_ruleTextSize:
                            ruleTextSize = typedArray.getDimensionPixelSize(type, (int) TypedValue.applyDimension(
                                    TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
                            break;
                        case R.styleable.DoubleSlideSeekBar_ruleLineHeight:
                            ruleLineHeight = (int) typedArray.getDimension(type, dip2px(getContext(), 10));
                            break;
                        case R.styleable.DoubleSlideSeekBar_bigValue:
                            bigValue = typedArray.getInteger(type, 100);
                            break;
                        case R.styleable.DoubleSlideSeekBar_smallValue:
                            smallValue = typedArray.getInteger(type, 100);
                            break;
                        default:
                            break;
                    }
                }
                typedArray.recycle();
                init();
            }
            private void init() {
                /**游标的默认图*/
                if (bitmapLow == null) {
                    bitmapLow = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
                }
                if (bitmapBig == null) {
                    bitmapBig = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
                }
                /**游标图片的真实高度 之后通过缩放比例可以把图片设置成想要的大小*/
                bitmapHeight = bitmapLow.getHeight();
                bitmapWidth = bitmapLow.getWidth();
                // 设置想要的大小
                int newWidth = imageWidth;
                int newHeight = imageHeight;
                // 计算缩放比例
                float scaleWidth = ((float) newWidth) / bitmapWidth;
                float scaleHeight = ((float) newHeight) / bitmapHeight;
                Matrix matrix = new Matrix();
                matrix.postScale(scaleWidth, scaleHeight);
                /**缩放图片*/
                bitmapLow = Bitmap.createBitmap(bitmapLow, 0, 0, bitmapWidth, bitmapHeight, matrix, true);
                bitmapBig = Bitmap.createBitmap(bitmapBig, 0, 0, bitmapWidth, bitmapHeight, matrix, true);
                /**重新获取游标图片的宽高*/
                bitmapHeight = bitmapLow.getHeight();
                bitmapWidth = bitmapLow.getWidth();
                /**初始化两个游标的位置*/
                slideLowX = lineStart;
                slideBigX = lineEnd;
                smallRange = smallValue;
                bigRange = bigValue;
                if (hasRule) {
                    //有刻度时 paddingTop 要加上(text高度)和(刻度线高度加刻度线上边文字的高度和) 之间的最大值
                    paddingTop = paddingTop + Math.max(textSize, ruleLineHeight + ruleTextSize);
                } else {
                    //没有刻度时 paddingTop 加上 text的高度
                    paddingTop = paddingTop + textSize;
                }
            }
            @Override
            protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
                int width = getMyMeasureWidth(widthMeasureSpec);
                int height = getMyMeasureHeight(heightMeasureSpec);
                setMeasuredDimension(width, height);
            }
            private int getMyMeasureHeight(int heightMeasureSpec) {
                int mode = MeasureSpec.getMode(heightMeasureSpec);
                int size = MeasureSpec.getSize(heightMeasureSpec);
                if (mode == MeasureSpec.EXACTLY) {
                    // matchparent 或者 固定大小 view最小应为 paddingBottom + paddingTop + bitmapHeight + 10 否则显示不全
                    size = Math.max(size, paddingBottom + paddingTop + bitmapHeight + 10);
                } else {
                    //wrap content
                    int height = paddingBottom + paddingTop + bitmapHeight + 10;
                    size = Math.min(size, height);
                }
                return size;
            }
            private int getMyMeasureWidth(int widthMeasureSpec) {
                int mode = MeasureSpec.getMode(widthMeasureSpec);
                int size = MeasureSpec.getSize(widthMeasureSpec);
                if (mode == MeasureSpec.EXACTLY) {
                    size = Math.max(size, paddingLeft + paddingRight + bitmapWidth * 2);
                } else {
                    //wrap content
                    int width = paddingLeft + paddingRight + bitmapWidth * 2;
                    size = Math.min(size, width);
                }
                // match parent 或者 固定大小 此时可以获取线(进度条)的长度
                lineLength = size - paddingLeft - paddingRight - bitmapWidth;
                //线(进度条)的结束位置
                lineEnd = lineLength + paddingLeft + bitmapWidth / 2;
                //线(进度条)的开始位置
                lineStart = paddingLeft + bitmapWidth / 2;
                //初始化 游标位置
                slideBigX = lineEnd;
                slideLowX = lineStart;
                return size;
            }
            @Override
            protected void onDraw(Canvas canvas) {
                super.onDraw(canvas);
                // Y轴 坐标
                lineY = getHeight() - paddingBottom - bitmapHeight / 2;
                // 字所在高度 100$
                textHeight = lineY - bitmapHeight / 2 - 10;
                //是否画刻度
                if (hasRule) {
                    drawRule(canvas);
                }
                if (linePaint == null) {
                    linePaint = new Paint();
                }
                //画内部线
                linePaint.setAntiAlias(true);
                linePaint.setStrokeWidth(lineWidth);
                linePaint.setColor(inColor);
                linePaint.setStrokeCap(Paint.Cap.ROUND);
                canvas.drawLine(slideLowX, lineY, slideBigX, lineY, linePaint);
                linePaint.setColor(outColor);
                linePaint.setStrokeCap(Paint.Cap.ROUND);
                //画 外部线
                canvas.drawLine(lineStart, lineY, slideLowX, lineY, linePaint);
                canvas.drawLine(slideBigX, lineY, lineEnd, lineY, linePaint);
                //画游标
                if (bitmapPaint == null) {
                    bitmapPaint = new Paint();
                }
                canvas.drawBitmap(bitmapLow, slideLowX - bitmapWidth / 2, lineY - bitmapHeight / 2, bitmapPaint);
                canvas.drawBitmap(bitmapBig, slideBigX - bitmapWidth / 2, lineY - bitmapHeight / 2, bitmapPaint);
                //画 游标上边的字
                if (textPaint == null) {
                    textPaint = new Paint();
                }
                textPaint.setColor(textColor);
                textPaint.setTextSize(textSize);
                textPaint.setAntiAlias(true);
                canvas.drawText(String.format("%.0f" + unit, smallRange), slideLowX - bitmapWidth / 2, textHeight, textPaint);
                canvas.drawText(String.format("%.0f" + unit, bigRange), slideBigX - bitmapWidth / 2, textHeight, textPaint);
            }
            @Override
            public boolean onTouchEvent(MotionEvent event) {
                //事件机制
                super.onTouchEvent(event);
                float nowX = event.getX();
                float nowY = event.getY();
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        //按下 在线(进度条)范围上
                        boolean rightY = Math.abs(nowY - lineY) < bitmapHeight / 2;
                        //按下 在左边游标上
                        boolean lowSlide = Math.abs(nowX - slideLowX) < bitmapWidth / 2;
                        //按下 在右边游标上
                        boolean bigSlide = Math.abs(nowX - slideBigX) < bitmapWidth / 2;
                        if (rightY && lowSlide) {
                            isLowerMoving = true;
                        } else if (rightY && bigSlide) {
                            isUpperMoving = true;
                            //点击了游标外部 的线上
                        } else if (nowX >= lineStart && nowX <= slideLowX - bitmapWidth / 2 && rightY) {
                            slideLowX = (int) nowX;
                            updateRange();
                            postInvalidate();
                        } else if (nowX <= lineEnd && nowX >= slideBigX + bitmapWidth / 2 && rightY) {
                            slideBigX = (int) nowX;
                            updateRange();
                            postInvalidate();
                        }
                        break;
                    case MotionEvent.ACTION_MOVE:
                        //左边游标是运动状态
                        if (isLowerMoving) {
                            //当前 X坐标在线上 且在右边游标的左边
                            if (nowX <= slideBigX - bitmapWidth && nowX >= lineStart - bitmapWidth / 2) {
                                slideLowX = (int) nowX;
                                if (slideLowX < lineStart) {
                                    slideLowX = lineStart;
                                }
                                //更新进度
                                updateRange();
                                postInvalidate();
                            }
                        } else if (isUpperMoving) {
                            //当前 X坐标在线上 且在左边游标的右边
                            if (nowX >= slideLowX + bitmapWidth && nowX <= lineEnd + bitmapWidth / 2) {
                                slideBigX = (int) nowX;
                                if (slideBigX > lineEnd) {
                                    slideBigX = lineEnd;
                                }
                                //更新进度
                                updateRange();
                                postInvalidate();
                            }
                        }
                        break;
                    //手指抬起
                    case MotionEvent.ACTION_UP:
                        isUpperMoving = false;
                        isLowerMoving = false;
                        break;
                    default:
                        break;
                }
                return true;
            }
            private void updateRange() {
                //当前 左边游标数值
                smallRange = computRange(slideLowX);
                //当前 右边游标数值
                bigRange = computRange(slideBigX);
                //接口 实现值的传递
                if (onRangeListener != null) {
                    onRangeListener.onRange(smallRange, bigRange);
                }
            }
            /**
             * 获取当前值
             */
            private float computRange(float range) {
                return (range - lineStart) * (bigValue - smallValue) / lineLength + smallValue;
            }
            public int dip2px(Context context, float dpValue) {
                final float scale = context.getResources().getDisplayMetrics().density;
                return (int) (dpValue * scale + 0.5f);
            }
            /**
             * 画刻度
             */
            protected void drawRule(Canvas canvas) {
                if (paintRule == null) {
                    paintRule = new Paint();
                }
                paintRule.setStrokeWidth(1);
                paintRule.setTextSize(ruleTextSize);
                paintRule.setTextAlign(Paint.Align.CENTER);
                paintRule.setAntiAlias(true);
                //遍历 equal份,画刻度
                for (int i = smallValue; i <= bigValue; i += (bigValue - smallValue) / equal) {
                    float degX = lineStart + i * lineLength / (bigValue - smallValue);
                    int degY = lineY - ruleLineHeight;
                    paintRule.setColor(ruleColor);
                    canvas.drawLine(degX, lineY, degX, degY, paintRule);
                    paintRule.setColor(ruleTextColor);
                    canvas.drawText(String.valueOf(i) + ruleUnit, degX, degY, paintRule);
                }
            }
            /**
             * 写个接口 用来传递最大最小值
             */
            public interface onRangeListener {
                void onRange(float low, float big);
            }
            private onRangeListener onRangeListener;
            public void setOnRangeListener(DoubleSlideSeekBar.onRangeListener onRangeListener) {
                this.onRangeListener = onRangeListener;
            }
        }

        2.2 实现流程

        代码的注解很详细,下面我们来进一步分析此自定义view的实现步骤。

        首先,我们要自定义一些属性,在res/values文件夹下创建文件attrs,内容如下:

         <resources>
            <!--线(进度条)宽度-->
            <attr name="lineHeight" format="dimension" />
            <!--字的大小 100元-->
            <attr name="textSize" format="dimension" />
            <!--字的颜色 100元-->
            <attr name="textColor" format="color" />
            <!--两个游标内部 线(进度条)的颜色-->
            <attr name="inColor" format="color" />
            <!--两个游标外部 线(进度条)的颜色-->
            <attr name="outColor" format="color" />
            <!--左边图标的图片-->
            <attr name="imageLow" format="reference"/>
            <!--右边图标 的图片-->
            <attr name="imageBig" format="reference"/>
            <!--游标 图片宽度-->
            <attr name="imagewidth" format="dimension" />
            <!--游标 图片高度-->
            <attr name="imageheight" format="dimension" />
            <!--是否有刻度线-->
            <attr name="hasRule" format="boolean" />
            <!--刻度的颜色-->
            <attr name="ruleColor" format="color" />
            <!--刻度上边的字 的颜色-->
            <attr name="ruleTextColor" format="color" />
            <!--单位 元-->
            <attr name="unit" format="string"/>
            <!--单位份数-->
            <attr name="equal" format="integer"/>
            <!--刻度单位 $-->
            <attr name="ruleUnit" format="string"/>
            <!--刻度上边文字的size-->
            <attr name="ruleTextSize" format="dimension" />
            <!--刻度线的高度-->
            <attr name="ruleLineHeight" format="dimension" />
            <!--选择器的最大值-->
            <attr name="bigValue" format="integer"/>
            <!--选择器的最小值-->
            <attr name="smallValue" format="integer"/>
            <declare-styleable name="DoubleSlideSeekBar">
                <attr name="lineHeight" />
                <attr name="textSize" />
                <attr name="textColor" />
                <attr name="inColor" />
                <attr name="outColor" />
                <attr name="imageLow"/>
                <attr name="imageBig"/>
                <attr name="imagewidth" />
                <attr name="imageheight" />
                <attr name="hasRule" />
                <attr name="ruleColor" />
                <attr name="ruleTextColor" />
                <attr name="unit" />
                <attr name="equal" />
                <attr name="ruleUnit" />
                <attr name="ruleTextSize" />
                <attr name="ruleLineHeight" />
                <attr name="bigValue" />
                <attr name="smallValue" />
            </declare-styleable>
        </resources>

        我们要先确定我们要控制的属性。综合下来,我们需要控制进度条的宽度(高)、颜色、游标上边字的大小、刻度上边字的大小、颜色、是否有游标等等功能,所有属性及说明如下(可以酌情定制):

        xml属性值解释lineHeightdimension控制我们线(进度条)的宽(高)度(例20dp)textSizedimension游标上边字的大小(例16sp)textColorcolor游标上边字的颜色 (例#e40627)inColorcolor两个游标之间进度条的颜色 (例#e40627)outColorcolor两个游标外部(游标到进度条两端)进度条的颜色 (例#e40627)imageLowreference左边游标的图片 (例@mipmap/imgv_slide)imageBigreference右边游标的图片 (例@mipmap/imgv_slide)imagewidthdimension游标图片的宽度 (例20dp)imagewidthdimension游标图片的高度 (例20dp)hasRuleboolean是否有刻度线(例 true or false)ruleColorcolor刻度线的颜色 (例#e40627)ruleTextColorcolor刻度线上边的字的颜色 (例#e40627)unitstring单位 (例 元)equalinteger单位份数,把全部数据分成equal份(例smallValue是0,bigValue是100,equal是10,则每个刻度大小为(100-0)/10 =10)ruleUnitstring刻度上边文字的单位 (例 $)ruleTextSizedimension刻度上边文字的大小 (例20sp)ruleLineHeightdimension刻度线高度(例16dp)bigValueinteger选择器的最大值 (例 100)smallValueinteger选择器的最小值 (例 0)

        之后在自定义View里面获取我们定义的属性:

        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DoubleSlideSeekBar, defStyleAttr, 0);
                int size = typedArray.getIndexCount();
                for (int i = 0; i < size; i++) {
                    int type = typedArray.getIndex(i);
                    switch (type) {
                        case R.styleable.DoubleSlideSeekBar_inColor:
                            inColor = typedArray.getColor(type, Color.BLACK);
                            break;
                        case R.styleable.DoubleSlideSeekBar_lineHeight:
                            lineWidth = (int) typedArray.getDimension(type, dip2px(getContext(), 10));
                            break;
                        case R.styleable.DoubleSlideSeekBar_outColor:
                            outColor = typedArray.getColor(type, Color.YELLOW);
                            break;
                        case R.styleable.DoubleSlideSeekBar_textColor:
                            textColor = typedArray.getColor(type, Color.BLUE);
                            break;
                        case R.styleable.DoubleSlideSeekBar_textSize:
                            textSize = typedArray.getDimensionPixelSize(type, (int) TypedValue.applyDimension(
                                    TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
                            break;
                        case R.styleable.DoubleSlideSeekBar_imageLow:
                            bitmapLow = BitmapFactory.decodeResource(getResources(), typedArray.getResourceId(type, 0));
                            break;
                        case R.styleable.DoubleSlideSeekBar_imageBig:
                            bitmapBig = BitmapFactory.decodeResource(getResources(), typedArray.getResourceId(type, 0));
                            break;
                        case R.styleable.DoubleSlideSeekBar_imageheight:
                            imageHeight = (int) typedArray.getDimension(type, dip2px(getContext(), 20));
                            break;
                        case R.styleable.DoubleSlideSeekBar_imagewidth:
                            imageWidth = (int) typedArray.getDimension(type, dip2px(getContext(), 20));
                            break;
                        case R.styleable.DoubleSlideSeekBar_hasRule:
                            hasRule = typedArray.getBoolean(type, false);
                            break;
                        case R.styleable.DoubleSlideSeekBar_ruleColor:
                            ruleColor = typedArray.getColor(type, Color.BLUE);
                            break;
                        case R.styleable.DoubleSlideSeekBar_ruleTextColor:
                            ruleTextColor = typedArray.getColor(type, Color.BLUE);
                            break;
                        case R.styleable.DoubleSlideSeekBar_unit:
                            unit = typedArray.getString(type);
                            break;
                        case R.styleable.DoubleSlideSeekBar_equal:
                            equal = typedArray.getInt(type, 10);
                            break;
                        case R.styleable.DoubleSlideSeekBar_ruleUnit:
                            ruleUnit = typedArray.getString(type);
                            break;
                        case R.styleable.DoubleSlideSeekBar_ruleTextSize:
                            ruleTextSize = typedArray.getDimensionPixelSize(type, (int) TypedValue.applyDimension(
                                    TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
                            break;
                        case R.styleable.DoubleSlideSeekBar_ruleLineHeight:
                            ruleLineHeight = (int) typedArray.getDimension(type, dip2px(getContext(), 10));
                            break;
                        case R.styleable.DoubleSlideSeekBar_bigValue:
                            bigValue = typedArray.getInteger(type, 100);
                            break;
                        case R.styleable.DoubleSlideSeekBar_smallValue:
                            smallValue = typedArray.getInteger(type, 100);
                            break;
                        default:
                            break;
                    }
                }
                typedArray.recycle();

        由于我们要使用的是三个参数的构造器,所以对应一参二参的构造器进行如下设置:

         public DoubleSlideSeekBar(Context context) {
                this(context, null);
            }
            public DoubleSlideSeekBar(Context context, @Nullable AttributeSet attrs) {
                this(context, attrs, 0);
            }

        初始化

         /**游标图片的真实高度 之后通过缩放比例可以把图片设置成想要的大小*/
                bitmapHeight = bitmapLow.getHeight();
                bitmapWidth = bitmapLow.getWidth();
                // 设置想要的大小
                int newWidth = imageWidth;
                int newHeight = imageHeight;
                // 计算缩放比例
                float scaleWidth = ((float) newWidth) / bitmapWidth;
                float scaleHeight = ((float) newHeight) / bitmapHeight;
                Matrix matrix = new Matrix();
                matrix.postScale(scaleWidth, scaleHeight);
                /**缩放图片*/
                bitmapLow = Bitmap.createBitmap(bitmapLow, 0, 0, bitmapWidth, bitmapHeight, matrix, true);
                bitmapBig = Bitmap.createBitmap(bitmapBig, 0, 0, bitmapWidth, bitmapHeight, matrix, true);
                /**重新获取游标图片的宽高*/
                bitmapHeight = bitmapLow.getHeight();
                bitmapWidth = bitmapLow.getWidth();
                /**初始化两个游标的位置*/
                slideLowX = lineStart;
                slideBigX = lineEnd;
                smallRange = smallValue;
                bigRange = bigValue;
                if (hasRule) {
                    //有刻度时 paddingTop 要加上(text高度)和(刻度线高度加刻度线上边文字的高度和) 之间的最大值
                    paddingTop = paddingTop + Math.max(textSize, ruleLineHeight + ruleTextSize);
                } else {
                    //没有刻度时 paddingTop 加上 text的高度
                    paddingTop = paddingTop + textSize;
                }

        通过Matrix对bitmap进行缩放,将游标设置成我们想要的大小。初始化两个游标在双向选择器的两头,一般都是在最大值和最小值处的,若有特殊需求也可更改slideLowX和slideBigX进行设置。由于我们在计算自定义view的高度时,需要把刻度以及刻度上边文字的高度算进去,所以有刻度时 paddingTop 要加上(text高度)和(刻度线高度加刻度线上边文字的高度和) 之间的最大值,没有刻度时 paddingTop 加上 text的高度。

        计算宽高

        计算View的高度:

        int mode = MeasureSpec.getMode(heightMeasureSpec);
                int size = MeasureSpec.getSize(heightMeasureSpec);
                if (mode == MeasureSpec.EXACTLY) {
                    // matchparent 或者 固定大小 view最小应为 paddingBottom + paddingTop + bitmapHeight + 10 否则显示不全
                    size = Math.max(size, paddingBottom + paddingTop + bitmapHeight + 10);
                } else {
                    //wrap content
                    int height = paddingBottom + paddingTop + bitmapHeight + 10;
                    size = Math.min(size, height);
                }

        当mode == MeasureSpec.EXACTLY时,我们在布局文件中已经固定了view的高度,但是view最小应为 paddingBottom + paddingTop + bitmapHeight + 10 否则显示不全。当没有固定大小时,一般是wrap content,那么它的高度应为 paddingBottom + paddingTop + bitmapHeight + 10(+10只是为了能让view所占的空间大一些而已,没有特殊意义,可以不加)。

        计算View的宽度:

                int mode = MeasureSpec.getMode(widthMeasureSpec);
                int size = MeasureSpec.getSize(widthMeasureSpec);
                if (mode == MeasureSpec.EXACTLY) {
                    size = Math.max(size, paddingLeft + paddingRight + bitmapWidth * 2);
                } else {
                    //wrap content
                    int width = paddingLeft + paddingRight + bitmapWidth * 2;
                    size = Math.min(size, width);
                }
                // match parent 或者 固定大小 此时可以获取线(进度条)的长度
                lineLength = size - paddingLeft - paddingRight - bitmapWidth;
                //线(进度条)的结束位置
                lineEnd = lineLength + paddingLeft + bitmapWidth / 2;
                //线(进度条)的开始位置
                lineStart = paddingLeft + bitmapWidth / 2;
                //初始化 游标位置
                slideBigX = lineEnd;
                slideLowX = lineStart;

        与计算高度同理,但此时,我们需要确定线(进度条)的长度,起始点。

        onDraw 绘制进度条

        画两个游标之间的线:

                linePaint.setAntiAlias(true);
                linePaint.setStrokeWidth(lineWidth);
                linePaint.setColor(inColor);
                linePaint.setStrokeCap(Paint.Cap.ROUND);
                canvas.drawLine(slideLowX, lineY, slideBigX, lineY, linePaint);

        此线从(slideLowX,lineY)到(slideBigX,lineY),其中slideLowX,slideBigY已经在计算宽度时赋值。lineY = getHeight() - paddingBottom - bitmapHeight / 2,即整个View的高度减paddingBottom再减bitmapHeight / 2(游标图的1/2高度),如果游标高度比线宽小的话,则lineY = getHeight() - paddingBottom - lineWidth / 2,不过这种需求应该很少。

        画两个游标到两端的线:

                linePaint.setColor(outColor);
                linePaint.setStrokeCap(Paint.Cap.ROUND);
                //画 外部线
                canvas.drawLine(lineStart, lineY, slideLowX, lineY, linePaint);
                canvas.drawLine(slideBigX, lineY, lineEnd, lineY, linePaint);

        linePaint.setStrokeCap(Paint.Cap.ROUND)可以画出带圆角的线。之后要画两条线,一条是从线的起点到左边游标的中心。另一条是从右边游标的中心到线的终点。画游标:

                canvas.drawBitmap(bitmapLow, slideLowX - bitmapWidth / 2, lineY - bitmapHeight / 2, bitmapPaint);
                canvas.drawBitmap(bitmapBig, slideBigX - bitmapWidth / 2, lineY - bitmapHeight / 2, bitmapPaint);

        即左边游标左部为slideLowX - bitmapWidth / 2,顶端在lineY - bitmapHeight / 2。右边游标同理。

        画游标上边的字:

                textPaint.setColor(textColor);
                textPaint.setTextSize(textSize);
                textPaint.setAntiAlias(true);
                canvas.drawText(String.format("%.0f" + unit, smallRange), slideLowX - bitmapWidth / 2, textHeight, textPaint);
                canvas.drawText(String.format("%.0f" + unit, bigRange), slideBigX - bitmapWidth / 2, textHeight, textPaint);

        字的位置控制在游标的正上方。有其他需求可以在此处调整。

        画刻度线:

                paintRule.setStrokeWidth(1);
                paintRule.setTextSize(ruleTextSize);
                paintRule.setTextAlign(Paint.Align.CENTER);
                paintRule.setAntiAlias(true);
                //遍历 equal份,画刻度
                for (int i = smallValue; i <= bigValue; i += (bigValue - smallValue) / equal) {
                    float degX = lineStart + i * lineLength / (bigValue - smallValue);
                    int degY = lineY - ruleLineHeight;
                    paintRule.setColor(ruleColor);
                    canvas.drawLine(degX, lineY, degX, degY, paintRule);
                    paintRule.setColor(ruleTextColor);
                    canvas.drawText(String.valueOf(i) + ruleUnit, degX, degY, paintRule);
                }

        我们已经传过来equal的值,即把所有数据分成equal份,每一份画一个刻度。并在刻度上方写上数字。如果有特殊需求,比如有的刻度线长有的刻度线短,则需要加个判断,根据判断的结果drawLine,不同的结果设置不同的高度。

        事件监听

        我们需要判断我们触摸屏幕时是否点击在游标上,是左边游标还是右边游标。此时则需要我们对点击事件的监听。

        判断点击位置的方法:

                        float nowX = event.getX();
                        float nowY = event.getY();
                        //按下 在游标范围上
                        boolean rightY = Math.abs(nowY - lineY) < bitmapHeight / 2;
                        //按下 在左边游标上
                        boolean lowSlide = Math.abs(nowX - slideLowX) < bitmapWidth / 2;
                        //按下 在右边游标上
                        boolean bigSlide = Math.abs(nowX - slideBigX) < bitmapWidth / 2;
                        if (rightY && lowSlide) {
                            isLowerMoving = true;
                        } else if (rightY && bigSlide) {
                            isUpperMoving = true;
                            //点击了游标外部 的线上
                        } else if (nowX >= lineStart && nowX <= slideLowX - bitmapWidth / 2 && rightY) {
                            slideLowX = (int) nowX;
                            updateRange();
                            postInvalidate();
                        } else if (nowX <= lineEnd && nowX >= slideBigX + bitmapWidth / 2 && rightY) {
                            slideBigX = (int) nowX;
                            updateRange();
                            postInvalidate();
                        }

        若Math.abs(nowY - lineY) < bitmapHeight / 2,则当前点击位置Y的坐标在游标上下顶点之间,此时可判定当前点击位置在Y轴方向上满足点到了游标。接下来判断X轴,若Math.abs(nowX - slideLowX) < bitmapWidth / 2即当前点击位置X的坐标在游标的左右顶点之间,此时满足当前点击到了左边游标的条件。我们此时才可以判定当前点击位置点在了左边游标上。右边游标的判定同理。完整的监听代码请在文末上传的项目中查看。

        滑动状态监听:

        //左边游标是运动状态
                        if (isLowerMoving) {
                            //当前 X坐标在线上 且在右边游标的左边
                            if (nowX <= slideBigX - bitmapWidth && nowX >= lineStart - bitmapWidth / 2) {
                                slideLowX = (int) nowX;
                                if (slideLowX < lineStart) {
                                    slideLowX = lineStart;
                                }
                                //更新进度
                                updateRange();
                                postInvalidate();
                            }
                        } else if (isUpperMoving) {
                            //当前 X坐标在线上 且在左边游标的右边
                            if (nowX >= slideLowX + bitmapWidth && nowX <= lineEnd + bitmapWidth / 2) {
                                slideBigX = (int) nowX;
                                if (slideBigX > lineEnd) {
                                    slideBigX = lineEnd;
                                }
                                //更新进度
                                updateRange();
                                postInvalidate();
                            }

        如果经判定,当前点击位置在左边游标上,且当前坐标在右边游标的左边,并且在线的起点的右边(当然还得考虑到游标图片大小的影响,不能让两个游标重合)(nowX <= slideBigX - bitmapWidth && nowX >= lineStart - bitmapWidth / 2),那么更新当前slideLowX,更新进度,之后调用postInvalidate()刷新界面。更新进度:

          private void updateRange() {
                //当前 左边游标数值
                smallRange = computRange(slideLowX);
                //当前 右边游标数值
                bigRange = computRange(slideBigX);
                //接口 实现值的传递
                if (onRangeListener != null) {
                    onRangeListener.onRange(smallRange, bigRange);
                }
            }

        通过此方法获取左右游标上的数值,然后通过我们自己定义的接口进行值的传递。

        computRange();

        private float computRange(float range) {
                return (range - lineStart) * (bigValue - smallValue) / lineLength + smallValue;
            }

        这个方法在我看来就是个数学题了。通过当前长度占总长度的比例,再乘以数据的总数,加上起点的数据(数据的最小值),就是我们当前的数据了。

        三、使用 布局文件

        布局文件(有刻度线)

        <com.example.txs.doubleslideseekbar.DoubleSlideSeekBar
                android:id="@+id/doubleslide_withrule"
                android:layout_marginTop="20dp"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                custom:lineHeight="6dp"
                custom:textSize="12sp"
                custom:textColor="#0628e4"
                custom:inColor="#f10a0a"
                custom:outColor="#af08e2"
                custom:imageLow="@mipmap/imgv_slide"
                custom:imageBig="@mipmap/imgv_slide"
                custom:imagewidth="20dp"
                custom:imageheight="20dp"
                custom:hasRule="true"
                custom:ruleColor="#0e0e0e"
                custom:ruleTextColor="#f74104"
                custom:unit="元"
                custom:equal="10"
                custom:ruleUnit="$"
                custom:ruleTextSize="8sp"
                custom:ruleLineHeight="10dp"
                />

        布局文件(无刻度线)

        <com.example.txs.doubleslideseekbar.DoubleSlideSeekBar
                android:id="@+id/doubleslide_withoutrule"
                android:layout_marginTop="40dp"
                android:layout_width="300dp"
                android:layout_height="wrap_content"
                custom:lineHeight="20dp"
                custom:textSize="16sp"
                custom:textColor="#e40627"
                custom:inColor="#0a40f1"
                custom:outColor="#ace208"
                custom:imageLow="@mipmap/imgv_slide"
                custom:imageBig="@mipmap/imgv_slide"
                custom:imagewidth="20dp"
                custom:imageheight="20dp"
                custom:hasRule="false"
                custom:bigValue="1000"
                custom:smallValue="0"
                />

        这里面包含了我们自定义属性的使用,又不懂得地方请看上方表格。

        - 代码中用法

        public class MainActivity extends AppCompatActivity {
            private DoubleSlideSeekBar mDoubleslideWithrule;
            private DoubleSlideSeekBar mDoubleslideWithoutrule;
            private TextView mTvMinRule;
            private TextView mTvMaxRule;
            private TextView mTvMinWithoutRule;
            private TextView mTvMaxWithoutRule;
            @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_main);
                initView();
                setListener();
            }
            private void setListener() {
                // 用法
                mDoubleslideWithrule.setOnRangeListener(new DoubleSlideSeekBar.onRangeListener() {
                    @Override
                    public void onRange(float low, float big) {
                        mTvMinRule.setText("最小值" + String.format("%.0f" , low));
                        mTvMaxRule.setText("最大值" + String.format("%.0f" , big));
                    }
                });
                mDoubleslideWithoutrule.setOnRangeListener(new DoubleSlideSeekBar.onRangeListener() {
                    @Override
                    public void onRange(float low, float big) {
                        mTvMinWithoutRule.setText("最小值" + String.format("%.0f" , low));
                        mTvMaxWithoutRule.setText("最大值" + String.format("%.0f" , big));
                    }
                });
            }
            private void initView() {
                mDoubleslideWithrule = (DoubleSlideSeekBar) findViewById(R.id.doubleslide_withrule);
                mDoubleslideWithoutrule = (DoubleSlideSeekBar) findViewById(R.id.doubleslide_withoutrule);
                mTvMinRule = (TextView) findViewById(R.id.tv_min_rule);
                mTvMaxRule = (TextView) findViewById(R.id.tv_max_rule);
                mTvMinWithoutRule = (TextView) findViewById(R.id.tv_min_without_rule);
                mTvMaxWithoutRule = (TextView) findViewById(R.id.tv_max_without_rule);
            }
        }

        用法很简单,我们可以通过我们定义的接口获取当前范围。

        四、后记

        此项目使用自定义view的知识比较多,大家若想巩固自己的自定义view的知识可以拿这个项目来练练手,而且由于时间问题,此项目可优化的地方还很多,比如再加一个属性控制游标上边的文字在游标上部,中部,下部。

        滑动监听判断tanα<1才判断是游标在滑动,控制不同长度的刻度线等。这些大家都可以根据自己的需求自由定制,我所实现的功能也只是符合大多数情况而已。

        github项目地址:https://github.com/tangxuesong6/DoubleSlideSeekBar

        以上就是Android开发双向滑动范围选择器SeekBar实现的详细内容,更多关于Android双向滑动SeekBar范围的资料请关注自由互联其它相关文章!

        网友评论