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