Android自定義View實(shí)現(xiàn)價(jià)格區(qū)間選擇控件
前言
之前我們的復(fù)習(xí)中,我們已經(jīng)對(duì)原生 Canvas 的繪制有了詳細(xì)的了解,我們對(duì)事件的處理也有了簡(jiǎn)單的了解,這一期我們就對(duì)繪制與事件的處理做更進(jìn)一步的實(shí)現(xiàn)。
如圖,我們需要做這么一個(gè)區(qū)間的選擇控件,此控件也是我們常用的控件,在一些篩選頁面,根據(jù)價(jià)格,數(shù)值進(jìn)行一些篩選的時(shí)候,我們需要設(shè)置一個(gè)最小值和一個(gè)最大值。然后取一段中間的區(qū)間值。
而這個(gè)控件的實(shí)現(xiàn)就是典型的自定義繪制與自定義事件處理的標(biāo)志性實(shí)現(xiàn)。我愿稱之為自定義View的筑基練習(xí),如果大家能從頭到尾實(shí)現(xiàn)一遍,那么對(duì)自定義流程基本上已經(jīng)駕輕就熟了。
慣例我們分析一下實(shí)現(xiàn)步驟:
- 左邊右邊的控制圓分別實(shí)現(xiàn),雖然一般情況下它們的屬性都是相同的,但是為了防止左右不同的圓,我們做好兼容處理。
- 中間的圓角矩形進(jìn)度條,我們也分為默認(rèn)的底色和選中的顏色。
- 對(duì)事件的處理,左右的圓形控件的移動(dòng)處理。
- 其他的文本顯示。
- 自定義屬性的抽取與回調(diào)處理。
思路我們已經(jīng)有了,下面一步一步的來實(shí)現(xiàn)吧! Let's go
1、繪制靜態(tài)的圖形
關(guān)于靜態(tài)的效果繪制,我們已經(jīng)駕輕就熟了。 測(cè)量,畫筆,矩陣的初始化,繪制,一套流程下來,都已經(jīng)是固定的模板了。
進(jìn)度矩形條,左右圓形的一些資源,我們就能實(shí)現(xiàn)一個(gè)靜態(tài)的繪制。
需要定義的變量如下:
private int mRangLineHeight = getResources().getDimensionPixelSize(R.dimen.d_4dp); //圓角矩形線的高度
private int mRangLineCornerRadius; //圓角矩形線的圓角半徑
private int mRangLineDefaultColor = Color.parseColor("#CDCDCD"); //默認(rèn)顏色
private int mRangLineCheckedColor = Color.parseColor("#0689FD"); //選中顏色
private int mCircleRadius = getResources().getDimensionPixelSize(R.dimen.d_14dp); //圓半徑
private int mCircleStrokeWidth = getResources().getDimensionPixelSize(R.dimen.d_1d5dp); //圓邊框的大小
private int mLeftCircleBGColor = Color.parseColor("#0689FD"); //左邊實(shí)心圓顏色
private int mLeftCircleStrokeColor = Color.parseColor("#FFFFFF"); //左邊圓邊框的顏色
private int mRightCircleBGColor = Color.parseColor("#0689FD"); //右邊實(shí)心圓顏色
private int mRightCircleStrokeColor = Color.parseColor("#FFFFFF"); //右邊圓邊框的顏色
private float mLeftCircleCenterX; //左右兩個(gè)圓心位置
private float mLeftCircleCenterY;
private float mRightCircleCenterX;
private float mRightCircleCenterY;
private RectF mDefaultCornerLineRect; //默認(rèn)顏色的圓角矩形
private RectF mSelectedCornerLineRect; //選中顏色的圓角矩形
private Paint mLeftCirclePaint; //各種畫筆
private Paint mLeftCircleStrokePaint;
private Paint mRightCirclePaint;
private Paint mRightCircleStrokePaint;
private Paint mDefaultLinePaint;
private Paint mSelectedLinePaint;畫筆與Rect的初始化:
private void initPaint() {
//初始化左邊實(shí)心圓
mLeftCirclePaint = new Paint();
mLeftCirclePaint.setAntiAlias(true);
mLeftCirclePaint.setDither(true);
mLeftCirclePaint.setStyle(Paint.Style.FILL);
mLeftCirclePaint.setColor(mLeftCircleBGColor);
//初始化左邊圓的邊框
mLeftCircleStrokePaint = new Paint();
mLeftCircleStrokePaint.setAntiAlias(true);
mLeftCircleStrokePaint.setDither(true);
mLeftCircleStrokePaint.setStyle(Paint.Style.STROKE);
mLeftCircleStrokePaint.setColor(mLeftCircleStrokeColor);
mLeftCircleStrokePaint.setStrokeWidth(mCircleStrokeWidth);
//初始化右邊實(shí)心圓
mRightCirclePaint = new Paint();
mRightCirclePaint.setAntiAlias(true);
mRightCirclePaint.setDither(true);
mRightCirclePaint.setStyle(Paint.Style.FILL);
mRightCirclePaint.setColor(mRightCircleBGColor);
//初始化右邊圓的邊框
mRightCircleStrokePaint = new Paint();
mRightCircleStrokePaint.setAntiAlias(true);
mRightCircleStrokePaint.setDither(true);
mRightCircleStrokePaint.setStyle(Paint.Style.STROKE);
mRightCircleStrokePaint.setColor(mRightCircleStrokeColor);
mRightCircleStrokePaint.setStrokeWidth(mCircleStrokeWidth);
//默認(rèn)顏色的圓角矩形線
mDefaultCornerLineRect = new RectF();
//中間選中顏色的圓角矩形
mSelectedCornerLineRect = new RectF();
mDefaultLinePaint = new Paint();
mDefaultLinePaint.setAntiAlias(true);
mDefaultLinePaint.setDither(true);
mSelectedLinePaint = new Paint();
mSelectedLinePaint.setAntiAlias(true);
mSelectedLinePaint.setDither(true);
}關(guān)于測(cè)量還是按我們前面文字介紹說的來,我們先確定測(cè)量的模式與寬高,再計(jì)算一個(gè)最小的寬高,然后根據(jù)xml里面定義的測(cè)量模式來確定測(cè)量的寬高。
具體實(shí)現(xiàn)如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int finalWidth, finalHeight;
//計(jì)算的寬度與高度
int calWidthSize = getPaddingLeft() + mCircleRadius * 2 + getPaddingRight() + mCircleStrokeWidth * 2;
int calHeightSize = getPaddingTop() + mCircleRadius * 2 + mCircleStrokeWidth * 2 + getPaddingBottom();
if (widthMode == MeasureSpec.EXACTLY) {
//如果是精確模式使用測(cè)量的寬度
finalWidth = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
//如果是WrapContent使用計(jì)算的寬度
finalWidth = Math.min(widthSize, calWidthSize);
} else {
//其他模式使用計(jì)算的寬度
finalWidth = calWidthSize;
}
if (heightMode == MeasureSpec.EXACTLY) {
//如果是精確模式使用測(cè)量的高度
finalHeight = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
//如果是WrapContent使用計(jì)算的高度
finalHeight = Math.min(heightSize, calHeightSize);
} else {
//其他模式使用計(jì)算的高度
finalHeight = calHeightSize;
}
//確定測(cè)量寬高
setMeasuredDimension(finalWidth, finalHeight);
}內(nèi)部有詳細(xì)的注釋,推薦大家寬度使用固定的數(shù)組,高度wrap_content。
測(cè)量完成之后當(dāng)顯示出來了,我們就可以對(duì)圓形和矩陣進(jìn)行一些賦值操作。
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//左邊圓的圓心坐標(biāo)
mLeftCircleCenterX = getPaddingLeft() + strokeRadius;
mLeftCircleCenterY = getPaddingTop() /*+ rectDialogHeightAndSpace */ + strokeRadius;
//右邊圓的圓心坐標(biāo)
mRightCircleCenterX = w - getPaddingRight() - strokeRadius;
mRightCircleCenterY = getPaddingTop() /*+ rectDialogHeightAndSpace */ + strokeRadius;
//默認(rèn)圓角矩形進(jìn)度條
mRangLineCornerRadius = mRangLineHeight / 2;//圓角半徑
mDefaultCornerLineRect.left = getPaddingLeft() + strokeRadius;
mDefaultCornerLineRect.top = getPaddingTop() /*+ rectDialogHeightAndSpace */ + strokeRadius - mRangLineCornerRadius;
mDefaultCornerLineRect.right = w - getPaddingRight() - strokeRadius;
mDefaultCornerLineRect.bottom = getPaddingTop() /*+ rectDialogHeightAndSpace */ + strokeRadius + mRangLineCornerRadius;
//選中狀態(tài)圓角矩形進(jìn)度條
mSelectedCornerLineRect.left = mLeftCircleCenterX;
mSelectedCornerLineRect.top = getPaddingTop() /*+ rectDialogHeightAndSpace */ + strokeRadius - mRangLineCornerRadius;
mSelectedCornerLineRect.right = mRightCircleCenterX;
mSelectedCornerLineRect.bottom = getPaddingTop() /*+ rectDialogHeightAndSpace */ + strokeRadius + mRangLineCornerRadius;
}我們確定了圓角進(jìn)度矩形線條的rect,和左右限制圓的圓心和大小,我們就可以使用對(duì)的畫筆進(jìn)行繪制出靜態(tài)的數(shù)據(jù)來。
//左側(cè)的控制圓與邊框
private void drawLeftCircle(Canvas canvas) {
//實(shí)心圓
canvas.drawCircle(mLeftCircleCenterX, mLeftCircleCenterY, mCircleRadius, mLeftCirclePaint);
//空心圓
canvas.drawCircle(mLeftCircleCenterX, mLeftCircleCenterY, mCircleRadius, mLeftCircleStrokePaint);
}
//右側(cè)的控制圓與邊框
private void drawRightCircle(Canvas canvas) {
//實(shí)心圓
canvas.drawCircle(mRightCircleCenterX, mRightCircleCenterY, mCircleRadius, mRightCirclePaint);
//空心圓
canvas.drawCircle(mRightCircleCenterX, mRightCircleCenterY, mCircleRadius, mRightCircleStrokePaint);
}
//中心的圓角矩形進(jìn)度條-默認(rèn)的底色
private void drawDefaultCornerRectLine(Canvas canvas) {
mDefaultLinePaint.setColor(mRangLineDefaultColor);
canvas.drawRoundRect(mDefaultCornerLineRect, mRangLineCornerRadius, mRangLineCornerRadius, mDefaultLinePaint);
}
//中心的圓角矩形進(jìn)度條-已經(jīng)選中的顏色
private void drawSelectedRectLine(Canvas canvas) {
mSelectedLinePaint.setColor(mRangLineCheckedColor);
canvas.drawRoundRect(mSelectedCornerLineRect, mRangLineCornerRadius, mRangLineCornerRadius, mSelectedLinePaint);
}這幾個(gè)東西繪制出來,我們的效果就如下所示:

