软件编程
位置:首页>> 软件编程>> Android编程>> Android开发双向滑动选择器范围SeekBar实现

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

作者:tangxuesong6  发布时间:2023-04-07 14:46:59 

标签:Android,双向,SeekBar,滑动范围

一、概述

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

首先,看效果图:

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

我对该范围选择器的属性进行了一些封装,例如我们可以自由控制我们的范围选择器是否显示刻度、刻度的长度、我们选择器上每个值的单位、最大值最小值、游标(即那个圆形图片)的样式、大小、选择器内部范围颜色以及外部颜色等等很多属性。更多玩法还请下载我的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&alpha;<1才判断是游标在滑动,控制不同长度的刻度线等。这些大家都可以根据自己的需求自由定制,我所实现的功能也只是符合大多数情况而已。

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

来源:https://blog.csdn.net/songtangxue/article/details/79229713

0
投稿

猜你喜欢

手机版 软件编程 asp之家 www.aspxhome.com