本文简介:在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,支持外部倒计时控制,支持触摸移动,点击,带动画,支持配置界面元素,适配屏幕。