為了方便顯示大小,我在控件里加一個(gè)灰色的背景為了方便觀看整個(gè)控件的大小。
靜態(tài)的實(shí)現(xiàn)之后我們就要開始讓兩邊的限制圓形動(dòng)起來。
2、讓兩邊的限制圓動(dòng)起來
我們?cè)?onDraw 的方法中可以得知,動(dòng)態(tài)的成員變量就是兩個(gè)圓的X軸坐標(biāo)即為 mLeftCircleCenterX 和 mRightCircleCenterX ,那么中間的進(jìn)度線條的繪制則是根據(jù) mSelectedCornerLineRect 的矩陣來繪制的。矩陣的left 和 right 也是根據(jù) mLeftCircleCenterX 和 mRightCircleCenterX 來計(jì)算的。
所以我們的最終目的就是動(dòng)態(tài)的記錄當(dāng)前事件中的 mLeftCircleCenterX 和 mRightCircleCenterX 值,但是有左右兩個(gè)控制圓,我們?cè)趺磁袛嘁苿?dòng)的是哪一個(gè)圓呢?
先上一個(gè)判斷方法。
/**
* 判斷當(dāng)前移動(dòng)的是左側(cè)限制圓,還是右側(cè)限制圓
*
* @param downX 按下的坐標(biāo)點(diǎn)
* @return true表示按下的左側(cè),false表示按下的右側(cè)
*/
private boolean checkTouchCircleLeftOrRight(float downX) {
//用一個(gè)取巧的方法,如果當(dāng)前按下的為X坐標(biāo),那么左側(cè)圓心X的坐標(biāo)減去按下的X坐標(biāo),如果大于右側(cè)的圓心X減去X坐標(biāo),那么就說明在左側(cè),否則就在右側(cè)
return !(Math.abs(mLeftCircleCenterX - downX) - Math.abs(mRightCircleCenterX - downX) > 0);
}當(dāng)我們移動(dòng)的時(shí)候我們?cè)趺从?jì)算呢?通常常用的方法是把進(jìn)度線條分為幾份,計(jì)算每一份的長(zhǎng)度。我們把這些變量提取出來:
private int mStrokeRadius; //半徑+邊框的總值
private int slice = 5; //代表整體進(jìn)度分為多少份
private float perSlice; //每一份所占的長(zhǎng)度
private int maxValue = 100; //最大值,默認(rèn)為100
private int minValue = 0; //最小值,默認(rèn)為0
private float downX;
private boolean touchLeftCircle;
通過入口方法對(duì)其賦值,并且顯示出來后對(duì)每一份長(zhǎng)度進(jìn)行計(jì)算:
/**
* 設(shè)置數(shù)據(jù)與回調(diào)處理
*/
public void setupData(int minValue, int maxValue, int sliceValue) {
this.minValue = minValue;
this.maxValue = maxValue;
int num = (maxValue - minValue) / sliceValue;
slice = (maxValue - minValue) % sliceValue == 0 ? num : num + 1;
invalidate();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
int realWidth = w - getPaddingLeft() - getPaddingRight();
mStrokeRadius = mCircleRadius + mCircleStrokeWidth;
//計(jì)算每一份對(duì)應(yīng)的距離
perSlice = (realWidth - mStrokeRadius * 2) * 1f / slice;
到處我們就能寫OnTouch方法了,這是核心方法,我們慢一點(diǎn)來。
我們先只對(duì)按下的事件做處理:
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
//按下的時(shí)候記錄當(dāng)前操作的是左側(cè)限制圓還是右側(cè)的限制圓
downX = event.getX();
touchLeftCircle = checkTouchCircleLeftOrRight(downX);
if (touchLeftCircle) {
//如果是左側(cè)
//如果超過右側(cè)最大值則不處理
if (downX + perSlice > mRightCircleCenterX) {
return false;
}
mLeftCircleCenterX = downX;
} else {
//如果是右側(cè)
//如果超過左側(cè)最小值則不處理
if (downX - perSlice < mLeftCircleCenterX) {
return false;
}
mRightCircleCenterX = downX;
}
}
//中間的進(jìn)度矩形是根據(jù)兩邊圓心點(diǎn)動(dòng)態(tài)計(jì)算的
mSelectedCornerLineRect.left = mLeftCircleCenterX;
mSelectedCornerLineRect.right = mRightCircleCenterX;
//全部的事件處理完畢,變量賦值完成之后,開始重繪
invalidate();
return true;
}按下的過程中對(duì),最大最小值做判斷,并且賦值進(jìn)度矩陣的 left 和 right ,那么我們就能實(shí)現(xiàn)指定的點(diǎn)擊效果,如下圖所示:

