風(fēng)起
最近的項(xiàng)目需要用到一個(gè)雙向范圍選擇器,遂自己操刀并做下記錄
介紹
范圍選擇器要實(shí)現(xiàn)的功能就是進(jìn)行范圍選擇,并提供接口向調(diào)用者暴露所選最小最大值,由于項(xiàng)目只是需要一個(gè)普通的范圍選擇器,所以并沒有其他的花哨的動(dòng)畫特效 duang ~(為自己的技窮找一個(gè)借口)
實(shí)現(xiàn)
確定范圍選擇器需要哪些自定義屬性,并在 res/values 目錄下新建一個(gè)資源文件 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>接下來接是創(chuàng)建范圍選擇器,LcRangeView 繼承自 View ,并實(shí)現(xiàn) LcRangeView 的三個(gè)構(gòu)造方法
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);
}之前我們定義選擇器所需要的屬性,那么現(xiàn)在我就要在 View 中拿到這些屬性的賦值并處理,當(dāng)然為了避免調(diào)用者沒有給這之中的哪個(gè)屬性賦值而產(chǎn)生繪圖顯示異常,我們也默認(rèn)得給這些屬性默認(rèn)值
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;
}-
拿到了繪圖所需要的數(shù)據(jù),接下來就是測(cè)量選擇器的大小,重寫 onMeasure() 方法。首先試想一下,自適應(yīng)情況控件的寬高應(yīng)該是多大,寬的話我們就填充完屏幕,高呢,選擇球的高度,外部大小決定好了就該考慮一下內(nèi)部的測(cè)量,標(biāo)刻線應(yīng)該為控件正中間位置,即兩個(gè)球心的連接線,寬則為控件左右邊各空出一個(gè)球的半徑位置以保證球在最左或最右顯示不完整。
@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("測(cè)試", "看看y"+mMidY); mLineStartX = markBallRadius; mLineEndX = mLineLength + markBallRadius; mMinPosition = mLineStartX; mMaxPosition = mLineEndX; } -
測(cè)量好了就該繪圖了,重寫 onDraw() 方法,我們要明確的畫圖的順序,標(biāo)準(zhǔn)刻度線 -> 選擇刻度線 -> 選擇球,想好了怎么畫就該準(zhǔn)備筆 (paint) 和 (canvas) ,繪制所需的參數(shù)在前面已經(jīng)定義過了,形狀一出立馬感覺成功了一半,
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); } -
圖形已經(jīng)出現(xiàn),我們目前要操作的是兩個(gè)球,那么我們就得判斷球是否被觸摸到,我這里觸摸的范圍是剛好裝下球的正方形,你也適當(dāng)?shù)迷龃笥|控面積(如果你的球需要繪制很小的話)
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; } -
寫好了判斷,接下來就是實(shí)現(xiàn)拖動(dòng)效果了,當(dāng)手指按下時(shí),就判斷是否觸摸到了球,觸摸了那個(gè)球,記錄下狀態(tài);當(dāng)手指抬起時(shí),都將觸摸狀態(tài)置為 false ;當(dāng)手指滑動(dòng)時(shí),根據(jù)觸摸狀態(tài)執(zhí)行相應(yīng)的的滑動(dòng)
@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; } -
繼續(xù)來處理滑動(dòng)邏輯,我們要先知道球的滑動(dòng)范圍,
minBall 的滑動(dòng)范圍為 標(biāo)準(zhǔn)線的起點(diǎn) -- maxBall 的球心位置,
maxBall 的滑動(dòng)位置為 maxBall 的球心位置 -- 標(biāo)準(zhǔn)線的終點(diǎn)。
(如果需要讓兩個(gè)球不重疊,可以邊界增加一個(gè)球的寬度)
確定球的新位置后,調(diào)用 invalidate() 進(jìn)行重繪
當(dāng)確定為正在移動(dòng)球的時(shí)候,即使脫離本控件的的范圍一樣可以更新視圖private void moveToMinPosition(MotionEvent event) { if (event.getX() < mMaxPosition && event.getX() >= mLineStartX) { mMinPosition = (int) event.getX(); invalidate(); /** 配合 10 一起看,這個(gè)必須判斷是否為空,如果調(diào)用者不監(jiān)聽會(huì)導(dǎo)致空指針異常 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)); } **/ } } -
現(xiàn)在界面的雛形已經(jīng)出現(xiàn)了,接下來我們要根據(jù)滑動(dòng)來實(shí)時(shí)更新我們的范圍值,一開始我們就拿到了總范圍值,然后根據(jù)滑動(dòng)比例獲取范圍值
計(jì)算公式
min 值:(minBall 位置 - 標(biāo)準(zhǔn)線起點(diǎn))/ 標(biāo)準(zhǔn)線長度 * 總范圍值
max 值:(maxBall 位置 - 標(biāo)準(zhǔn)線起點(diǎn))/ 標(biāo)準(zhǔn)線長度 * 總范圍值
-
范圍值我們拿到了,最后一步結(jié)束范圍值提供給調(diào)用者,這個(gè)部分大家都很熟悉了,直接貼
public interface RangeChangeListener { void onMinChange(int minValue); void onMaxChange(int maxValue); } public void setRangeChangeListener(RangeChangeListener rangeChangeListener) { mRangeChangeListener = rangeChangeListener; }
總結(jié)
到此為止一個(gè)簡單的范圍選擇器就完成了,由于最近還在趕其他項(xiàng)目,所以目前先這么簡陋的吧,如果有其他需要還可以更加完善,如多點(diǎn)操作,在標(biāo)準(zhǔn)線上點(diǎn)擊實(shí)現(xiàn)球的位置跳轉(zhuǎn),變化動(dòng)畫等。沒什么技術(shù)含量,純粹寫寫文記錄開發(fā)經(jīng)歷而已。(有空補(bǔ)上源碼圖片)