自定義view之seekbar
本文簡介:在github上找了不少seekbar贩耐,有些庫具備相當(dāng)復(fù)雜的功能狭魂,所以我想自己寫一個(gè)簡單易用的seekbar。本文主要講述為什么要自定義view肋层,自定義view的大體步驟橄妆,編寫重難點(diǎn)。
1倒彰、為什么要自定義view
由于工作上的需要审洞,我們往往需要實(shí)現(xiàn)某種特殊的布局或者界面效果,這時(shí)候官方?jīng)]有提供相應(yīng)的控件支持待讳,需要我們繼承view或者其它view類擴(kuò)展芒澜。一般初學(xué)者入門可以先嘗試組合view,即先自己利用多個(gè)官方控件拼裝成需要的效果创淡,然后內(nèi)置邏輯(參考本人的數(shù)量加減view)痴晦。也就是把a(bǔ)bc等多個(gè)view組合在一起使用,比include方式多了內(nèi)置邏輯的好處琳彩。(具體范例參考本人其它博客)
接下來本文講述的是如何自定義一個(gè)seekbar誊酌。先看效果圖,如下露乏。
2碧浊、分析要繪制的自定義view
1)根據(jù)最終效果圖或者需求方提供的功能說明等,去分析界面效果包含哪些動(dòng)作瘟仿,比如手勢(shì)(點(diǎn)擊箱锐,觸摸移動(dòng)),要顯示的圖形形狀劳较、文本(矩形驹止,原型,弧形观蜗,隨圖形一起繪制的文本等等臊恋,都要仔細(xì)分析),拆解view圖形為小的模塊墓捻。
2)比如本文的seekbar抖仅,明顯分為3個(gè)部分,一個(gè)是后面刻度的進(jìn)度條砖第,一個(gè)是當(dāng)前的進(jìn)度條撤卢。還有一個(gè)圓形按鈕。然后手指點(diǎn)擊刻度條厂画,會(huì)根據(jù)點(diǎn)擊位置當(dāng)前進(jìn)度跳轉(zhuǎn)至此,并且圓形按鈕也是如此拷邢。有一個(gè)特殊的需求是可以圓角也可以無圓角袱院,并且圓形按鈕可有可無。所以需要2個(gè)標(biāo)記boolean去區(qū)分。需要注意的一點(diǎn)是忽洛,按照習(xí)慣一般圓形按鈕的圓心的x所在坐標(biāo)應(yīng)該是在白色的當(dāng)前進(jìn)度的最右邊x坐標(biāo)腻惠。
3)根據(jù)圖片,我們可以得出欲虚,3個(gè)模塊的繪制都是自己有自身的大小控制集灌,而為了適配左右padding,所以的繪制進(jìn)度條時(shí)复哆,要預(yù)留padding欣喧。
而上下padding,我不準(zhǔn)備處理梯找,直接讓seekbar繪制在縱向的中間即可唆阿。即縱坐標(biāo)y中心點(diǎn)都是height/2,并且限制3個(gè)模塊的最大高度為view的高度锈锤,避免繪制出界驯鳖。
3、自定義view主要方法介紹
主要方法有onmeasure久免、ondraw浅辙、ontouchevent、構(gòu)造函數(shù)阎姥。自定義view一般圍繞這幾個(gè)方法進(jìn)行處理记舆,構(gòu)造函數(shù)里獲取自定義屬性的值,初始化paint等對(duì)象丁寄,初始化一些view參數(shù)氨淌。ondraw進(jìn)行繪制圖形,這個(gè)主要有drawarc等方法伊磺,這個(gè)不多講盛正,自行搜索相關(guān)方法總覽。ontouchevent就是處理點(diǎn)擊坐標(biāo)屑埋,然后觸發(fā)一些繪制操作或響應(yīng)某個(gè)方法動(dòng)作豪筝。對(duì)于viewgroup的話還有onlayout等方法。
4摘能、開始繪制
先準(zhǔn)備本view需要的自定義屬性续崖,3個(gè)模塊的高度大小、是否圓角团搞、顏色等严望。tickBar是刻度條,circlebutton是圓形按鈕逻恐,progress就是當(dāng)前進(jìn)度住涉,代碼如下。
<!--自定義 seekbar-->
<declare-styleable name="NumTipSeekBar">
<attr name="tickBarHeight" format="dimension"/>
<attr name="tickBarColor" format="color"/>
<attr name="circleButtonColor" format="color"/>
<attr name="circleButtonTextColor" format="color"/>
<attr name="circleButtonTextSize" format="dimension"/>
<attr name="circleButtonRadius" format="dimension"/>
<attr name="progressHeight" format="dimension"/>
<attr name="progressColor" format="color"/>
<attr name="selectProgress" format="integer"/>
<attr name="startProgress" format="integer"/>
<attr name="maxProgress" format="integer"/>
<attr name="isShowButtonText" format="boolean"/>
<attr name="isShowButton" format="boolean"/>
<attr name="isRound" format="boolean"/>
</declare-styleable>
接下來就是獲取自定義屬性促煮,然后初始化view參數(shù)了。TypedArray對(duì)象一定要記得attr.recycle();關(guān)閉姆涩,一般textsize是getDimension,而高度大小什么的是獲取getDimensionPixelOffset惭每,view本身測(cè)試出來的也是px值骨饿,但是settextsize的方法需要傳入dp或者sp值。我在initview方法里初始化所需要的paint對(duì)象台腥,避免ondraw反復(fù)繪制里new對(duì)象耗費(fèi)不必要的內(nèi)存宏赘。可能初學(xué)者不清楚RectF是什么東西览爵,你百度一下會(huì)死啊置鼻。。蜓竹。代碼如下箕母。
public NumTipSeekBar(Context context) {
this(context, null);
}
public NumTipSeekBar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public NumTipSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
/**
* 初始化view的屬性
*
* @param context
* @param attrs
*/
private void init(Context context, AttributeSet attrs) {
TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.NumTipSeekBar);
mTickBarHeight = attr.getDimensionPixelOffset(R.styleable
.NumTipSeekBar_tickBarHeight, getDpValue(8));
mTickBarColor = attr.getColor(R.styleable.NumTipSeekBar_tickBarColor, getResources()
.getColor(R.color.orange_f6));
mCircleButtonColor = attr.getColor(R.styleable.NumTipSeekBar_circleButtonColor,
getResources().getColor(R.color.white));
mCircleButtonTextColor = attr.getColor(R.styleable.NumTipSeekBar_circleButtonTextColor,
getResources().getColor(R.color.purple_82));
mCircleButtonTextSize = attr.getDimension(R.styleable
.NumTipSeekBar_circleButtonTextSize, getDpValue(16));
mCircleButtonRadius = attr.getDimensionPixelOffset(R.styleable
.NumTipSeekBar_circleButtonRadius, getDpValue(16));
mProgressHeight = attr.getDimensionPixelOffset(R.styleable
.NumTipSeekBar_progressHeight, getDpValue(20));
mProgressColor = attr.getColor(R.styleable.NumTipSeekBar_progressColor,
getResources().getColor(R.color.white));
mSelectProgress = attr.getInt(R.styleable.NumTipSeekBar_selectProgress, 0);
mStartProgress = attr.getInt(R.styleable.NumTipSeekBar_startProgress, 0);
mMaxProgress = attr.getInt(R.styleable.NumTipSeekBar_maxProgress, 10);
mIsShowButtonText = attr.getBoolean(R.styleable.NumTipSeekBar_isShowButtonText, true);
mIsShowButton = attr.getBoolean(R.styleable.NumTipSeekBar_isShowButton, true);
mIsRound = attr.getBoolean(R.styleable.NumTipSeekBar_isRound, true);
initView();
attr.recycle();
}
private void initView() {
mProgressPaint = new Paint();
mProgressPaint.setColor(mProgressColor);
mProgressPaint.setStyle(Paint.Style.FILL);
mProgressPaint.setAntiAlias(true);
mCircleButtonPaint = new Paint();
mCircleButtonPaint.setColor(mCircleButtonColor);
mCircleButtonPaint.setStyle(Paint.Style.FILL);
mCircleButtonPaint.setAntiAlias(true);
mCircleButtonTextPaint = new Paint();
mCircleButtonTextPaint.setTextAlign(Paint.Align.CENTER);
mCircleButtonTextPaint.setColor(mCircleButtonTextColor);
mCircleButtonTextPaint.setStyle(Paint.Style.FILL);
mCircleButtonTextPaint.setTextSize(mCircleButtonTextSize);
mCircleButtonTextPaint.setAntiAlias(true);
mTickBarPaint = new Paint();
mTickBarPaint.setColor(mTickBarColor);
mTickBarPaint.setStyle(Paint.Style.FILL);
mTickBarPaint.setAntiAlias(true);
mTickBarRecf = new RectF();//矩形,一會(huì)根據(jù)這個(gè)繪制刻度條在這個(gè)矩形內(nèi)
mProgressRecf = new RectF();
mCircleRecf = new RectF();
}
由于本view沒有太大必要編寫onmeasure方法去適配wrapcontent俱济。所以接下來就是ondraw里進(jìn)行繪制了嘶是。首先我們先繪制刻度條,首先獲取當(dāng)前view的高寬蛛碌,刻度條設(shè)置的高寬聂喇,然后計(jì)算y坐標(biāo)中心,計(jì)算出剛才RectF矩形范圍蔚携。要設(shè)置上下左右的坐標(biāo)起點(diǎn)希太,左就是getPaddingLeft()作為起點(diǎn),即默認(rèn)自定義view支持paddingleft的設(shè)置酝蜒。top的起點(diǎn)就是(mViewHeight - mTickBarHeight) / 2誊辉,即含義是繪制在view縱坐標(biāo)y的中心點(diǎn),然后tickbar高度從此點(diǎn)分為上下2半亡脑。同理求出橫向的終點(diǎn)的x坐標(biāo)以及底部坐標(biāo)等
@Overrid
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
initValues(width, height);
// do........
}
private void initValues(int width, int height) {
mViewWidth = width - getPaddingRight() - getPaddingLeft();
mViewHeight = height;
if (mTickBarHeight > mViewHeight) {
//如果刻度條的高度大于view本身的高度的1/2堕澄,則顯示不完整,所以處理下霉咨。
mTickBarHeight = mViewHeight;
}
mTickBarRecf.set(getPaddingLeft(), (mViewHeight - mTickBarHeight) / 2,
mViewWidth + getPaddingLeft(), mTickBarHeight / 2 +
mViewHeight / 2);
同理處理進(jìn)度條部分的繪制蛙紫,這個(gè)比剛才多了一層邏輯,起點(diǎn)依舊途戒,但是終點(diǎn)x(矩形的right坐標(biāo))需要根據(jù)當(dāng)前進(jìn)度計(jì)算坑傅。mSelectProgress 是當(dāng)前進(jìn)度值,mMaxProgress 是最大值喷斋,mStartProgress是默認(rèn)起點(diǎn)代表多少刻度值唁毒,比如1-10的seekbar效果(起點(diǎn)是1矢渊,終點(diǎn)是10)。求出比值然后乘以view本身的實(shí)際繪制范圍的寬度(上面代碼有計(jì)算)枉证,加上paddingleft,得出矩形的終點(diǎn)x移必。
mCirclePotionX = (float) (mSelectProgress - mStartProgress) /
(mMaxProgress - mStartProgress) * mViewWidth + getPaddingLeft();
if (mProgressHeight > mViewHeight) {
//如果刻度條的高度大于view本身的高度的1/2室谚,則顯示不完整,所以處理下崔泵。
mProgressHeight = mViewHeight;
}
mProgressRecf.set(getPaddingLeft(), (mViewHeight - mProgressHeight) / 2,
mCirclePotionX, mProgressHeight / 2 + mViewHeight / 2);
同理求出圓形按鈕的坐標(biāo)范圍
if (mCircleButtonRadius > mViewHeight / 2) {
//如果圓形按鈕的半徑大于view本身的高度的1/2秒赤,則顯示不完整,所以處理下憎瘸。
mCircleButtonRadius = mViewHeight / 2;
}
mCircleRecf.set(mCirclePotionX - mCircleButtonRadius, mViewHeight / 2 -
mCircleButtonRadius / 2,
mCirclePotionX + mCircleButtonRadius, mViewHeight / 2 +
mCircleButtonRadius / 2);
開始繪制入篮,mIsRound控制圓角。重點(diǎn)說明的是 Paint.FontMetricsInt處理文本的居中顯示幌甘。
代碼如下潮售。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
initValues(width, height);
if (mIsRound) {
canvas.drawRoundRect(mTickBarRecf, mProgressHeight / 2, mProgressHeight / 2,
mTickBarPaint);
canvas.drawRoundRect(mProgressRecf, mProgressHeight / 2, mProgressHeight / 2,
mProgressPaint);
} else {
canvas.drawRect(mTickBarRecf, mTickBarPaint);
canvas.drawRect(mProgressRecf, mProgressPaint);
}
// canvas.drawArc(mCircleRecf, 0, 360, true, mCircleButtonPaint);
if (mIsShowButton) {
canvas.drawCircle(mCirclePotionX, mViewHeight / 2, mCircleButtonRadius,
mCircleButtonPaint);
}
if (mIsShowButtonText) {
Paint.FontMetricsInt fontMetrics = mCircleButtonTextPaint.getFontMetricsInt();
int baseline = (int) ((mCircleRecf.bottom + mCircleRecf.top - fontMetrics.bottom -
fontMetrics
.top) / 2);
// 下面這行是實(shí)現(xiàn)水平居中,drawText對(duì)應(yīng)改為傳入targetRect.centerX()
canvas.drawText(String.valueOf(mSelectProgress), mCircleRecf.centerX
(), baseline,
mCircleButtonTextPaint);
}
}
5锅风、處理觸摸邏輯
這里主要是依賴onTouchEvent判斷手勢(shì)酥诽,當(dāng)event滿足某個(gè)觸摸條件就進(jìn)行獲取當(dāng)前坐標(biāo)計(jì)算進(jìn)度。本view是ACTION_MOVE皱埠、ACTION_DOWN時(shí)觸發(fā)肮帐。isEnabled判斷是否設(shè)置setEnabled屬性,如果設(shè)置則屏蔽觸摸繪制边器,這是我的特殊需求训枢。judgePosition()主要是根據(jù)x坐標(biāo)進(jìn)行計(jì)算進(jìn)度。BigDecimal 是處理四舍五入忘巧,大概發(fā)生進(jìn)度變化時(shí)重新繪制自身view恒界。return true;是為了消費(fèi)觸摸事件。(觸摸事件分發(fā)機(jī)制袋坑,請(qǐng)移步大牛的博客)
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) {
//如果設(shè)置不可用仗处,則禁用觸摸設(shè)置進(jìn)度
return false;
}
float x = event.getX();
float y = event.getY();
// Log.i(TAG, "onTouchEvent: x:" + x);
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
judgePosition(x);
return true;
case MotionEvent.ACTION_DOWN:
judgePosition(x);
return true;
case MotionEvent.ACTION_UP:
if (mOnProgressChangeListener != null) {
Log.i(TAG, "onTouchEvent: 觸摸結(jié)束,通知監(jiān)聽器-mSelectProgress:"+mSelectProgress);
mOnProgressChangeListener.onChange(mSelectProgress);
}
return true;
default:
break;
}
return super.onTouchEvent(event);
}
private void judgePosition(float x) {
float end = getPaddingLeft() + mViewWidth;
float start = getPaddingLeft();
int progress = mSelectProgress;
// Log.i(TAG, "judgePosition: x-start:" + (x - start));
// Log.i(TAG, "judgePosition: start:" + start + " end:" + end + " mMaxProgress:" +
// mMaxProgress);
if (x >= start) {
double result = (x - start) / mViewWidth * (float) mMaxProgress;
BigDecimal bigDecimal = new BigDecimal(result).setScale(0, BigDecimal.ROUND_HALF_UP);
// Log.i(TAG, "judgePosition: progress:" + bigDecimal.intValue() + " result:" + result
// + " (x - start) / end :" + (x - start) / end);
progress = bigDecimal.intValue();
if (progress > mMaxProgress) {
// Log.i(TAG, "judgePosition:x > end 超出坐標(biāo)范圍:");
progress = mMaxProgress;
}
} else if (x < start) {
// Log.i(TAG, "judgePosition: x < start 超出坐標(biāo)范圍:");
progress = 0;
}
if (progress != mSelectProgress) {
//發(fā)生變化才通知view重新繪制
setSelectProgress(progress, false);
}
}
下面是一些主要的set方法枣宫,用來更新view婆誓。
/**
* 設(shè)置當(dāng)前選中的值
*
* @param selectProgress 進(jìn)度
*/
public void setSelectProgress(int selectProgress) {
this.setSelectProgress(selectProgress, true);
}
/**
* 設(shè)置當(dāng)前選中的值
*
* @param selectProgress 進(jìn)度
* @param isNotifyListener 是否通知progresschangelistener
*/
public void setSelectProgress(int selectProgress, boolean isNotifyListener) {
getSelectProgressValue(selectProgress);
Log.i(TAG, "mSelectProgress: " + mSelectProgress + " mMaxProgress: " +
mMaxProgress);
if (mOnProgressChangeListener != null && isNotifyListener) {
mOnProgressChangeListener.onChange(mSelectProgress);
}
invalidate();
}
/**
* 計(jì)算當(dāng)前選中的進(jìn)度條的值
*
* @param selectProgress 進(jìn)度
*/
private void getSelectProgressValue(int selectProgress) {
mSelectProgress = selectProgress;
if (mSelectProgress > mMaxProgress) {
mSelectProgress = mMaxProgress;
} else if (mSelectProgress <= mStartProgress) {
mSelectProgress = mStartProgress;
}
}
自此本seekbar基本講述完畢,觀看下面源碼也颤,可以了解詳細(xì)的內(nèi)容洋幻,每個(gè)字段都有注釋,初學(xué)者可以進(jìn)行源碼查看翅娶。
源碼地址:https://github.com/389273716/highscalabilityseekbar
下一篇預(yù)告:
刻度盤view文留,支持外部倒計(jì)時(shí)控制好唯,支持觸摸移動(dòng),點(diǎn)擊燥翅,帶動(dòng)畫骑篙,支持配置界面元素,適配屏幕森书。