這只是點(diǎn)擊呢,效果太挫了,我們想要按著滑動(dòng)怎么辦?那我們就需要重寫Move事件。
3、動(dòng)態(tài)滑動(dòng)并計(jì)算當(dāng)前的區(qū)間值
滑動(dòng)相對(duì)來說是比較難得,我們要處理兩個(gè)限制圓的滾動(dòng),并且當(dāng)它們兩個(gè)圓碰撞在一起的時(shí)候,我們要處理交換的邏輯,并且還需要注意滑動(dòng)邊界的處理。
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
//按下的時(shí)候記錄當(dāng)前操作的是左側(cè)限制圓還是右側(cè)的限制圓
downX = event.getX();
touchLeftCircle = checkTouchCircleLeftOrRight(downX);
if (touchLeftCircle) {
//如果是左側(cè)
//如果超過右側(cè)最大值則不處理
if (downX + perSlice > mRightCircleCenterX) {
return false;
}
mLeftCircleCenterX = (int) downX;
} else {
//如果是右側(cè)
//如果超過左側(cè)最小值則不處理
if (downX - perSlice < mLeftCircleCenterX) {
return false;
}
mRightCircleCenterX = downX;
}
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
float moveX = event.getX();
if (mLeftCircleCenterX + perSlice > mRightCircleCenterX) {
//兩圓重合的情況下的處理
if (touchLeftCircle) {
// 左側(cè)到最右邊
if (mLeftCircleCenterX == getWidth() - getPaddingRight() - mStrokeRadius) {
touchLeftCircle = true;
mLeftCircleCenterX = getWidth() - getPaddingRight() - mStrokeRadius;
} else {
//交換右側(cè)滑動(dòng)
touchLeftCircle = false;
mRightCircleCenterX = (int) moveX;
}
} else {
//右側(cè)到最左邊
if (mRightCircleCenterX == getPaddingLeft() + mStrokeRadius) {
touchLeftCircle = false;
mRightCircleCenterX = getPaddingLeft() + mStrokeRadius;
} else {
//交換左側(cè)滑動(dòng)
touchLeftCircle = true;
mLeftCircleCenterX = (int) moveX;
}
}
} else {
//如果是正常的移動(dòng)
if (touchLeftCircle) {
//滑動(dòng)左邊限制圓,如果左邊圓超過右邊圓,那么把右邊圓賦值給左邊圓,如果沒超過就賦值當(dāng)前的moveX
mLeftCircleCenterX = mLeftCircleCenterX - mRightCircleCenterX >= 0 ? mRightCircleCenterX : moveX;
} else {
//滑動(dòng)右邊限制圓,如果右邊圓超過左邊圓,那么把左邊圓賦值給右邊圓,如果沒超過就賦值當(dāng)前的moveX
mRightCircleCenterX = mRightCircleCenterX - mLeftCircleCenterX <= 0 ? mLeftCircleCenterX : moveX;
}
}
}
//對(duì)所有的手勢(shì)效果進(jìn)行過濾操作,不能超過最大最小值
limitMinAndMax();
//中間的進(jìn)度矩形是根據(jù)兩邊圓心點(diǎn)動(dòng)態(tài)計(jì)算的
mSelectedCornerLineRect.left = mLeftCircleCenterX;
mSelectedCornerLineRect.right = mRightCircleCenterX;
//全部的事件處理完畢,變量賦值完成之后,開始重繪
invalidate();
return true;
}主要需要處理的是交換身位的方法,當(dāng)兩個(gè)圓相撞的時(shí)候,需要賦值處理,交換X的賦值,然后切換 touchLeftCircle 的值,然后對(duì)另一個(gè)圓進(jìn)行移動(dòng)。
需要注意的是我們一定要在賦值之前對(duì)最大值與最小值進(jìn)行校驗(yàn),以免滑到天邊去了。
private void limitMinAndMax() {
//如果是操作的左側(cè)限制圓,超過最小值了
if (mLeftCircleCenterX < getPaddingLeft() + mStrokeRadius) {
mLeftCircleCenterX = getPaddingLeft() + mStrokeRadius;
}
//如果是操作的左側(cè)限制圓,超過最大值了
if (mLeftCircleCenterX > getWidth() - getPaddingRight() - mStrokeRadius) {
mLeftCircleCenterX = getWidth() - getPaddingRight() - mStrokeRadius;
}
//如果是操作的右側(cè)限制圓,超過最大值了
if (mRightCircleCenterX > getWidth() - getPaddingRight() - mStrokeRadius) {
mRightCircleCenterX = getWidth() - getPaddingRight() - mStrokeRadius;
}
//如果是操作的右側(cè)限制圓,超過最小值了
if (mRightCircleCenterX < getPaddingLeft() + mStrokeRadius) {
mRightCircleCenterX = getPaddingLeft() + mStrokeRadius;
}
}此時(shí)大致的效果已經(jīng)出來了,效果如圖:

