Android自定義View實(shí)現(xiàn)價格區(qū)間選擇控件
前言
之前我們的復(fù)習(xí)中,我們已經(jīng)對原生 Canvas 的繪制有了詳細(xì)的了解,我們對事件的處理也有了簡單的了解,這一期我們就對繪制與事件的處理做更進(jìn)一步的實(shí)現(xiàn)。
如圖,我們需要做這么一個區(qū)間的選擇控件,此控件也是我們常用的控件,在一些篩選頁面,根據(jù)價格,數(shù)值進(jìn)行一些篩選的時候,我們需要設(shè)置一個最小值和一個最大值。然后取一段中間的區(qū)間值。
而這個控件的實(shí)現(xiàn)就是典型的自定義繪制與自定義事件處理的標(biāo)志性實(shí)現(xiàn)。我愿稱之為自定義View的筑基練習(xí),如果大家能從頭到尾實(shí)現(xiàn)一遍,那么對自定義流程基本上已經(jīng)駕輕就熟了。
慣例我們分析一下實(shí)現(xiàn)步驟:
- 左邊右邊的控制圓分別實(shí)現(xiàn),雖然一般情況下它們的屬性都是相同的,但是為了防止左右不同的圓,我們做好兼容處理。
- 中間的圓角矩形進(jìn)度條,我們也分為默認(rèn)的底色和選中的顏色。
- 對事件的處理,左右的圓形控件的移動處理。
- 其他的文本顯示。
- 自定義屬性的抽取與回調(diào)處理。
思路我們已經(jīng)有了,下面一步一步的來實(shí)現(xiàn)吧! Let's go
1、繪制靜態(tài)的圖形
關(guān)于靜態(tài)的效果繪制,我們已經(jīng)駕輕就熟了。 測量,畫筆,矩陣的初始化,繪制,一套流程下來,都已經(jīng)是固定的模板了。
進(jìn)度矩形條,左右圓形的一些資源,我們就能實(shí)現(xiàn)一個靜態(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; //左右兩個圓心位置 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)于測量還是按我們前面文字介紹說的來,我們先確定測量的模式與寬高,再計算一個最小的寬高,然后根據(jù)xml里面定義的測量模式來確定測量的寬高。
具體實(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; //計算的寬度與高度 int calWidthSize = getPaddingLeft() + mCircleRadius * 2 + getPaddingRight() + mCircleStrokeWidth * 2; int calHeightSize = getPaddingTop() + mCircleRadius * 2 + mCircleStrokeWidth * 2 + getPaddingBottom(); if (widthMode == MeasureSpec.EXACTLY) { //如果是精確模式使用測量的寬度 finalWidth = widthSize; } else if (widthMode == MeasureSpec.AT_MOST) { //如果是WrapContent使用計算的寬度 finalWidth = Math.min(widthSize, calWidthSize); } else { //其他模式使用計算的寬度 finalWidth = calWidthSize; } if (heightMode == MeasureSpec.EXACTLY) { //如果是精確模式使用測量的高度 finalHeight = heightSize; } else if (heightMode == MeasureSpec.AT_MOST) { //如果是WrapContent使用計算的高度 finalHeight = Math.min(heightSize, calHeightSize); } else { //其他模式使用計算的高度 finalHeight = calHeightSize; } //確定測量寬高 setMeasuredDimension(finalWidth, finalHeight); }
內(nèi)部有詳細(xì)的注釋,推薦大家寬度使用固定的數(shù)組,高度wrap_content。
測量完成之后當(dāng)顯示出來了,我們就可以對圓形和矩陣進(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,和左右限制圓的圓心和大小,我們就可以使用對的畫筆進(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); }
這幾個東西繪制出來,我們的效果就如下所示:
為了方便顯示大小,我在控件里加一個灰色的背景為了方便觀看整個控件的大小。
靜態(tài)的實(shí)現(xiàn)之后我們就要開始讓兩邊的限制圓形動起來。
2、讓兩邊的限制圓動起來
我們在 onDraw 的方法中可以得知,動態(tài)的成員變量就是兩個圓的X軸坐標(biāo)即為 mLeftCircleCenterX 和 mRightCircleCenterX ,那么中間的進(jìn)度線條的繪制則是根據(jù) mSelectedCornerLineRect 的矩陣來繪制的。矩陣的left 和 right 也是根據(jù) mLeftCircleCenterX 和 mRightCircleCenterX 來計算的。
所以我們的最終目的就是動態(tài)的記錄當(dāng)前事件中的 mLeftCircleCenterX 和 mRightCircleCenterX 值,但是有左右兩個控制圓,我們怎么判斷移動的是哪一個圓呢?
先上一個判斷方法。
/** * 判斷當(dāng)前移動的是左側(cè)限制圓,還是右側(cè)限制圓 * * @param downX 按下的坐標(biāo)點(diǎn) * @return true表示按下的左側(cè),false表示按下的右側(cè) */ private boolean checkTouchCircleLeftOrRight(float downX) { //用一個取巧的方法,如果當(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)我們移動的時候我們怎么計算呢?通常常用的方法是把進(jìn)度線條分為幾份,計算每一份的長度。我們把這些變量提取出來:
private int mStrokeRadius; //半徑+邊框的總值 private int slice = 5; //代表整體進(jìn)度分為多少份 private float perSlice; //每一份所占的長度 private int maxValue = 100; //最大值,默認(rèn)為100 private int minValue = 0; //最小值,默認(rèn)為0 private float downX; private boolean touchLeftCircle;
通過入口方法對其賦值,并且顯示出來后對每一份長度進(jìn)行計算:
/** * 設(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; //計算每一份對應(yīng)的距離 perSlice = (realWidth - mStrokeRadius * 2) * 1f / slice;
到處我們就能寫OnTouch方法了,這是核心方法,我們慢一點(diǎn)來。
我們先只對按下的事件做處理:
@Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { //按下的時候記錄當(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)動態(tài)計算的 mSelectedCornerLineRect.left = mLeftCircleCenterX; mSelectedCornerLineRect.right = mRightCircleCenterX; //全部的事件處理完畢,變量賦值完成之后,開始重繪 invalidate(); return true; }
按下的過程中對,最大最小值做判斷,并且賦值進(jìn)度矩陣的 left 和 right ,那么我們就能實(shí)現(xiàn)指定的點(diǎn)擊效果,如下圖所示:
這只是點(diǎn)擊呢,效果太挫了,我們想要按著滑動怎么辦?那我們就需要重寫Move事件。
3、動態(tài)滑動并計算當(dāng)前的區(qū)間值
滑動相對來說是比較難得,我們要處理兩個限制圓的滾動,并且當(dāng)它們兩個圓碰撞在一起的時候,我們要處理交換的邏輯,并且還需要注意滑動邊界的處理。
@Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { //按下的時候記錄當(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è)滑動 touchLeftCircle = false; mRightCircleCenterX = (int) moveX; } } else { //右側(cè)到最左邊 if (mRightCircleCenterX == getPaddingLeft() + mStrokeRadius) { touchLeftCircle = false; mRightCircleCenterX = getPaddingLeft() + mStrokeRadius; } else { //交換左側(cè)滑動 touchLeftCircle = true; mLeftCircleCenterX = (int) moveX; } } } else { //如果是正常的移動 if (touchLeftCircle) { //滑動左邊限制圓,如果左邊圓超過右邊圓,那么把右邊圓賦值給左邊圓,如果沒超過就賦值當(dāng)前的moveX mLeftCircleCenterX = mLeftCircleCenterX - mRightCircleCenterX >= 0 ? mRightCircleCenterX : moveX; } else { //滑動右邊限制圓,如果右邊圓超過左邊圓,那么把左邊圓賦值給右邊圓,如果沒超過就賦值當(dāng)前的moveX mRightCircleCenterX = mRightCircleCenterX - mLeftCircleCenterX <= 0 ? mLeftCircleCenterX : moveX; } } } //對所有的手勢效果進(jìn)行過濾操作,不能超過最大最小值 limitMinAndMax(); //中間的進(jìn)度矩形是根據(jù)兩邊圓心點(diǎn)動態(tài)計算的 mSelectedCornerLineRect.left = mLeftCircleCenterX; mSelectedCornerLineRect.right = mRightCircleCenterX; //全部的事件處理完畢,變量賦值完成之后,開始重繪 invalidate(); return true; }
主要需要處理的是交換身位的方法,當(dāng)兩個圓相撞的時候,需要賦值處理,交換X的賦值,然后切換 touchLeftCircle 的值,然后對另一個圓進(jìn)行移動。
需要注意的是我們一定要在賦值之前對最大值與最小值進(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; } }
此時大致的效果已經(jīng)出來了,效果如圖:
4、計算當(dāng)前值與回調(diào)處理
我們一直都是計算的是兩個圓的中心點(diǎn)X軸的計算,那么我們真正選中的值是多少呢?總不能把X軸坐標(biāo)給調(diào)用者吧。所以我們需要通過滑動的百分比動態(tài)的計算具體的值。
//根據(jù)移動的距離計算當(dāng)前的值 private int getPercentMax(float distance) { //計算此時的位置坐標(biāo)對應(yīng)的距離能分多少份 int lineLength = getWidth() - getPaddingLeft() - getPaddingRight() - mStrokeRadius * 2; distance = distance <= 0 ? 0 : (distance >= lineLength ? lineLength : distance); //計算滑動的百分比 float percentage = distance / lineLength; return (int) (percentage * maxValue); }
那我們需要在Move事件中一直回調(diào)嗎?沒必要,我們在取消的時候,或者說事件完畢的時候,當(dāng)用戶選好了區(qū)間之后,我們回調(diào)一次即可。
if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) { //計算當(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); }
我們在Activity中通過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í)時文本顯示與后續(xù)的擴(kuò)展
這么看起來倒是似模似樣了,我們的需求是在拖動的時候?qū)崟r在頂部展示一個彈窗,展示當(dāng)前的值,這怎么搞?
其實(shí)就是在頂部繪制一個圓角矩形,在矩形內(nèi)部繪制文本,然后我們通過左右限制圓的位置計算出中間 的位置,讓頂部的圓角矩形在中間位置顯示不就行了嘛。開干
先定義需要用到的成員變量:
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)度的真正距離)計算得出 private Path mTrianglePath; //畫小三角形路徑 private int mTriangleLength = 15; //等邊三角形邊長 private int mTriangleHeight; //等邊三角形的高 private Paint textPaint;
然后我們在初始化畫筆與資源的時候,初始化文本的畫筆和頂部彈窗的矩陣:
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); }
由于我們加了頂部的高度,那么我們就需要在測量的時候也要把高度加上去
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { ... //計算的寬度與高度 int calWidthSize = getPaddingLeft() + mCircleRadius * 2 + getPaddingRight() + mCircleStrokeWidth * 2; int calHeightSize = getPaddingTop() + mTopDialogCornerRadius * 2 + mTriangleHeight + mTopDialogSpaceToProgress + mCircleRadius * 2 + mCircleStrokeWidth * 2 + getPaddingBottom(); ... }
顯示的時候我們計算真正的高度,我們比設(shè)置的再高出一點(diǎn)點(diǎn)方便展示。并且對頂部彈窗的矩陣賦值
@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ù)是頂部的矩陣,所以我們在onTouch中,還需要對頂部的矩陣進(jìn)行重新動態(tài)賦值,才能讓他動起來:
@Override public boolean onTouchEvent(MotionEvent event) { ... } else if (event.getAction() == MotionEvent.ACTION_MOVE) { //計算當(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) { //計算當(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); //移除消息,并重新開始延時關(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(); } };
總體來說繪制和動態(tài)賦值矩陣的left right并不算太難,相比兩個圓的觸摸事件要相對簡單一點(diǎn)。
實(shí)現(xiàn)的效果就是如下:
后記
當(dāng)然后面如果我們有更對的需求還能繼續(xù)繪制一些東西,例如:
如果我們想在圖片紅框處添加固定的最小值和最大值,也簡單,我們直接drawText到指定的位置即可,我們不是已經(jīng)有進(jìn)度條Rect的 left 和 right 了嗎?就可以很方便的添加文本。
由于是我自用的一個控件,目前需求并沒有更多的要求就并沒有進(jìn)行更多的擴(kuò)展,輪子已經(jīng)在這里了,如果大家有興趣也可以很方便的修改的。大家跟著一起復(fù)習(xí)一遍,是不是感覺自定義的繪制和自定義的事件已經(jīng)入門了呢 - -!
到此這篇關(guān)于Android自定義View實(shí)現(xiàn)價格區(qū)間選擇控件的文章就介紹到這了,更多相關(guān)Android區(qū)間選擇控件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android 實(shí)現(xiàn)搶購倒計時功能的示例
這篇文章主要介紹了Android 實(shí)現(xiàn)搶購倒計時功能的示例,幫助大家更好的理解和學(xué)習(xí)使用Android開發(fā),感興趣的朋友可以了解下2021-03-03android studio 的下拉菜單Spinner使用詳解
這篇文章主要介紹了android studio 的下拉菜單Spinner使用詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12Android 進(jìn)度條自動前進(jìn)效果的實(shí)現(xiàn)代碼
這篇文章主要介紹了Android 進(jìn)度條自動前進(jìn)效果,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07Android RecyclerView上拉加載更多功能回彈實(shí)現(xiàn)代碼
這篇文章主要介紹了Android RecyclerView上拉加載更多功能回彈實(shí)現(xiàn)代碼,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2017-02-02Android基于Sensor感應(yīng)器獲取重力感應(yīng)加速度的方法
這篇文章主要介紹了Android基于Sensor感應(yīng)器獲取重力感應(yīng)加速度的方法,涉及Android使用Sensor類實(shí)現(xiàn)感應(yīng)重力變化的功能,需要的朋友可以參考下2015-12-12Android利用RecyclerView實(shí)現(xiàn)全選、置頂和拖拽功能示例
列表控件可以說是我們絕大部分App中都會使用的,為了提升交互樂趣,我們經(jīng)常需要在列表中加入置頂、拖拽等操作,下面這篇文章主要介紹了Android利用RecyclerView如何實(shí)現(xiàn)全選、置頂和拖拽功能的相關(guān)資料,需要的朋友可以參考借鑒,下面來一起看看吧。2017-04-04Android自定義listview布局實(shí)現(xiàn)上拉加載下拉刷新功能
這篇文章主要介紹了Android自定義listview布局實(shí)現(xiàn)上拉加載下拉刷新功能,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-12-12android如何添加桌面圖標(biāo)和卸載程序后自動刪除圖標(biāo)
android如何添加桌面圖標(biāo)和卸載程序后自動刪除桌面圖標(biāo),這是一個應(yīng)用的安裝與卸載過程對桌面圖標(biāo)的操作,下面與大家分享下具體是如何實(shí)現(xiàn)的,感興趣的朋友可以參考下哈2013-06-06