風(fēng)起
最近的項目需要用到一個雙向范圍選擇器,遂自己操刀并做下記錄
介紹
范圍選擇器要實現(xiàn)的功能就是進行范圍選擇规丽,并提供接口向調(diào)用者暴露所選最小最大值蒲牧,由于項目只是需要一個普通的范圍選擇器,所以并沒有其他的花哨的動畫特效 duang ~(為自己的技窮找一個借口)
實現(xiàn)
確定范圍選擇器需要哪些自定義屬性赌莺,并在 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>接下來接是創(chuàng)建范圍選擇器冰抢,LcRangeView 繼承自 View ,并實現(xiàn) LcRangeView 的三個構(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 中拿到這些屬性的賦值并處理挎扰,當然為了避免調(diào)用者沒有給這之中的哪個屬性賦值而產(chǎ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ù)巢音,接下來就是測量選擇器的大小遵倦,重寫 onMeasure() 方法。首先試想一下官撼,自適應(yīng)情況控件的寬高應(yīng)該是多大梧躺,寬的話我們就填充完屏幕,高呢傲绣,選擇球的高度掠哥,外部大小決定好了就該考慮一下內(nèi)部的測量巩踏,標刻線應(yīng)該為控件正中間位置,即兩個球心的連接線续搀,寬則為控件左右邊各空出一個球的半徑位置以保證球在最左或最右顯示不完整塞琼。
@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) ,繪制所需的參數(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),我們目前要操作的是兩個球霜大,那么我們就得判斷球是否被觸摸到构哺,我這里觸摸的范圍是剛好裝下球的正方形,你也適當?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; }
-
寫好了判斷战坤,接下來就是實現(xiàn)拖動效果了曙强,當手指按下時,就判斷是否觸摸到了球途茫,觸摸了那個球碟嘴,記錄下狀態(tài);當手指抬起時囊卜,都將觸摸狀態(tài)置為 false 娜扇;當手指滑動時,根據(jù)觸摸狀態(tài)執(zhí)行相應(yī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ù)來處理滑動邏輯栅组,我們要先知道球的滑動范圍雀瓢,
minBall 的滑動范圍為 標準線的起點 -- maxBall 的球心位置,
maxBall 的滑動位置為 maxBall 的球心位置 -- 標準線的終點玉掸。
(如果需要讓兩個球不重疊刃麸,可以邊界增加一個球的寬度)
確定球的新位置后,調(diào)用 invalidate() 進行重繪
當確定為正在移動球的時候司浪,即使脫離本控件的的范圍一樣可以更新視圖private void moveToMinPosition(MotionEvent event) { if (event.getX() < mMaxPosition && event.getX() >= mLineStartX) { mMinPosition = (int) event.getX(); invalidate(); /** 配合 10 一起看泊业,這個必須判斷是否為空,如果調(diào)用者不監(jiān)聽會導(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ù)滑動來實時更新我們的范圍值脱吱,一開始我們就拿到了總范圍值,然后根據(jù)滑動比例獲取范圍值
計算公式
min 值:(minBall 位置 - 標準線起點)/ 標準線長度 * 總范圍值
max 值:(maxBall 位置 - 標準線起點)/ 標準線長度 * 總范圍值
-
范圍值我們拿到了认罩,最后一步結(jié)束范圍值提供給調(diào)用者,這個部分大家都很熟悉了续捂,直接貼
public interface RangeChangeListener { void onMinChange(int minValue); void onMaxChange(int maxValue); } public void setRangeChangeListener(RangeChangeListener rangeChangeListener) { mRangeChangeListener = rangeChangeListener; }
總結(jié)
到此為止一個簡單的范圍選擇器就完成了垦垂,由于最近還在趕其他項目宦搬,所以目前先這么簡陋的吧,如果有其他需要還可以更加完善劫拗,如多點操作间校,在標準線上點擊實現(xiàn)球的位置跳轉(zhuǎn),變化動畫等页慷。沒什么技術(shù)含量憔足,純粹寫寫文記錄開發(fā)經(jīng)歷而已。(有空補上源碼圖片)