4、計(jì)算當(dāng)前值與回調(diào)處理
我們一直都是計(jì)算的是兩個(gè)圓的中心點(diǎn)X軸的計(jì)算,那么我們真正選中的值是多少呢?總不能把X軸坐標(biāo)給調(diào)用者吧。所以我們需要通過滑動(dòng)的百分比動(dòng)態(tài)的計(jì)算具體的值。
//根據(jù)移動(dòng)的距離計(jì)算當(dāng)前的值
private int getPercentMax(float distance) {
//計(jì)算此時(shí)的位置坐標(biāo)對(duì)應(yīng)的距離能分多少份
int lineLength = getWidth() - getPaddingLeft() - getPaddingRight() - mStrokeRadius * 2;
distance = distance <= 0 ? 0 : (distance >= lineLength ? lineLength : distance);
//計(jì)算滑動(dòng)的百分比
float percentage = distance / lineLength;
return (int) (percentage * maxValue);
}那我們需要在Move事件中一直回調(diào)嗎?沒必要,我們?cè)谌∠臅r(shí)候,或者說事件完畢的時(shí)候,當(dāng)用戶選好了區(qū)間之后,我們回調(diào)一次即可。
if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) {
//計(jì)算當(dāng)前的左側(cè)右側(cè)真正的限制值
int moveLeftData = getPercentMax(mLeftCircleCenterX - getPaddingLeft() - mStrokeRadius);
int moveRightData = getPercentMax(mRightCircleCenterX - getPaddingLeft() - mStrokeRadius);
//順便賦值當(dāng)前的真正值,便于后面的回調(diào)
int leftValue = Math.min(moveLeftData, maxValue);
int rightValue = Math.min(moveRightData, maxValue);
if (mListener != null) mListener.onMoveValue(leftValue, rightValue);
}
//回調(diào)區(qū)間值的監(jiān)聽
private OnRangeValueListener mListener;
public interface OnRangeValueListener {
void onMoveValue(int leftValue, int rightValue);
}我們?cè)贏ctivity中通過setup方法就可以設(shè)置值并監(jiān)聽到最后的區(qū)間事件
override fun init() {
findViewById<RangeView>(R.id.range_view).setupData(0, 100, 1) { leftValue, rightValue ->
toast("leftValue:$leftValue rightValue:$rightValue")
}
}效果:

