本文簡介:在github上找了很多seekbar,有些庫具有至關複雜的功能,因此我想本身寫一個簡單易用的seekbar。本文主要講述爲何要自定義view,自定義view的大致步驟,編寫重難點。git
因爲工做上的須要,咱們每每須要實現某種特殊的佈局或者界面效果,這時候官方沒有提供相應的控件支持,須要咱們繼承view或者其它view類擴展。通常初學者入門能夠先嚐試組合view,即先本身利用多個官方控件拼裝成須要的效果,而後內置邏輯(參考本人的數量加減view)。也就是把abc等多個view組合在一塊兒使用,比include方式多了內置邏輯的好處。(具體範例參考本人其它博客)github
接下來本文講述的是如何自定義一個seekbar。先看效果圖,以下。canvas
1)根據最終效果圖或者需求方提供的功能說明等,去分析界面效果包含哪些動做,好比手勢(點擊,觸摸移動),要顯示的圖形形狀、文本(矩形,原型,弧形,隨圖形一塊兒繪製的文本等等,都要仔細分析),拆解view圖形爲小的模塊。bash
2)好比本文的seekbar,明顯分爲3個部分,一個是後面刻度的進度條,一個是當前的進度條。還有一個圓形按鈕。而後手指點擊刻度條,會根據點擊位置當前進度跳轉至此,而且圓形按鈕也是如此。有一個特殊的需求是能夠圓角也能夠無圓角,而且圓形按鈕無關緊要。因此須要2個標記boolean去區分。須要注意的一點是,按照習慣通常圓形按鈕的圓心的x所在座標應該是在白色的當前進度的最右邊x座標。ide
3)根據圖片,咱們能夠得出,3個模塊的繪製都是本身有自身的大小控制,而爲了適配左右padding,因此的繪製進度條時,要預留padding。 而上下padding,我不許備處理,直接讓seekbar繪製在縱向的中間便可。即縱座標y中心點都是height/2,而且限制3個模塊的最大高度爲view的高度,避免繪製出界。函數
主要方法有onmeasure、ondraw、ontouchevent、構造函數。自定義view通常圍繞這幾個方法進行處理,構造函數裏獲取自定義屬性的值,初始化paint等對象,初始化一些view參數。ondraw進行繪製圖形,這個主要有drawarc等方法,這個很少講,自行搜索相關方法總覽。ontouchevent就是處理點擊座標,而後觸發一些繪製操做或響應某個方法動做。對於viewgroup的話還有onlayout等方法。佈局
先準備本view須要的自定義屬性,3個模塊的高度大小、是否圓角、顏色等。tickBar是刻度條,circlebutton是圓形按鈕,progress就是當前進度,代碼以下。測試
<!--自定義 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參數了。TypedArray對象必定要記得attr.recycle();關閉,通常textsize是getDimension,而高度大小什麼的是獲取getDimensionPixelOffset,view自己測試出來的也是px值,可是settextsize的方法須要傳入dp或者sp值。我在initview方法裏初始化所須要的paint對象,避免ondraw反覆繪製裏new對象耗費沒必要要的內存。可能初學者不清楚RectF是什麼東西,你百度一下會死啊。。。代碼以下。動畫
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();//矩形,一會根據這個繪製刻度條在這個矩形內
mProgressRecf = new RectF();
mCircleRecf = new RectF();
}
複製代碼
因爲本view沒有太大必要編寫onmeasure方法去適配wrapcontent。因此接下來就是ondraw裏進行繪製了。首先咱們先繪製刻度條,首先獲取當前view的高寬,刻度條設置的高寬,而後計算y座標中心,計算出剛纔RectF矩形範圍。要設置上下左右的座標起點,左就是getPaddingLeft()做爲起點,即默認自定義view支持paddingleft的設置。top的起點就是(mViewHeight - mTickBarHeight) / 2,即含義是繪製在view縱座標y的中心點,而後tickbar高度今後點分爲上下2半。同理求出橫向的終點的x座標以及底部座標等ui
@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);
複製代碼
同理處理進度條部分的繪製,這個比剛纔多了一層邏輯,起點依舊,可是終點x(矩形的right座標)須要根據當前進度計算。mSelectProgress 是當前進度值,mMaxProgress 是最大值,mStartProgress是默認起點表明多少刻度值,好比1-10的seekbar效果(起點是1,終點是10)。求出比值而後乘以view自己的實際繪製範圍的寬度(上面代碼有計算),加上paddingleft,得出矩形的終點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);
複製代碼
同理求出圓形按鈕的座標範圍
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控制圓角。重點說明的是 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);
// 下面這行是實現水平居中,drawText對應改成傳入targetRect.centerX()
canvas.drawText(String.valueOf(mSelectProgress), mCircleRecf.centerX
(), baseline,
mCircleButtonTextPaint);
}
}
複製代碼
這裏主要是依賴onTouchEvent判斷手勢,當event知足某個觸摸條件就進行獲取當前座標計算進度。本view是ACTION_MOVE、ACTION_DOWN時觸發。isEnabled判斷是否設置setEnabled屬性,若是設置則屏蔽觸摸繪製,這是個人特殊需求。judgePosition()主要是根據x座標進行計算進度。BigDecimal 是處理四捨五入,大概發生進度變化時從新繪製自身view。return true;是爲了消費觸摸事件。(觸摸事件分發機制,請移步大牛的博客)
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) {
//若是設置不可用,則禁用觸摸設置進度
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: 觸摸結束,通知監聽器-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 超出座標範圍:");
progress = mMaxProgress;
}
} else if (x < start) {
// Log.i(TAG, "judgePosition: x < start 超出座標範圍:");
progress = 0;
}
if (progress != mSelectProgress) {
//發生變化才通知view從新繪製
setSelectProgress(progress, false);
}
}
複製代碼
下面是一些主要的set方法,用來更新view。
/**
* 設置當前選中的值
*
* @param selectProgress 進度
*/
public void setSelectProgress(int selectProgress) {
this.setSelectProgress(selectProgress, true);
}
/**
* 設置當前選中的值
*
* @param selectProgress 進度
* @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();
}
/**
* 計算當前選中的進度條的值
*
* @param selectProgress 進度
*/
private void getSelectProgressValue(int selectProgress) {
mSelectProgress = selectProgress;
if (mSelectProgress > mMaxProgress) {
mSelectProgress = mMaxProgress;
} else if (mSelectProgress <= mStartProgress) {
mSelectProgress = mStartProgress;
}
}
複製代碼
自此本seekbar基本講述完畢,觀看下面源碼,能夠了解詳細的內容,每一個字段都有註釋,初學者能夠進行源碼查看。 源碼地址:github.com/389273716/h…
下一篇預告: 刻度盤view,支持外部倒計時控制,支持觸摸移動,點擊,帶動畫,支持配置界面元素,適配屏幕。