风起
最近的项目需要用到一个双向范围选择器,遂自己操刀并做下记录
介绍
范围选择器要实现的功能就是进行范围选择,并提供接口向调用者暴露所选最小最大值,由于项目只是需要一个普通的范围选择器,所以并没有其他的花哨的动画特效 duang ~(为自己的技穷找一个借口)
实现
-
确定范围选择器需要哪些自定义属性,并在 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> -
接下来接是创建范围选择器,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);
} -
之前我们定义选择器所需要的属性,那么现在我就要在 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;
} -
拿到了绘图所需要的数据,接下来就是测量选择器的大小,重写 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; }
-
测量好了就该绘图了,重写 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); }
-
图形已经出现,我们目前要操作的是两个球,那么我们就得判断球是否被触摸到,我这里触摸的范围是刚好装下球的正方形,你也适当得增大触控面积(如果你的球需要绘制很小的话)
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; }
-
写好了判断,接下来就是实现拖动效果了,当手指按下时,就判断是否触摸到了球,触摸了那个球,记录下状态;当手指抬起时,都将触摸状态置为 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; }
-
继续来处理滑动逻辑,我们要先知道球的滑动范围,
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)); } **/ } }
-
现在界面的雏形已经出现了,接下来我们要根据滑动来实时更新我们的范围值,一开始我们就拿到了总范围值,然后根据滑动比例获取范围值
计算公式
-
min 值:(minBall 位置 - 标准线起点)/ 标准线长度 * 总范围值
-
max 值:(maxBall 位置 - 标准线起点)/ 标准线长度 * 总范围值
-
-
范围值我们拿到了,最后一步结束范围值提供给调用者,这个部分大家都很熟悉了,直接贴
public interface RangeChangeListener { void onMinChange(int minValue); void onMaxChange(int maxValue); } public void setRangeChangeListener(RangeChangeListener rangeChangeListener) { mRangeChangeListener = rangeChangeListener; }
总结
到此为止一个简单的范围选择器就完成了,由于最近还在赶其他项目,所以目前先这么简陋的吧,如果有其他需要还可以更加完善,如多点操作,在标准线上点击实现球的位置跳转,变化动画等。没什么技术含量,纯粹写写文记录开发经历而已。(有空补上源码图片)
网友评论