5、實(shí)時(shí)文本顯示與后續(xù)的擴(kuò)展
這么看起來倒是似模似樣了,我們的需求是在拖動(dòng)的時(shí)候?qū)崟r(shí)在頂部展示一個(gè)彈窗,展示當(dāng)前的值,這怎么搞?
其實(shí)就是在頂部繪制一個(gè)圓角矩形,在矩形內(nèi)部繪制文本,然后我們通過左右限制圓的位置計(jì)算出中間 的位置,讓頂部的圓角矩形在中間位置顯示不就行了嘛。開干
先定義需要用到的成員變量:
private int mTopDialogTextSize = getResources().getDimensionPixelSize(R.dimen.d_12dp); //頂部文字的大小
private int mTopDialogTextColor = Color.parseColor("#000000"); //頂部文字的顏色
private int mTopDialogWidth = getResources().getDimensionPixelSize(R.dimen.d_70dp); //頂部描述信息彈窗的寬度
private int mTopDialogCornerRadius = getResources().getDimensionPixelSize(R.dimen.d_15dp); //頂部描述信息彈窗圓角半徑
private int mTopDialogBGColor = Color.parseColor("#0689FD"); //頂部框的顏色
private int mTopDialogSpaceToProgress = getResources().getDimensionPixelSize(R.dimen.d_2dp); //頂部描述信息彈窗距離進(jìn)度條的間距(配置)
private int mRealDialogDistanceSpace; //頂部彈窗與進(jìn)度條的間距(頂部彈窗與進(jìn)度的真正距離)計(jì)算得出
private Path mTrianglePath; //畫小三角形路徑
private int mTriangleLength = 15; //等邊三角形邊長(zhǎng)
private int mTriangleHeight; //等邊三角形的高
private Paint textPaint;
然后我們?cè)诔跏蓟嫻P與資源的時(shí)候,初始化文本的畫筆和頂部彈窗的矩陣:
private void initPaint() {
//頂部圓角矩形
mTopDialogRect = new RectF();
//畫小三角形指針
mTrianglePath = new Path();
//小三角形的高
mTriangleHeight = (int) Math.sqrt(mTriangleLength * mTriangleLength - mTriangleLength / 2 * (mTriangleLength / 2));
textPaint = new Paint();
textPaint.setAntiAlias(true);
textPaint.setDither(true);
textPaint.setTextSize(mTopDialogTextSize);
textPaint.setColor(mTopDialogTextColor);
}由于我們加了頂部的高度,那么我們就需要在測(cè)量的時(shí)候也要把高度加上去
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
//計(jì)算的寬度與高度
int calWidthSize = getPaddingLeft() + mCircleRadius * 2 + getPaddingRight() + mCircleStrokeWidth * 2;
int calHeightSize = getPaddingTop() + mTopDialogCornerRadius * 2 + mTriangleHeight + mTopDialogSpaceToProgress
+ mCircleRadius * 2 + mCircleStrokeWidth * 2 + getPaddingBottom();
...
}顯示的時(shí)候我們計(jì)算真正的高度,我們比設(shè)置的再高出一點(diǎn)點(diǎn)方便展示。并且對(duì)頂部彈窗的矩陣賦值
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
...
mRealDialogDistanceSpace = mTopDialogCornerRadius * 2 + mTopDialogSpaceToProgress;
//數(shù)值描述圓角矩形
mTopDialogRect.left = w / 2 - mTopDialogWidth / 2;
mTopDialogRect.top = getPaddingTop();
mTopDialogRect.right = w / 2 + mTopDialogWidth / 2;
mTopDialogRect.bottom = getPaddingTop() + mTopDialogCornerRadius * 2;
...
}然后我們就能繪制頂部的彈窗背景與內(nèi)部的文本,再繪制彈窗下面的小三角指針
//頂部的文字框
private void drawTopTextRectDialog(Canvas canvas) {
if (leftValue == minValue && (rightValue == maxValue || rightValue < maxValue)) {
textDesc = "低于 " + rightValue;
} else if (leftValue > minValue && rightValue == maxValue) {
textDesc = "高于 " + leftValue;
} else if (leftValue > minValue && rightValue < maxValue) {
if (leftValue == rightValue) {
textDesc = "低于 " + rightValue;
} else
textDesc = leftValue + "-" + rightValue;
}
if (isShowRectDialog) {
//繪制圓角矩形框
mSelectedLinePaint.setShader(null);
mSelectedLinePaint.setColor(mTopDialogBGColor);
canvas.drawRoundRect(mTopDialogRect, mTopDialogCornerRadius, mTopDialogCornerRadius, mSelectedLinePaint);
//繪制文本
textPaint.setColor(mTopDialogTextColor);
textPaint.setTextSize(mTopDialogTextSize);
float textWidth = textPaint.measureText(textDesc);
float textLeft = mTopDialogRect.left + mTopDialogWidth / 2 - textWidth / 2;
canvas.drawText(textDesc, textLeft, getPaddingTop() + mTopDialogCornerRadius + mTopDialogTextSize / 4, textPaint);
}
}
//頂部文字框下面的三角箭頭
private void drawSmallTriangle(Canvas canvas) {
if (isShowRectDialog) {
mTrianglePath.reset();
mTrianglePath.moveTo(mTopDialogRect.left + mTopDialogWidth / 2 - mTriangleLength / 2, getPaddingTop() + mTopDialogCornerRadius * 2);
mTrianglePath.lineTo(mTopDialogRect.left + mTopDialogWidth / 2 + mTriangleLength / 2, getPaddingTop() + mTopDialogCornerRadius * 2);
mTrianglePath.lineTo(mTopDialogRect.left + mTopDialogWidth / 2, getPaddingTop() + mTopDialogCornerRadius * 2 + mTriangleHeight);
mTrianglePath.close();
canvas.drawPath(mTrianglePath, mSelectedLinePaint);
}
}由于要繪制的參數(shù)是頂部的矩陣,所以我們?cè)趏nTouch中,還需要對(duì)頂部的矩陣進(jìn)行重新動(dòng)態(tài)賦值,才能讓他動(dòng)起來:
@Override
public boolean onTouchEvent(MotionEvent event) {
...
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
//計(jì)算當(dāng)前的左側(cè)右側(cè)真正的限制值
int moveLeftData = getPercentMax(mLeftCircleCenterX - getPaddingLeft() - mStrokeRadius);
int moveRightData = getPercentMax(mRightCircleCenterX - getPaddingLeft() - mStrokeRadius);
//順便賦值當(dāng)前的真正值,可以讓頂部文字顯示
leftValue = Math.min(moveLeftData, maxValue);
rightValue = Math.min(moveRightData, maxValue);
} else if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) {
//計(jì)算當(dāng)前的左側(cè)右側(cè)真正的限制值
int moveLeftData = getPercentMax(mLeftCircleCenterX - getPaddingLeft() - mStrokeRadius);
int moveRightData = getPercentMax(mRightCircleCenterX - getPaddingLeft() - mStrokeRadius);
//賦值方便回調(diào)
leftValue = Math.min(moveLeftData, maxValue);
rightValue = Math.min(moveRightData, maxValue);
if (mListener != null) mListener.onMoveValue(leftValue, rightValue);
//移除消息,并重新開始延時(shí)關(guān)閉頂部彈窗
removeCallbacks(dismissRunnable);
postDelayed(dismissRunnable, 1000);
}
...
//頂部的文本框矩陣也要居中布局
mTopDialogRect.left = (mRightCircleCenterX + mLeftCircleCenterX) / 2 - mTopDialogWidth / 2;
mTopDialogRect.right = (mRightCircleCenterX + mLeftCircleCenterX) / 2 + mTopDialogWidth / 2;
//全部的事件處理完畢,變量賦值完成之后,開始重繪
invalidate();
return true;
}
//頂部彈窗的顯示
Runnable dismissRunnable = new Runnable() {
@Override
public void run() {
if (!isRectDialogShowing) {
isShowRectDialog = false;
}
postInvalidate();
}
};總體來說繪制和動(dòng)態(tài)賦值矩陣的left right并不算太難,相比兩個(gè)圓的觸摸事件要相對(duì)簡(jiǎn)單一點(diǎn)。
實(shí)現(xiàn)的效果就是如下:

