github地址:https://github.com/zhou-you/EasySignSeekBar
簡(jiǎn)述
最近在工作上的需要乎澄,自定義了一個(gè)漂亮而強(qiáng)大的自定義view薇搁,但不僅僅只是一個(gè)SeekBar而已哦薪捍,一定要耐心看完。剛開始是不愿意自己去寫的,這東西太浪費(fèi)時(shí)間蹭沛,UI這東西不一定是個(gè)技術(shù)活,但一定是個(gè)細(xì)活章鲤。瀏覽了很多自定義控件摊灭,都沒有符合需要的,最終只能自己開擼败徊。實(shí)現(xiàn)了效果后想著看能不能也方便他人帚呼,如果其他人有類似的效果,修改下屬性配置就可以直接使用皱蹦,于是就分享出來萝挤,取名:EasySignSeekBar 御毅。
EasySignSeekBar
本庫主要提供一個(gè)漂亮而強(qiáng)大的自定義SeekBar,進(jìn)度變化由提示牌 (sign)展示,具有強(qiáng)大的屬性設(shè)置怜珍,支持設(shè)置section(節(jié)點(diǎn))端蛆、mark(標(biāo)記)、track(軌跡)酥泛、thumb(拖動(dòng)塊)今豆、progress(進(jìn)度)、sign(提示框)等功能
主要功能
- 強(qiáng)大的track(軌跡)和second track (選中軌跡)的最小值柔袁、最大值呆躲、軌跡粗細(xì),顏色等設(shè)置捶索;
- 靈活的數(shù)字顯示支持整數(shù)和小數(shù)插掂;
- 支持設(shè)置進(jìn)度單位,例如 10s,15km/h腥例、平方等辅甥;
- 支持手柄拖動(dòng)塊thumb半徑、顏色燎竖、陰影璃弄、透明度等;
- 支持節(jié)點(diǎn)個(gè)數(shù)构回、文字大小夏块、顏色設(shè)置;
- 支持自動(dòng)滾動(dòng)最近區(qū)段標(biāo)記節(jié)點(diǎn)纤掸;
- 支持指示牌寬高脐供、顏色、圓角半徑借跪、三角arrow指示患民、border邊框、跟隨thumb移動(dòng)等垦梆;
- 支持設(shè)置拖動(dòng)進(jìn)度監(jiān)聽回掉匹颤;
- ......
效果預(yù)覽
因GIF圖壓縮的原因動(dòng)畫看起來有些不流程和模糊。
![](https://github.com/zhou-you/EasySignSeekBar/blob/master/screenshot/1.gif?raw=true)
![](https://github.com/zhou-you/EasySignSeekBar/blob/master/screenshot/2.gif?raw=true)
![](https://github.com/zhou-you/EasySignSeekBar/blob/master/screenshot/3.gif?raw=true)
![](https://github.com/zhou-you/EasySignSeekBar/blob/master/screenshot/4.gif?raw=true)
用法介紹
build.gradle設(shè)置
dependencies {
compile 'com.zhouyou:signseekbar:1.0.1'
}
想查看所有版本托猩,請(qǐng)點(diǎn)擊下面地址印蓖。
xml
<com.zhouyou.view.seekbar.SignSeekBar
android:id="@+id/seek_bar"
android:layout_width="match_parent"
android:layout_height="16dp"
app:ssb_section_text_position="bottom_sides"
app:ssb_show_progress_in_float="false"
app:ssb_show_section_mark="false"
app:ssb_show_section_text="true"
app:ssb_show_sign="true"
app:ssb_show_thumb_text="false"
app:ssb_sign_arrow_height="5dp"
app:ssb_sign_arrow_width="10dp"
app:ssb_sign_border_color="@color/color_red"
app:ssb_sign_border_size="1dp"
app:ssb_sign_color="@color/color_gray"
app:ssb_sign_show_border="true"/>
java
signSeekBar.getConfigBuilder()
.min(0)
.max(4)
.progress(2)
.sectionCount(4)
.trackColor(ContextCompat.getColor(getContext(), R.color.color_gray))
.secondTrackColor(ContextCompat.getColor(getContext(), R.color.color_blue))
.thumbColor(ContextCompat.getColor(getContext(), R.color.color_blue))
.sectionTextColor(ContextCompat.getColor(getContext(), R.color.colorPrimary))
.sectionTextSize(16)
.thumbTextColor(ContextCompat.getColor(getContext(), R.color.color_red))
.thumbTextSize(18)
.signColor(ContextCompat.getColor(getContext(), R.color.color_green))
.signTextSize(18)
.autoAdjustSectionMark()
.sectionTextPosition(SignSeekBar.TextPosition.BELOW_SECTION_MARK)
.build();
回調(diào)
設(shè)置回調(diào)可以監(jiān)聽進(jìn)度變化情況。
signSeekBar.setOnProgressChangedListener(new SignSeekBar.OnProgressChangedListener() {
@Override
public void onProgressChanged(SignSeekBar signSeekBar, int progress, float progressFloat,boolean fromUser) {
//fromUser 表示是否是用戶觸發(fā) 是否是用戶touch事件產(chǎn)生
String s = String.format(Locale.CHINA, "onChanged int:%d, float:%.1f", progress, progressFloat);
progressText.setText(s);
}
@Override
public void getProgressOnActionUp(SignSeekBar signSeekBar, int progress, float progressFloat) {
String s = String.format(Locale.CHINA, "onActionUp int:%d, float:%.1f", progress, progressFloat);
progressText.setText(s);
}
@Override
public void getProgressOnFinally(SignSeekBar signSeekBar, int progress, float progressFloat,boolean fromUser) {
String s = String.format(Locale.CHINA, "onFinally int:%d, float:%.1f", progress, progressFloat);
progressText.setText(s + getContext().getResources().getStringArray(R.array.labels)[progress]);
}
});
Attributes
支持很多自定義屬性設(shè)置京腥,具體請(qǐng)看源碼赦肃。
主要實(shí)現(xiàn)思路介紹
概況
本庫自定義控件主要是用了Canvas相關(guān)的drawXXX系列方法、一些簡(jiǎn)單的算法和動(dòng)畫來完成的。比如拖動(dòng)軌跡他宛、滑塊thumb拖動(dòng)船侧、放大、自動(dòng)滾動(dòng)最近節(jié)點(diǎn)厅各、指示牌镜撩、區(qū)段節(jié)點(diǎn)標(biāo)記、進(jìn)度單位顯示等队塘。接下來會(huì)講解下主要的實(shí)現(xiàn)思路袁梗,對(duì)于自定義View的其它基本流程,屬性獲取和設(shè)置憔古、onMeasure的重寫等都不重點(diǎn)介紹遮怜,想了解完整流程請(qǐng)看源碼。
track(軌道)繪制
畫軌道比較簡(jiǎn)單鸿市,主要實(shí)現(xiàn)方式就是畫兩條不同顏色的線條(其實(shí)畫的是一條分為左右兩部分锯梁,銜接的地方是有個(gè)thumb遮擋著),主要是要求出滑動(dòng)thumb的中心點(diǎn)mThumbCenterX焰情,mThumbCenterX的計(jì)算非常重要陌凳,本庫的很多計(jì)算都是圍繞mThumbCenterX,mThumbCenterX是通過onTouchEvent事件MotionEvent
根據(jù)down烙样、move事件實(shí)時(shí)計(jì)算出中心點(diǎn)x坐標(biāo)冯遂。
// draw track
mPaint.setColor(mSecondTrackColor);
mPaint.setStrokeWidth(mSecondTrackSize);
canvas.drawLine(xLeft, yTop, mThumbCenterX, yTop, mPaint);
// draw second track
mPaint.setColor(mTrackColor);
mPaint.setStrokeWidth(mTrackSize);
canvas.drawLine(mThumbCenterX, yTop, xRight, yTop, mPaint);
track(軌道)接觸有效計(jì)算
MotionEvent的getX()和getY()獲得的永遠(yuǎn)是相對(duì)view的觸摸位置坐標(biāo)蕊肥,getRawX()和getRawY()獲得的是相對(duì)屏幕的位置谒获,軌道計(jì)算用的是getX,getY 相對(duì)于容器的位置坐標(biāo)x,y,計(jì)算x,y坐標(biāo)是否在軌道的矩形方框內(nèi),從而判斷是否在軌道上壁却。
private boolean isTrackTouched(MotionEvent event) {
return isEnabled() && event.getX() >= getPaddingLeft() && event.getX() <= getMeasuredWidth() - getPaddingRight()
&& event.getY() >= getPaddingTop() && event.getY() <= getMeasuredHeight() - getPaddingBottom();
}
thumb(滑塊)接觸有效計(jì)算
thumb就是軌道上的圓形滑塊批狱,如何判斷手指拖動(dòng)的區(qū)域是否在滑塊上呢,使用圓的標(biāo)準(zhǔn)方程(x-a)2+(y-b)2=r2來判斷
private boolean isThumbTouched(MotionEvent event) {
if (!isEnabled())
return false;
float mCircleR = isThumbOnDragging ? mThumbRadiusOnDragging : mThumbRadius;
float x = mTrackLength / mDelta * (mProgress - mMin) + mLeft;
float y = getMeasuredHeight() / 2f;
return (event.getX() - x) * (event.getX() - x) + (event.getY() - y) * (event.getY() - y)
<= (mLeft + mCircleR) * (mLeft + mCircleR);
}
thumb(滑塊)透明度實(shí)現(xiàn)
滑塊的透明度展东,是將滑塊的顏色值進(jìn)行計(jì)算加上alpha,求出一個(gè)新的顏色值赔硫,主要是使用Color這個(gè)工具類的方法,大家經(jīng)常用到的是Color的parseColor(@Size(min=1) String colorString)方法盐肃,庫中主要用的是Color的另一些方法alpha(int color)爪膊、red(int color)、green(int color)砸王、blue(int color)方法分別求出argb值推盛,求出的透明度經(jīng)過計(jì)算修改后,再用 Color.argb(alpha, r, g, b)組合得出一個(gè)新的顏色值谦铃。
/**
* 計(jì)算新的透明度顏色
*
* @param color 舊顏色
* @param ratio 透明度系數(shù)
*/
public int getColorWithAlpha(int color, float ratio) {
int newColor = 0;
int alpha = Math.round(Color.alpha(color) * ratio);
int r = Color.red(color);
int g = Color.green(color);
int b = Color.blue(color);
newColor = Color.argb(alpha, r, g, b);
return newColor;
}
thumb(滑塊) 最近節(jié)點(diǎn)位置計(jì)算方法
根據(jù)節(jié)點(diǎn)個(gè)數(shù)mSectionCount和兩個(gè)節(jié)點(diǎn)之間的間隔mSectionOffset耘成,與滑塊當(dāng)前位置mThumbCenterX的比較,求出最近一個(gè)節(jié)點(diǎn)的位置。
//計(jì)算最近節(jié)點(diǎn)位置瘪菌,mSectionCount:節(jié)點(diǎn)個(gè)數(shù)撒会,mSectionOffset:兩個(gè)節(jié)點(diǎn)間隔距離,mThumbCenterX:滑塊中心點(diǎn)位置
float x = 0;
for (i = 0; i <= mSectionCount; i++) {
x = i * mSectionOffset + mLeft;
if (x <= mThumbCenterX && mThumbCenterX - x <= mSectionOffset) {
break;
}
}
thumb(滑塊) 滾動(dòng)最近節(jié)點(diǎn)動(dòng)畫效果實(shí)現(xiàn)
滑塊自動(dòng)滾動(dòng)到最近節(jié)點(diǎn)增加了動(dòng)畫移動(dòng)效果师妙,使用ValueAnimator實(shí)現(xiàn)動(dòng)畫诵肛,Property Animation提供了Animator.AnimatorListener和Animator.AnimatorUpdateListener兩個(gè)監(jiān)聽器用于動(dòng)畫在播放過程中的重要?jiǎng)赢嬍录F渲蠥nimatorUpdateListener監(jiān)聽中onAnimationUpdate() 方法疆栏,動(dòng)畫每播放一幀時(shí)調(diào)用曾掂,在動(dòng)畫過程中,可偵聽此事件來獲取并使用 ValueAnimator 計(jì)算出來的屬性值壁顶。利用傳入事件的 ValueAnimator 對(duì)象珠洗,調(diào)用其 getAnimatedValue() 方法即可獲取當(dāng)前的屬性值,就是修改后滑塊的位置mThumbCenterX。此動(dòng)畫還配合有Interpolator若专,動(dòng)畫播放采用LinearInterpolator線性插值的方式執(zhí)行動(dòng)畫许蓖。插值器它定義了動(dòng)畫變化過程中的屬性變化規(guī)則,它根據(jù)時(shí)間比例因子計(jì)算出一個(gè)插值因子调衰,用于設(shè)定目標(biāo)對(duì)象的動(dòng)畫執(zhí)行是否為線性變化膊爪、非線性變化或先加速后減速等等。Android系統(tǒng)本身內(nèi)置了一些通用的Interpolator(插值器),如下:
類或接口名 | 說明 |
---|---|
AccelerateDecelerateInterpolator | 在動(dòng)畫開始與結(jié)束的地方速率改變比較慢嚎莉,在中間的時(shí)候加速 |
AccelerateInterpolator | 在動(dòng)畫開始的地方速率改變比較慢米酬,然后開始加速 |
AnticipateInterpolator | 開始的時(shí)候向后然后向前甩 |
AnticipateOvershootInterpolator | 開始的時(shí)候向后然后向前甩一定值后返回最后的值 |
BounceInterpolator | 動(dòng)畫結(jié)束的時(shí)候彈起 |
CycleInterpolator | 動(dòng)畫循環(huán)播放特定的次數(shù),速率改變沿著正弦曲線 |
DecelerateInterpolator | 在動(dòng)畫開始的地方快然后慢 |
LinearInterpolator | 以常量速率改變 |
OvershootInterpolator | 向前甩一定值后再回到原來位置 |
完整源碼:
private void autoAdjustSection() {
int i;
//計(jì)算最近節(jié)點(diǎn)位置趋箩,mSectionCount:節(jié)點(diǎn)個(gè)數(shù)赃额,mSectionOffset:兩個(gè)節(jié)點(diǎn)間隔距離,mThumbCenterX:滑塊中心點(diǎn)位置
float x = 0;
for (i = 0; i <= mSectionCount; i++) {
x = i * mSectionOffset + mLeft;
if (x <= mThumbCenterX && mThumbCenterX - x <= mSectionOffset) {
break;
}
}
BigDecimal bigDecimal = BigDecimal.valueOf(mThumbCenterX);
//BigDecimal setScale保留1位小數(shù)叫确,四舍五入跳芳,2.35變成2.4
float x_ = bigDecimal.setScale(1, BigDecimal.ROUND_HALF_UP).floatValue();
boolean onSection = x_ == x; // 就在section處,不作valueAnim竹勉,優(yōu)化性能
AnimatorSet animatorSet = new AnimatorSet();
ValueAnimator valueAnim = null;
if (!onSection) {
if (mThumbCenterX - x <= mSectionOffset / 2f) {
valueAnim = ValueAnimator.ofFloat(mThumbCenterX, x);
} else {
valueAnim = ValueAnimator.ofFloat(mThumbCenterX, (i + 1) * mSectionOffset + mLeft);
}
valueAnim.setInterpolator(new LinearInterpolator());
valueAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mThumbCenterX = (float) animation.getAnimatedValue();
mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin;
invalidate();
if (mProgressListener != null) {
mProgressListener.onProgressChanged(SignSeekBar.this, getProgress(), getProgressFloat(),true);
}
}
});
}
if (!onSection) {
animatorSet.setDuration(mAnimDuration).playTogether(valueAnim);
}
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin;
isThumbOnDragging = false;
isTouchToSeekAnimEnd = true;
invalidate();
if (mProgressListener != null) {
mProgressListener.getProgressOnFinally(SignSeekBar.this, getProgress(), getProgressFloat(),true);
}
}
@Override
public void onAnimationCancel(Animator animation) {
mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin;
isThumbOnDragging = false;
isTouchToSeekAnimEnd = true;
invalidate();
}
});
animatorSet.start();
}
采用BigDecimal處理小數(shù)
代碼中的小數(shù)采用BigDecimal來處理飞盆,只介紹setScale相關(guān)方法,其它更多方法可以自己去學(xué)習(xí)次乓,這里只是拋磚引玉吓歇。
BigDecimal.setScale()方法用于格式化小數(shù)點(diǎn)
setScale(1)表示保留一位小數(shù),默認(rèn)用四舍五入方式
setScale(1,BigDecimal.ROUND_DOWN)直接刪除多余的小數(shù)位票腰,如2.35會(huì)變成2.3
setScale(1,BigDecimal.ROUND_UP)進(jìn)位處理城看,2.35變成2.4
setScale(1,BigDecimal.ROUND_HALF_UP)四舍五入,2.35變成2.4
setScaler(1,BigDecimal.ROUND_HALF_DOWN)四舍五入丧慈,2.35變成2.3析命,如果是5則向下舍
Sign 提示框--三角形邊框繪制
單純的進(jìn)度提示框?qū)崿F(xiàn)比較簡(jiǎn)單主卫,主要是由矩形框+三角形組成,但是加邊框繪制的時(shí)候比較麻煩一點(diǎn)鹃愤,需要留出矩形和三角形交接的地方不能畫線簇搅,這里做了假象交接的地方其實(shí)額外繪制了三角形的底邊,顏色采用的是矩形庫填充的顏色软吐。三角形邊框繪制如下:
private void drawTriangleBoder(Canvas canvas, Point point1, Point point2, Point point3, Paint paint) {
triangleboderPath.reset();
triangleboderPath.moveTo(point1.x, point1.y);
triangleboderPath.lineTo(point2.x, point2.y);
paint.setColor(signPaint.getColor());
float value = mSignBorderSize / 6;
paint.setStrokeWidth(mSignBorderSize + 1f);
canvas.drawPath(triangleboderPath, paint);
triangleboderPath.reset();
paint.setStrokeWidth(mSignBorderSize);
triangleboderPath.moveTo(point1.x - value, point1.y - value);
triangleboderPath.lineTo(point3.x, point3.y);
triangleboderPath.lineTo(point2.x + value, point2.y - value);
paint.setColor(mSignBorderColor);
canvas.drawPath(triangleboderPath, paint);
}
Sign 提示框--進(jìn)度單位unit實(shí)現(xiàn)方式
進(jìn)度單位很多需求也是需要的瘩将,不是單純的用canvas.drawText來繪制。這里采用的是StaticLayout凹耙。使用Canvas的drawText繪制文本是不會(huì)自動(dòng)換行的姿现,即使一個(gè)很長(zhǎng)很長(zhǎng)的字符串,drawText也只顯示一行肖抱,超出部分被隱藏在屏幕之外备典。可以逐個(gè)計(jì)算每個(gè)字符的寬度意述,通過一定的算法將字符串分割成多個(gè)部分提佣,然后分別調(diào)用drawText一部分一部分的顯示, 但是這種顯示效率會(huì)很低荤崇。StaticLayout是android中處理文字換行的一個(gè)工具類拌屏,StaticLayout已經(jīng)實(shí)現(xiàn)了文本繪制換行處理,也支持標(biāo)簽屬性<small>术荤,m/s<sup>2</sup>倚喂,μmol/l,μ/l
從而實(shí)現(xiàn)強(qiáng)大靈活的單位設(shè)置瓣戚。
private void createValueTextLayout() {
String value = isShowProgressInFloat ? String.valueOf(getProgressFloat()) : String.valueOf(getProgress());
if (value != null && unit != null && !unit.isEmpty())
value += String.format(" <small>%s</small>", unit);
Spanned spanned = Html.fromHtml(value);
valueTextLayout = new StaticLayout(spanned, valueTextPaint, mSignWidth, Layout.Alignment.ALIGN_CENTER, 1, 0, false);
}
圓圈中心繪制文本
圓圈中心繪制文字端圈,高度是比較難控制的,特別是中文带兜,不能簡(jiǎn)單的通過bounds.height()來獲取高度的方式計(jì)算枫笛,需要先求出baseline這種方式來處理吨灭,求baseline的方式是固定的刚照。下面提供一個(gè)通用的方法:
/**
* 精確畫圓圈中心文字(通用方法),其中文字的高度是最難計(jì)算適配的喧兄,采用此方法无畔,可以完美解決
*
* @param canvas 畫板
* @param paint 畫筆panit
* @param centerX 圓圈中心X坐標(biāo)
* @param centerY 圓圈中心Y坐標(biāo)
* @param radius 半徑
* @param text 顯示的文本
*/
private void drawCircleText(Canvas canvas, Paint paint, float centerX, float centerY, float radius, String text) {
paint.setTextAlign(Paint.Align.LEFT);
Rect bounds = new Rect();
paint.getTextBounds(text, 0, text.length(), bounds);
Paint.FontMetricsInt fontMetrics = paint.getFontMetricsInt();
float baseline = centerY - radius + (2 * radius - fontMetrics.bottom + fontMetrics.top) / 2 - fontMetrics.top;
canvas.drawText(text, centerX - radius + radius - bounds.width() / 2, baseline, paint);
}
總結(jié)
歡迎大家Star of Fork,使用Gradle依賴很方便,也可以clone來試著按自己的想法修改吠冤,后期想法是再進(jìn)行優(yōu)化浑彰,直接繼承此view然后自己擴(kuò)展和修改。歡迎大家提出意見和更好的創(chuàng)意拯辙。
關(guān)于我
聯(lián)系方式
本群旨在為使用我github項(xiàng)目的人提供方便郭变,如果遇到問題歡迎在群里提問颜价。