還記得幾年前寫過一個雙向seekbar嗎,不足的是不支援步長擴充,老的雙向seekbar連結
這幾天正好做需求,要擴充一個支援步長,一次隻能滑動50個,松開,即刻回彈到距離它最近的機關坐标上,WFK.那麼我們要開車了.
需求理一下
- 雙向拖動
- 定義步長
- 回彈确定最終值
- 文字描述不能因為太近而遮蓋
- …..還有一堆擴充屬性不說了
老規矩,效果圖如下
直接看做出來的成品
接下裡就手把手,我們來實作一下這個支援定制步長的拖拽seekbar
GIF豈不是更爽
實作步驟
- 分析自定義屬性擴充
- 定義并且擷取屬性
- 計算要繪制的區域的坐标以及範圍
- 暴露接口傳回資料
1.自定義屬性
<declare-styleable name="MyElongScaleSeekBar">
<attr name="scale_progress_normal_color" format="color" />
<attr name="scale_progress_section_color" format="color" />
<attr name="scale_left_ball_bg_color" format="color" />
<attr name="scale_left_ball_stroke_color" format="color" />
<attr name="scale_left_ball_stroke_with" format="dimension" />
<attr name="scale_right_ball_bg_color" format="color" />
<attr name="scale_right_ball_stroke_color" format="color" />
<attr name="scale_right_ball_stroke_with" format="dimension" />
<attr name="scale_ball_radio" format="dimension" />
<attr name="scale_ball_shadow_radio" format="dimension" />
<attr name="scale_ball_shadow_color" format="color" />
<attr name="scale_seek_height" format="dimension" />
<attr name="scale_left_text_color" format="dimension" />
<attr name="scale_left_text_size" format="dimension" />
<attr name="scale_right_text_color" format="dimension" />
<attr name="scale_right_text_size" format="dimension" />
<attr name="scale_text_margin_ball" format="dimension" />
<attr name="scale_ball_stick_height" format="dimension" />
<attr name="scale_ball_stick_width" format="dimension" />
<attr name="scale_ball_stick_margin" format="dimension" />
<attr name="scale_ball_stick_color" format="color" />
<attr name="scale_progress_unit" format="integer" />
<attr name="scale_symbol_front" format="string" />
</declare-styleable>
這裡就不多解釋了,思路就是和你用Android原生textview一樣,盡可能的考慮到每個屬性的擴充,比如這次我們seekbar的步數,小球的顔色,陰影的顔色,等等.
2.擷取自定義屬性
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyElongScaleSeekBar, , R.style.default_scale_seekbar_style);
int indexCount = typedArray.getIndexCount();
for (int i = ; i < indexCount; i++) {
int attr = typedArray.getIndex(i);
switch (attr) {
case R.styleable.MyElongScaleSeekBar_scale_progress_normal_color:
scaleProgressNormalColor = typedArray.getColor(attr, Color.BLACK);
break;
....省略.....
自定義屬性的擷取,其實就是view在購置的時候,我們用context拿到TypedArray,去循環周遊我們聲明的擴充屬性,循環到一個屬性,根據你定義的類型,去getvalue即可擷取到
3.确定要繪制的圖案分幾部分組成
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 背景進度條
drawScaleSeekNormal(canvas);
// 目前選中的進度
drawScaleSeekSection(canvas);
// 左邊的球
drawLeftBall(canvas);
// 右邊的球
drawRightBall(canvas);
// 左邊球的文字
drawLeftText(canvas);
// 右邊球的文字
drawRightText(canvas);
// 說了這麼多,其實就是畫圓圈和畫方塊的小把戲
}
4.我們定義了要畫哪些東西,接下裡就是調用canvas的api
// 畫方塊
canvas.drawRect(...)
// 畫圓圈
canvas.drawCircle(...)
// 本效果就用到了這兩個api
5.繪制之前,我們要重寫view的測量.這個要根據你的效果圖自己設定,我們view的寬度肯定是充滿父布局的,是以直接外面給match就行主要的是高度,因為效果圖分兩部分,上半部分是文字區域,下半部分是拖拽區域是以view的高度要在測量模式中改成EXACTLY模式,因為高度你自己從定義的屬性裡面能擷取到.
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int measureHeight;
if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) {
//确定view的高度(文字高度+距離+拖拽高度)
measureHeight = Math.max(scaleLeftTextSize, scaleRightTextSize) + + scaleTextMarginBall + scaleBallRadio * ;
heightMeasureSpec = MeasureSpec.makeMeasureSpec(measureHeight, MeasureSpec.EXACTLY);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
6.确定繪制的圖形的原始位置,展現靜态圖
思路很簡單,把UI給你的效果圖,在一張紙上畫一下,
你用鉛筆畫的過程,就是你轉換代碼确定位置的過程,
你的紙就是Android的坐标系,畫圓圈要确定圓心位置,畫方塊要确定方塊的四個頂點.那麼接下來你隻要确定了圓心位置,和方塊位置定點,那麼靜态圖就出來了.
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//球的半徑包含描邊
radioWithStroke = scaleBallRadio + scaleLeftBallStrokeWith * F;
xCoordinateUnit = radioWithStroke * ;
valueEntities = calculateAllXCoordinate(scaleProgressUnit, w - radioWithStroke * , maxValue);
if (valueEntities != null && valueEntities.size() > ) {
currentLeft = valueEntities.get();
currentRight = valueEntities.get(valueEntities.size() - );
leftDesc = creatCurrentDataDesc(currentLeft);
rightDesc = creatCurrentDataDesc(currentRight);
if (this.seekBarDragListener != null) {
this.seekBarDragListener.seekMoveValue(currentLeft.value, currentRight.value);
}
}
if (currentLeft != null && currentRight != null) {
// 背景進度條的區域
scaleSeekNormalRectF = new RectF();
scaleSeekNormalRectF.left = ;
float top = h - (scaleBallRadio + scaleLeftBallStrokeWith * F) - scaleSeekHeight * F - scaleBallShadowRadio;
scaleSeekNormalRectF.top = top;
scaleSeekNormalRectF.bottom = top + scaleSeekHeight;
scaleSeekNormalRectF.right = w;
// 左邊球的圓心坐标
leftBallPoint = new SeekPoint();
leftBallPoint.x = currentLeft.xCoordinat + radioWithStroke;
leftBallPoint.y = h - radioWithStroke - scaleBallShadowRadio;
// 左邊球中間的猴三棍
calculateLeftSticks();
// 右邊球的坐标
rightBallPoint = new SeekPoint();
rightBallPoint.x = currentRight.xCoordinat + radioWithStroke;
rightBallPoint.y = h - radioWithStroke - scaleBallShadowRadio;
// 右邊球中間的猴三棍
calculateRightSticks();
// 選中背景條的間距部分
scaleSeekSectionRectF = new RectF();
scaleSeekSectionRectF.left = leftBallPoint.x - radioWithStroke;
scaleSeekSectionRectF.right = rightBallPoint.x - radioWithStroke;
scaleSeekSectionRectF.top = scaleSeekNormalRectF.top;
scaleSeekSectionRectF.bottom = scaleSeekNormalRectF.bottom;
// 圓球的路徑和區域
scaleLeftBallPath = new Path();
scaleLeftBallPath.addCircle(leftBallPoint.x, leftBallPoint.y, radioWithStroke, Path.Direction.CW);
scaleRightBallPath = new Path();
scaleRightBallPath.addCircle(rightBallPoint.x, rightBallPoint.y, radioWithStroke, Path.Direction.CW);
scaleLeftBallRegion = updateRegionByPath(scaleLeftBallPath);
scaleRightBallRegion = updateRegionByPath(scaleRightBallPath);
}
}
7.最後一步就是拖拽,将靜态圖改變成動态重新整理的靜态圖
因為你每次的拖拽都執行的move,那麼成千上萬的move組成的靜态圖在一zhen一zhen的過的時候,是不是所謂的連續動畫,類似于小時候的動畫書…
- 動起來,根據ontouch的位置x坐标實時計算圓心的坐标和矩形的四個頂點
- 确定拖拽的是哪個圓圈?
// 圓球的路徑和區域
scaleLeftBallPath = new Path();
scaleLeftBallPath.addCircle(leftBallPoint.x, leftBallPoint.y, radioWithStroke, Path.Direction.CW);
Region region = new Region();
if (path != null) {
RectF tempRectF = new RectF();
path.computeBounds(tempRectF, true);
region.setPath(path, new Region((int) tempRectF.left, (int) tempRectF.top, (int) tempRectF.right, (int) tempRectF.bottom));
}
return region;
我們把圓圈的坐标放到path裡面,根據path生成一個他的region,那麼我們在觸摸的時候,能拿到目前觸摸的坐标x,y,那麼我們在ACTION_DOWN裡面就可以判斷你觸摸的點是左邊的球還是右邊的球,那麼拖動那個就對那個進行坐标幀重新整理即可.
case MotionEvent.ACTION_DOWN:
boolean touchLeftBall = scaleLeftBallRegion.contains((int) event.getX(), (int) event.getY()) && !scaleRightBallRegion.contains((int) event.getX(), (int) event.getY());
boolean touchRightBall = scaleRightBallRegion.contains((int) event.getX(), (int) event.getY()) && !scaleLeftBallRegion.contains((int) event.getX(), (int) event.getY());
8.最後一步,彈性回到目前坐标對應的步長.
這個就是一個經常面試的時候一個小問題,給你一個任意數字,在一個數組裡面找到與他最接近的數字并傳回.
對應到我們onTouch裡面就是,你觸摸的任意一點,要從你生成的步長資料集合中找到與之對應的最接近的值即可
public UnitValueEntity binarySearchKey(List<UnitValueEntity> data, int targetNum) {
if (data != null && data.size() > ) {
int left = , right = ;
for (right = data.size() - ; left != right; ) {
int midIndex = (right + left) / ;
int mid = (right - left);
int midValue = (int) data.get(midIndex).xCoordinat;
if (targetNum == midValue) {
return data.get(midIndex);
}
if (targetNum > midValue) {
left = midIndex;
} else {
right = midIndex;
}
if (mid <= ) {
break;
}
}
UnitValueEntity rightnum = data.get(right);
UnitValueEntity leftnum = data.get(left);
return Math.abs((rightnum.xCoordinat - leftnum.xCoordinat) / ) > Math.abs(rightnum.xCoordinat - targetNum) ? rightnum : leftnum;
}
return null;
}
// 我們這邊就用二分查找,來找到這個值,并且傳回.