后記
當(dāng)然后面如果我們有更對(duì)的需求還能繼續(xù)繪制一些東西,例如:

如果我們想在圖片紅框處添加固定的最小值和最大值,也簡(jiǎn)單,我們直接drawText到指定的位置即可,我們不是已經(jīng)有進(jìn)度條Rect的 left 和 right 了嗎?就可以很方便的添加文本。
由于是我自用的一個(gè)控件,目前需求并沒有更多的要求就并沒有進(jìn)行更多的擴(kuò)展,輪子已經(jīng)在這里了,如果大家有興趣也可以很方便的修改的。大家跟著一起復(fù)習(xí)一遍,是不是感覺自定義的繪制和自定義的事件已經(jīng)入門了呢 - -!
到此這篇關(guān)于Android自定義View實(shí)現(xiàn)價(jià)格區(qū)間選擇控件的文章就介紹到這了,更多相關(guān)Android區(qū)間選擇控件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android逆向入門之常見Davlik字節(jié)碼解析
Dalvik是Google公司自己設(shè)計(jì)用于Android平臺(tái)的虛擬機(jī)。Dalvik虛擬機(jī)是Google等廠商合作開發(fā)的Android移動(dòng)設(shè)備平臺(tái)的核心組成部分之一,本篇文章我們來詳細(xì)解釋常見Davlik字節(jié)碼2021-11-11
Android通過Webservice操作sqlserver數(shù)據(jù)庫實(shí)例代碼
這篇文章主要介紹了Android通過Webservice操作sqlserver數(shù)據(jù)庫的相關(guān)知識(shí),對(duì)webservice操作數(shù)據(jù)庫相關(guān)知識(shí)感興趣的朋友一起學(xué)習(xí)吧2016-01-01
Android之ImageSwitcher的實(shí)例詳解
這篇文章主要介紹了Android之ImageSwitcher的實(shí)例詳解的相關(guān)資料,這里提供實(shí)例幫助大家理解這個(gè)控件的功能,希望能幫助到大家,需要的朋友可以參考下2017-08-08
Android仿微信公眾號(hào)文章頁面加載進(jìn)度條
這篇文章主要為大家詳細(xì)介紹了Android仿微信公眾號(hào)文章頁面加載進(jìn)度條,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-06-06
Android?RecyclerView曝光采集的實(shí)現(xiàn)方法
這篇文章主要為大家詳細(xì)介紹了Android?RecyclerView曝光采集的實(shí)現(xiàn)方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01
Android開發(fā)中的9個(gè)常見錯(cuò)誤和解決方法
這篇文章主要介紹了Android開發(fā)中的9個(gè)常見錯(cuò)誤和解決方法,這是Android開發(fā)中最常見的9個(gè)錯(cuò)誤,經(jīng)過各種各樣的整理,以及和熱心網(wǎng)友討論總結(jié)而來,需要的朋友可以參考下2015-01-01
Android系統(tǒng)進(jìn)程間通信Binder機(jī)制在應(yīng)用程序框架層的Java接口源代碼分析
本文主要介紹 Android系統(tǒng)進(jìn)程間通信Binder機(jī)制Java 接口源碼分析,這里詳細(xì)介紹了如何實(shí)現(xiàn)Binder 機(jī)制和Java接口直接的通信,有興趣的小伙伴可以參考下2016-08-08

