自定义区间滑动取值控件主要涉及到的知识点有坐标系、画笔、画布以及自定义属性。因此,在自定义控件之前,先来了解一下相关的知识。
关于坐标系,android中有两种,分别为Android坐标系和视图坐标系。
在自定义控件时,一定会涉及到控件的大小以及间距的值,而这些值又是如何得到的呢。下面我们来看看获取距离的几种方法。
View提供的获取的坐标以及距离的方法:
2 . MotionEvent提供的方法(触发屏幕触碰事件时,在onTouchEvent回调方法中传入MotionEvent的参数):
PS:需要特别注意,Y轴的正方向是向下的。
画笔样式:
关于画笔,这里只说明一下我们用到的几个方法以及其参数。
Canvas的几个draw方法,这些方法都进行了不同形式的重载。
细说一下接下来要用的三个方法:
drawLine(float startX, float startY, float stopX, float stopY, @NonNull Paint paint)
drawCircle(float cx, float cy, float radius, @NonNull Paint paint)
drawText(@NonNull String text, float x, float y, @NonNull Paint paint)
Canvas还有几个应该关注的方法:
onMeasure(int widthMeasureSpec, int heightMeasureSpec):这两个参数widthMeasureSpec, heightMeasureSpec由ViewGroup中的layout_width,la
这个方法主要是对控件的测量,通过重写这个方法,根据传入的两个参数,并调用MeasureSpec.getSize()方法,我们才能获得控件的宽高,如下
onDraw(Canvas canvas):根据设置的画笔及控件的相关参数来绘制控件,对于canvas的相关操作,已在上面进行说明。
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
在res/valuse的attrs.xml中自定义属性,几个关键词的理解:
format类型有:
在自定义控件时,我们通过Context实例调用obtainStyledAttributes(android.util.AttributeSet, int[], int, int)方法得到TypedArray实例,该实例中有相应的方法来获取对应格式的属性值。在自定义控件的构造方法中去调用,最后还要调用TypedArray实例的recycle()方法来释放资源(注:TypeArray内部是通过一个静态方法来维护实例的,也就是说这一个典型的单例模式)。
//自定义属性
<resources>
<declare-styleable name="RangeSelectionView">
<attr name="backLineColor" format="color" />
<attr name="connectLineColor" format="color" />
<attr name="circleColor" format="color" />
<attr name="whileCircleColor" format="color" />
<attr name="startValueColor" format="color" />
<attr name="endValueColor" format="color" />
<attr name="resultValueColor" format="color" />
<attr name="isShowResult" format="boolean" />
<attr name="isInteger" format="boolean" />
<attr name="valuePrecision" format="integer" />
<attr name="startValue" format="float" />
<attr name="endValue" format="float" />
<attr name="leftUnit" format="string" />
<attr name="rightUnit" format="string" />
declare-styleable>
resources>
这个控件中,需要计算的几个关键值
基线的起始和终点坐标值;
//无论是起点还是终点,Y轴的值均是控件高度的一半
//起点的X轴:设定的边距值+控件的paddingStart
//终点的x轴:控件宽度-设定的边距值-paddingEnd
由于代码中已经有很明确的注释,因此对于所定义的变量和一些计算,在这里就不进行解释了。下面直接上代码。
/**
* Description:区间滑动取值控件
* Created by Kevin.Li on 2018-01-11.
*/
public class RangeSelectionView extends View {
private Paint paintBackground;//背景线的画笔
private Paint paintCircle;//起始点圆环的画笔
private Paint paintWhileCircle;//起始点内圈白色区域的画笔
private Paint paintStartText;//起点数值的笔
private Paint paintEndText;//终点数值的画笔
private Paint paintResultText;//顶部结果数值的画笔
private Paint paintConnectLine;//起始点连接线的画笔
private int mHeight = 0;//控件的高度
private int mWidth = 0;//控件的宽度
private float centerVertical = 0;//y轴的中间位置
private float backLineWidth = 5;//底线的宽度
private float marginHorizontal = 1;//横向边距
private float marginTop = 60;//文字距基线顶部的距离
private float marginBottom = 40;//文字距基线底部的距离
private float pointStart = 0;//起点的X轴位置
private float pointEnd = 0;//始点的Y轴位置
private float circleRadius = 30;//起始点圆环的半径
private float numStart = 0;//数值的开始值
private float numEnd = 0;//数值的结束值
private int textSize = 35;//文字的大小
private String strConnector = " - ";//连接符
private boolean isRunning = false;//是否可以滑动
/**
* 起点还是终点 true:起点;false:终点。
*/
private boolean isStart = true;
private int pdStart;//控件padding值
private int pdEnd;
private float scaling;//取值比例
/**
* 进度值范围——起点值
*/
private float startNum = 0.00F;
/**
* 进度值范围——终点值
*/
private float endNum = 100.00F;
/**
* 左侧单位
*/
private String leftUnit;
/**
* 右侧单位
*/
private String rightUnit;
/**
* 是否保留整形
*/
private boolean isInteger = false;
/**
* 保留精度,默认为2。
*/
private int precision = 2;
/**
* 是否显示结果值,默认显示。
*/
private boolean isShowResult = true;
/**
* 开始文字颜色
*/
private int startValueColor;
/**
* 终点文字颜色
*/
private int endValueColor;
/**
* 结果值文字颜色
*/
private int resultValueColor;
/**
* 基线颜色
*/
private int backLineColor;
/**
* 连接线颜色
*/
private int connectLineColor;
/**
* 外圆填充色
*/
private int circleColor;
/**
* 圆形填充色
*/
private int whileCircleColor;
private OnChangeListener mOnChangeListener;
public RangeSelectionView(Context context) {
super(context);
init();
}
public RangeSelectionView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
init();
}
public RangeSelectionView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
handleAttrs(context, attrs, defStyleAttr);
init();
}
/**
* 获取自定义属性的值
*/
private void handleAttrs(Context context, AttributeSet attrs, int defStyleAttr) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RangeSelectionView, defStyleAttr, 0);
backLineColor = ta.getColor(R.styleable.RangeSelectionView_backLineColor, Color.CYAN);
connectLineColor = ta.getColor(R.styleable.RangeSelectionView_connectLineColor, Color.BLUE);
circleColor = ta.getColor(R.styleable.RangeSelectionView_circleColor, Color.BLUE);
whileCircleColor = ta.getColor(R.styleable.RangeSelectionView_whileCircleColor, Color.WHITE);
startValueColor = ta.getColor(R.styleable.RangeSelectionView_startValueColor, Color.MAGENTA);
endValueColor = ta.getColor(R.styleable.RangeSelectionView_endValueColor, Color.MAGENTA);
resultValueColor = ta.getColor(R.styleable.RangeSelectionView_resultValueColor, Color.MAGENTA);
isShowResult = ta.getBoolean(R.styleable.RangeSelectionView_isShowResult, true);
isInteger = ta.getBoolean(R.styleable.RangeSelectionView_isInteger, false);
precision = ta.getInteger(R.styleable.RangeSelectionView_valuePrecision, 2);
startNum = ta.getFloat(R.styleable.RangeSelectionView_startValue, startNum);
endNum = ta.getFloat(R.styleable.RangeSelectionView_endValue, endNum);
if (ta.getString(R.styleable.RangeSelectionView_leftUnit) != null) {
leftUnit = ta.getString(R.styleable.RangeSelectionView_leftUnit);
}
if (ta.getString(R.styleable.RangeSelectionView_rightUnit) != null) {
rightUnit = ta.getString(R.styleable.RangeSelectionView_rightUnit);
}
ta.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取控件的宽高、中线位置、起始点、起始数值
mHeight = MeasureSpec.getSize(heightMeasureSpec);//获取总高度,是包含padding值
mWidth = MeasureSpec.getSize(widthMeasureSpec);//获取总宽度,是包含padding值
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
pdStart = getPaddingStart();
pdEnd = getPaddingEnd();
} else {
pdStart = getPaddingLeft();
pdEnd = getPaddingRight();
}
centerVertical = mHeight / 2;
pointStart = marginHorizontal + pdStart + circleRadius;
pointEnd = mWidth - marginHorizontal - pdEnd - circleRadius;
initBaseData();
}
/**
* 初始化基础值
*/
private void initBaseData() {
// (父级控件宽度-左右边距-圆直径)/(结束值-起点值)
scaling = (mWidth - 2 * marginHorizontal - (pdStart + pdEnd) - 2 * circleRadius) / (endNum - startNum);
numStart = getProgressNum(pointStart);
numEnd = getProgressNum(pointEnd);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//如果点击的点在第一个圆内就是起点,并且可以滑动
if (event.getX() >= (pointStart - circleRadius) && event.getX() <= (pointStart + circleRadius)) {
isRunning = true;
isStart = true;
pointStart = event.getX();
//如果点击的点在第二个圆内就是终点,并且可以滑动
} else if (event.getX() <= (pointEnd + circleRadius) && event.getX() >= (pointEnd - circleRadius)) {
isRunning = true;
isStart = false;
pointEnd = event.getX();
} else {
//如果触控点不在圆环内,则不能滑动
isRunning = false;
}
break;
case MotionEvent.ACTION_MOVE:
if (isRunning) {
if (isStart) {
//起点滑动时,重置起点的位置和进度值
pointStart = event.getX();
if (pointStart < marginHorizontal + pdStart + circleRadius) {
pointStart = marginHorizontal + pdStart + circleRadius;
numStart = startNum;
} else {
if (pointStart + circleRadius < pointEnd - circleRadius) {//防止起点不动而值增加的问题
numStart = getProgressNum(pointStart);
}
}
} else {
//始点滑动时,重置始点的位置和进度值
pointEnd = event.getX();
if (pointEnd > mWidth - marginHorizontal - pdEnd - circleRadius) {
pointEnd = mWidth - marginHorizontal - pdEnd - circleRadius;
numEnd = endNum;
} else {
if (pointEnd < marginHorizontal + pdStart + 3 * circleRadius) {//防止终点和起点在起始点相连时,往左移动,终点不动,而值减小的问题。
pointEnd = marginHorizontal + pdStart + 3 * circleRadius;
}
numEnd = getProgressNum(pointEnd);
}
}
flushState();//刷新状态
}
break;
case MotionEvent.ACTION_UP:
flushState();
break;
}
return true;
}
/**
* 刷新状态和屏蔽非法值
*/
private void flushState() {
//起点非法值
if (pointStart < marginHorizontal + pdStart + circleRadius) {
pointStart = marginHorizontal + pdStart + circleRadius;
}
//终点非法值
if (pointEnd > mWidth - marginHorizontal - pdEnd - circleRadius) {
pointEnd = mWidth - marginHorizontal - pdEnd - circleRadius;
}
//防止起点位置大于终点位置(规定:如果起点位置大于终点位置,则将起点位置放在终点位置前面,即:终点可以推着起点走,而起点不能推着终点走)
if (pointStart + circleRadius > pointEnd - circleRadius) {
pointStart = pointEnd - 2 * circleRadius;
numStart = getProgressNum(pointStart);//更新起点值
}
//防止终点把起点推到线性范围之外
if (pointEnd < marginHorizontal + pdStart + 3 * circleRadius) {
pointEnd = marginHorizontal + pdStart + 3 * circleRadius;
pointStart = marginHorizontal + pdStart + circleRadius;
}
invalidate();//这个方法会导致onDraw方法重新绘制
if (mOnChangeListener != null) {// call back listener.
mOnChangeListener.leftCursor(String.valueOf(numStart));
mOnChangeListener.rightCursor(String.valueOf(numEnd));
}
}
/**
* 根据屏幕像素值计算进度数值
*/
private float getProgressNum(float progress) {
if (progress == marginHorizontal + pdStart + circleRadius) {// 处理边界问题,起始值
return startNum;
}
if (progress == mWidth - marginHorizontal - pdEnd - circleRadius) {// 处理边界问题,终止值
return endNum;
}
// (坐标点-左边距-圆半径)/比例 + 起始值
return (progress - marginHorizontal - pdEnd - circleRadius) / scaling + startNum;
}
/**
* 初始化画笔
*/
private void init() {
paintBackground = new Paint();
paintBackground.setColor(backLineColor);
paintBackground.setStrokeWidth(backLineWidth);
paintBackground.setAntiAlias(true);
paintCircle = new Paint();
paintCircle.setColor(circleColor);
paintCircle.setStrokeWidth(backLineWidth);
paintCircle.setStyle(Paint.Style.STROKE);
paintCircle.setAntiAlias(true);
paintWhileCircle = new Paint();
paintWhileCircle.setColor(whileCircleColor);
paintCircle.setStyle(Paint.Style.FILL);
paintWhileCircle.setAntiAlias(true);
paintStartText = new Paint();
paintStartText.setColor(startValueColor);
paintStartText.setTextSize(textSize);
paintStartText.setAntiAlias(true);
paintEndText = new Paint();
paintEndText.setColor(endValueColor);
paintEndText.setTextSize(textSize);
paintEndText.setAntiAlias(true);
paintEndText.setTextAlign(Paint.Align.RIGHT);
paintResultText = new Paint();
paintResultText.setColor(resultValueColor);
paintResultText.setTextSize(textSize);
paintResultText.setAntiAlias(true);
paintConnectLine = new Paint();
paintConnectLine.setColor(connectLineColor);
paintConnectLine.setStrokeWidth(backLineWidth + 5);
paintConnectLine.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas);
//背景线
//无论是起点还是终点,Y轴的值均是控件高度的一半
//起点的X轴:设定的边距值+控件的paddingStart
//终点的x轴:控件宽度-设定的边距值-paddingEnd
canvas.drawLine(marginHorizontal + pdStart, centerVertical, mWidth - marginHorizontal - pdEnd, centerVertical, paintBackground);
//起点位置的外圈圆
canvas.drawCircle(pointStart, centerVertical, circleRadius, paintCircle);
//起点位置的内圈圆
canvas.drawCircle(pointStart, centerVertical, circleRadius - backLineWidth, paintWhileCircle);
//终点位置的外圈圆
canvas.drawCircle(pointEnd, centerVertical, circleRadius, paintCircle);
//终点位置的内圈圆
canvas.drawCircle(pointEnd, centerVertical, circleRadius - backLineWidth, paintWhileCircle);
//起始点连接线
canvas.drawLine(pointStart + circleRadius, centerVertical, pointEnd - circleRadius, centerVertical, paintConnectLine);
//起点数值
canvas.drawText(assembleStartText(), pointStart - circleRadius, centerVertical + marginBottom + circleRadius, paintStartText);
//终点数值
canvas.drawText(assembleEndText(), pointEnd + circleRadius, centerVertical + marginBottom + circleRadius, paintEndText);
if (isShowResult) {
//结果值
canvas.drawText(assembleResultText(), marginHorizontal + pdStart, centerVertical - marginTop, paintResultText);
}
}
/**
* 处理起点值精度
*/
private float handleNumStartPrecision(float value) {
BigDecimal bd = new BigDecimal(value);
bd = bd.setScale(precision, BigDecimal.ROUND_HALF_UP);
return bd.floatValue();
}
/**
* 处理终点值精度
*/
private float handleNumEndPrecision(float value) {
BigDecimal bd = new BigDecimal(value);
bd = bd.setScale(precision, BigDecimal.ROUND_HALF_DOWN);
return bd.floatValue();
}
/**
* 组装起点文字
*/
private String assembleStartText() {
StringBuilder sb = new StringBuilder();
if (!TextUtils.isEmpty(leftUnit)) sb.append(leftUnit);
//必须在此调用String.valueOf()来提前转化为String,否则会因为append()重载而导致整形参数无效的问题。
sb.append(isInteger ? String.valueOf((int) numStart) : String.valueOf(handleNumStartPrecision(numStart)));
if (!TextUtils.isEmpty(rightUnit)) sb.append(" ").append(rightUnit);
return sb.toString();
}
/**
* 组装终点文字
*/
private String assembleEndText() {
StringBuilder sb = new StringBuilder();
if (!TextUtils.isEmpty(leftUnit)) sb.append(leftUnit);
sb.append(isInteger ? String.valueOf((int) numEnd) : handleNumEndPrecision(numEnd));
if (!TextUtils.isEmpty(rightUnit)) sb.append(" ").append(rightUnit);
return sb.toString();
}
/**
* 组装结果值
*/
private String assembleResultText() {
StringBuilder sb = new StringBuilder();
if (!TextUtils.isEmpty(leftUnit)) sb.append(leftUnit);
sb.append(isInteger ? String.valueOf((int) numStart) : handleNumStartPrecision(numStart));
if (!TextUtils.isEmpty(rightUnit)) sb.append(" ").append(rightUnit);
sb.append(strConnector);
if (!TextUtils.isEmpty(leftUnit)) sb.append(leftUnit);
sb.append(isInteger ? String.valueOf((int) numEnd) : handleNumEndPrecision(numEnd));
if (!TextUtils.isEmpty(rightUnit)) sb.append(" ").append(rightUnit);
return sb.toString();
}
/**
* 左侧单位
*/
public RangeSelectionView setLeftUnit(String leftUnit) {
this.leftUnit = leftUnit;
return this;
}
/**
* 右侧单位
*/
public RangeSelectionView setRightUnit(String rightUnit) {
this.rightUnit = rightUnit;
return this;
}
/**
* 是否保留整形
*/
public RangeSelectionView setInteger(boolean integer) {
isInteger = integer;
return this;
}
/**
* 是否显示结果值,默认显示。
*/
public RangeSelectionView setShowResult(boolean showResult) {
isShowResult = showResult;
return this;
}
/**
* 保留精度,默认为2。
*/
public RangeSelectionView setPrecision(int precision) {
this.precision = precision;
return this;
}
/**
* 起始值
*/
public RangeSelectionView setStartNum(float startNum) {
this.startNum = startNum;
return this;
}
/**
* 结束值
*/
public RangeSelectionView setEndNum(float endNum) {
this.endNum = endNum;
return this;
}
/**
* 开始文字颜色
*/
public RangeSelectionView setStartValueColor(int startValueColor) {
this.startValueColor = startValueColor;
return this;
}
/**
* 终点文字颜色
*/
public RangeSelectionView setEndValueColor(int endValueColor) {
this.endValueColor = endValueColor;
return this;
}
/**
* 结果值文字颜色
*/
public RangeSelectionView setResultValueColor(int resultValueColor) {
this.resultValueColor = resultValueColor;
return this;
}
/**
* 基线颜色
*/
public RangeSelectionView setBackLineColor(int backLineColor) {
this.backLineColor = backLineColor;
return this;
}
/**
* 连接线颜色
*/
public RangeSelectionView setConnectLineColor(int connectLineColor) {
this.connectLineColor = connectLineColor;
return this;
}
/**
* 外圆填充色
*/
public RangeSelectionView setCircleColor(int circleColor) {
this.circleColor = circleColor;
return this;
}
/**
* 圆形填充色
*/
public RangeSelectionView setWhileCircleColor(int whileCircleColor) {
this.whileCircleColor = whileCircleColor;
return this;
}
/**
* 通知刷新。
* 在调用系列setXxx方法之后,需要调用此方法,才会生效。
*/
public void notifyRefresh() {
init();
initBaseData();
invalidate();//这个方法会导致onDraw方法重新绘制
}
/**
* 主要充值起点和终点的画笔值
*/
public void reSetValue() {
pointStart = marginHorizontal + pdStart + circleRadius;
pointEnd = mWidth - marginHorizontal - pdEnd - circleRadius;
}
public void setOnChangeListener(OnChangeListener onChangeListener) {
mOnChangeListener = onChangeListener;
}
/**
* 值变化监听器
*/
public interface OnChangeListener {
/**
* 起点进度值变化回调
*/
void leftCursor(String resultValue);
/**
* 终点进度值变化回调
*/
void rightCursor(String resultValue);
}
}
"http://schemas.android.com/apk/res-auto"
android:id="@+id/rsv_view"
android:layout_width="match_parent"
android:layout_height="120dp"
android:background="@android:color/white"
android:visibility="visible"
rsv:backLineColor="@color/blue_transparent_background_70"
rsv:circleColor="@color/blue_transparent_background_70"
rsv:connectLineColor="@color/auxiliary_blue"
rsv:endValue="1.0"
rsv:endValueColor="@color/warn"
rsv:isInteger="true"
rsv:leftUnit="@string/monetary_unit_rmb"
rsv:resultValueColor="@color/danger"
rsv:startValue="10.0"
rsv:startValueColor="@color/warn"
rsv:whileCircleColor="@color/white" />
//java代码调用
final RangeSelectionView rsv;
rsv = (RangeSelectionView) findViewById(R.id.rsv_view);
rsv.setBackLineColor(Color.RED);
rsv.setResultValueColor(Color.DKGRAY);
rsv.setStartNum(5);
rsv.setEndNum(20);
rsv.notifyRefresh();
Demo源码
Android Paint的使用详解:Android Paint的使用详解
Android自定义属性之format解析:Android自定义属性之format解析
Android自定义View(三、深入解析控件测量onMeasure):Android自定义View(三、深入解析控件测量onMeasure)