最近闲的很,如是写下了本篇博客。
自定义控件基本步骤如下,反正基本上我觉得按我这个套路来写,基本上都能很快上手。
推荐按顺序阅读。。
第一部分,构造函数,我相信这个大家都应该是明白的。初始化和自定义属性无关必要的成员变量,mDensity是屏幕密度,将dp转px时要用到。
/**
* 第一部分 构造函数 所有的自定义view都可以用下面这段代码直接copy改下名字就行
**/
//当不需要使用xml声明或者不需要使用inflate动态加载时候,实现此构造函数即可
public RulerView(Context context) {
super(context, null);
}
//当需要在xml中声明此控件,则需要实现此构造函数。并且在构造函数中把自定义的属性与控件的数据成员连接起来。
public RulerView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
//接受一个style资源
public RulerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//所有的构造函数都会汇总到这一步,API21的会走到一个包含4个参数的构造函数。
mContext = context;
mDensity = mContext.getResources().getDisplayMetrics().density;
initRulerView(attrs);
}
第二部分,获取自定义属性,初始化需要获取自定义属性之后的画笔等等之类的成员变量。
值得注意的是,这个地方获取属性值的时候有2种写法,一种是我注释了的,一种是没有注释的,写法的区别是:我注释过的那段代码,如果属性在使用控件的时候没有在xml中使用的话,赋值的方法是不走的。那么就没有默认值。。我没注释的那种写法是肯定会走,获取不到就给默认值。推荐第二种,以免出错。
此处的name我看过很多博客,都要么没说,要么说这个名字随便写。其实不对,这个名字,
代表了在这个declare-styleable之间的属性只能在RulerView中使用,其他的空间是看
不到这个属性的,同学们可以去试一下
关于format值的一些说明
1,reference :通过@dimen @color @drawable @layout 等等获取属性值
2,color : 颜色值 "#ff0" "#ff0000" "#0ff" "#00ff00000"
3,boolean :布尔值 true false
4, dimension : 尺寸 50dp 50px 50sp
5,float :浮点值 0.5
6,integer : 整型值 30
7,string : 字符串 abs
8,fraction :百分数 25%
9,enum :枚举
10,flag :位或运算
/**
* 第二部分,获取自定义属性
*
* @param attrs
*/
private void initRulerView(AttributeSet attrs) {
if (null != attrs) {
TypedArray ta = mContext.obtainStyledAttributes(attrs, R.styleable.RulerView);
// int count = ta.getIndexCount();
//
// for (int i = 0; i < count; i++) {
// int attr = ta.getIndex(i);
// switch (attr) {
// case R.styleable.RulerView_cursorColor:
// mCursorColor = ta.getColor(attr, DEFAULT_CURSOR_COLOR);
// break;
//
// }
// }
mBorderWidth = ta.getDimension(R.styleable.RulerView_borderWidth, CustomViewUtil.dp2px(mContext, BORDER_WIDTH));
mCornerRadius = ta.getDimension(R.styleable.RulerView_cornerRadius, CustomViewUtil.dp2px(mContext, CORNER_RADIUS));
mBorderColor = ta.getColor(R.styleable.RulerView_borderColor, BORDER_COLOR);
mCursorWidth = ta.getDimension(R.styleable.RulerView_cursorWidth, CustomViewUtil.dp2px(mContext, DEFAULT_CURSOR_WIDTH));
if (ta.getDrawable(R.styleable.RulerView_cursorDrawable) == null) {
mCursorColor = ta.getColor(R.styleable.RulerView_cursorColor, DEFAULT_CURSOR_COLOR);
} else {
mCursorDrawable = ta.getDrawable(R.styleable.RulerView_cursorDrawable);
}
mCalibrationTailsLength = ta.getDimension(R.styleable.RulerView_calibrationTailsLength, CustomViewUtil.dp2px(mContext, CALIBRATION_TAILS_LENGTH));
mCalibrationTailsWidth = ta.getDimension(R.styleable.RulerView_calibrationTailsWidth, CustomViewUtil.dp2px(mContext, CALIBRATION_TAILS_WIDTH));
mCalibrationTailsColor = ta.getColor(R.styleable.RulerView_calibrationTailsColor, CALIBRATION_TAILS_COLOR);
mCalibrationTailsDistance = ta.getDimension(R.styleable.RulerView_calibrationTailsDistance, CustomViewUtil.dp2px(mContext, CALIBRATION_TAILS_DISTANCE));
mImportantTailsLength = ta.getDimension(R.styleable.RulerView_importantTailsLength, CustomViewUtil.dp2px(mContext, IMPORTANT_TAILS_LENGTH));
mImportantTailsWidth = ta.getDimension(R.styleable.RulerView_importantTailsWidth, CustomViewUtil.dp2px(mContext, IMPORTANT_TAILS_WIDTH));
mImportantTailsColor = ta.getColor(R.styleable.RulerView_importantTailsColor, IMPORTANT_TAILS_COLOR);
mImportantTailsGap = ta.getInteger(R.styleable.RulerView_importantTailsGap, IMPORTANT_TAILS_GAP);
maxValue = ta.getInteger(R.styleable.RulerView_maxValue, MAX_VALUE);
minValue = ta.getInteger(R.styleable.RulerView_minValue, MIN_VALUE);
currentValue = ta.getInteger(R.styleable.RulerView_currentValue, DEFAULT_VALUE);
mValueDistanceImportantTails = ta.getDimension(R.styleable.RulerView_valueDistanceImportantTails, CustomViewUtil.dp2px(mContext, VALUE_DISTANCE_IMPORTANT_TAILS));
mValueColor = ta.getColor(R.styleable.RulerView_valueColor, VALUE_COLOR);
mValueSize = ta.getDimension(R.styleable.RulerView_valueSize, CustomViewUtil.sp2px(mContext, VALUE_SIZE));
dampNumber = ta.getFloat(R.styleable.RulerView_dampNumber, DAMP_NUMBER);
ta.recycle();
}
if (mCalibrationTailsDistance <= Math.max(mImportantTailsWidth, mCalibrationTailsWidth)) {
throw new IllegalArgumentException("mCalibrationTailsDistance must bigger than Math.max(mImportantTailsWidth,mCalibrationTailsWidth)");
}
mCalibrationTailsPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mImportantTailsPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCursorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCalibrationTailsPaint.setColor(mCalibrationTailsColor);
mCalibrationTailsPaint.setStrokeWidth(mCalibrationTailsWidth);
mImportantTailsPaint.setColor(mImportantTailsColor);
mImportantTailsPaint.setStrokeWidth(mImportantTailsWidth);
mCursorPaint.setColor(mCursorColor);
mCursorPaint.setStrokeWidth(mCursorWidth);
mTextPaint.setTextSize(mValueSize);
mTextPaint.setColor(mValueColor);
mTextPaint.setStyle(Paint.Style.FILL);
mTouchSlop = ViewConfiguration.get(mContext).getScaledPagingTouchSlop();
mScroller = new Scroller(mContext);
mMinVelocity = ViewConfiguration.get(getContext()).getScaledMinimumFlingVelocity();
rectOfBackGround = new Rect();
setColors(colors);
}
第三部分,测量宽高,有点同学这一部分很是艰难,但是不用怕,赋值一下代码,重写其中的2个方法就行了
/**
* 第三部分,测量控件大小,一般情况下,前面的方法可以直接copy过去,只需要自己重写measureWrapWitdh,measureWrapHeight
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//这一部分,可写可不写,如若你不想重写控件大小,直接用super就行。如果需要重写的话套路也很简单
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = measureWitdh(widthMeasureSpec);
int measureHeight = measureHeight(heightMeasureSpec);
setMeasuredDimension(measureWidth, measureHeight);
}
private int measureWitdh(int widthMeasureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = measureWrapWitdh();
break;
case MeasureSpec.AT_MOST:
result = Math.min(measureWrapWitdh(), specSize);
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
private int measureWrapWitdh() {
//测量wrapContent时宽度,这个请根据自己的需求自己计算,我这里默认300dp
return CustomViewUtil.dp2px(mContext, 300);
}
private int measureHeight(int heightMeasureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(heightMeasureSpec);
int specSize = MeasureSpec.getSize(heightMeasureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = measureWrapHeight();
break;
case MeasureSpec.AT_MOST:
result = Math.min(measureWrapHeight(), specSize);
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
private int measureWrapHeight() {
//测量wrapContent时高度,这个请根据自己的需求自己计算,我这里默认50dp
return CustomViewUtil.dp2px(mContext, 50);
}
第四部分,在控件发生变化的时候 重新赋值宽高,以及重新为一些需要在控件大小发生变化时需要充值赋值的属性赋值。
/**
* 第四部分,在size发生变化时,重新赋值宽高
*
* @param w
* @param h
* @param oldw
* @param oldh
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
LayerDrawable background = (LayerDrawable) getBackground();
Drawable drawable = background.getDrawable(0);
drawable.getPadding(rectOfBackGround);
}
第五部分,属于核心以及难点吧,具体实现我就不说了,自己去看代码。其实代码也没啥看的,就是通过计算位置绘制而已。但是这个计算位置是很精细,很容易出错的地方,得加倍小心
/**
* 第五部分,绘制,绘制时,建议分层重下往上绘制
*
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawCalibrationTails(canvas);
drawCursor(canvas);
}
private void drawCalibrationTails(Canvas canvas) {
//计算需要画多少刻度线
//1,先计算可画刻度线区域的总长度
int totalLengthCanDrawCalibrationTails = (int) (mWidth - rectOfBackGround.left - rectOfBackGround.right - 2 * mBorderWidth);
int count = (int) (totalLengthCanDrawCalibrationTails / mCalibrationTailsWidth);
canvas.save();
for (int i = 0; i < count; i++) {
int offset = i % 2 == 0 ? (i + 1) / 2 : -(i + 1) / 2;
if (currentValue + offset > maxValue || currentValue + offset < minValue) {
continue;
} else {
//画刻度线
if (i == 0) {
if ((currentValue + offset) % mImportantTailsGap == 0) {
canvas.drawLine(mWidth / 2, rectOfBackGround.top + mBorderWidth, mWidth / 2, rectOfBackGround.top + mBorderWidth + mImportantTailsLength, mImportantTailsPaint);
} else {
canvas.drawLine(mWidth / 2, rectOfBackGround.top + mBorderWidth, mWidth / 2, rectOfBackGround.top + mBorderWidth + mCalibrationTailsLength, mCalibrationTailsPaint);
}
} else if (i % 2 == 0) {
if (mWidth / 2 + ((i + 1) / 2) * mCalibrationTailsDistance >= mWidth - rectOfBackGround.right - mBorderWidth) {
continue;
}
if ((currentValue + offset) % mImportantTailsGap == 0) {
canvas.drawLine(mWidth / 2 + ((i + 1) / 2) * mCalibrationTailsDistance, rectOfBackGround.top + mBorderWidth, mWidth / 2 + ((i + 1) / 2) * mCalibrationTailsDistance, rectOfBackGround.top + mBorderWidth + mImportantTailsLength, mImportantTailsPaint);
} else {
canvas.drawLine(mWidth / 2 + ((i + 1) / 2) * mCalibrationTailsDistance, rectOfBackGround.top + mBorderWidth, mWidth / 2 + ((i + 1) / 2) * mCalibrationTailsDistance, rectOfBackGround.top + mBorderWidth + mCalibrationTailsLength, mCalibrationTailsPaint);
}
} else {
if (mWidth / 2 - ((i + 1) / 2) * mCalibrationTailsDistance <= rectOfBackGround.left + mBorderWidth) {
continue;
}
if ((currentValue + offset) % mImportantTailsGap == 0) {
canvas.drawLine(mWidth / 2 - ((i + 1) / 2) * mCalibrationTailsDistance, rectOfBackGround.top + mBorderWidth, mWidth / 2 - ((i + 1) / 2) * mCalibrationTailsDistance, rectOfBackGround.top + mBorderWidth + mImportantTailsLength, mImportantTailsPaint);
} else {
canvas.drawLine(mWidth / 2 - ((i + 1) / 2) * mCalibrationTailsDistance, rectOfBackGround.top + mBorderWidth, mWidth / 2 - ((i + 1) / 2) * mCalibrationTailsDistance, rectOfBackGround.top + mBorderWidth + mCalibrationTailsLength, mCalibrationTailsPaint);
}
}
//画刻度线下面的文字
if ((currentValue + offset) % mImportantTailsGap == 0) {
if ((mWidth / 2 + offset * mCalibrationTailsDistance + mTextPaint.measureText(String.valueOf(currentValue + offset)) / 2) > mWidth - rectOfBackGround.right - mBorderWidth) {
continue;
}
if ((mWidth / 2 + offset * mCalibrationTailsDistance - mTextPaint.measureText(String.valueOf(currentValue + offset)) / 2) < rectOfBackGround.left + mBorderWidth) {
continue;
}
canvas.drawText(String.valueOf(currentValue + offset), mWidth / 2 + offset * mCalibrationTailsDistance - mTextPaint.measureText(String.valueOf(currentValue + offset)) / 2, CustomViewUtil.drawTextFromTop(rectOfBackGround.top + mBorderWidth + Math.max(mCalibrationTailsLength, mImportantTailsLength) + mValueDistanceImportantTails, mTextPaint), mTextPaint);
}
}
}
canvas.restore();
}
private void drawCursor(Canvas canvas) {
canvas.save();
if (mCursorDrawable != null) {
mCursorDrawable.setBounds(mWidth / 2 - mCursorDrawable.getIntrinsicWidth() / 2, (int) (rectOfBackGround.top + mBorderWidth), mWidth / 2 + mCursorDrawable.getIntrinsicWidth() / 2, (int) (rectOfBackGround.top + mBorderWidth + mCursorDrawable.getIntrinsicHeight()));
mCursorDrawable.draw(canvas);
} else {
mCursorPaint.setColor(mCursorColor);
mCursorPaint.setStrokeWidth(mCursorWidth);
mCursorPaint.setStyle(Paint.Style.FILL_AND_STROKE);
canvas.drawLine(mWidth / 2, 0 + rectOfBackGround.top + mBorderWidth, mWidth / 2, mHeight - rectOfBackGround.bottom - mBorderWidth, mCursorPaint);
}
canvas.restore();
}
第六部分:这个不解释了
/**
* 第六部分
* 设置数据,用以第一次赋值,或者在listView recycleView复用的时候赋值,或者在交互中重新赋值等等
*/
public void setData(int currentValue) {
if (currentValue > maxValue || currentValue < minValue) {
throw new IllegalArgumentException("the value must between minValue and maxValue");
}
this.currentValue = currentValue;
invalidate();
if (mListener != null) {
mListener.onValueChange(currentValue);
}if (mListener != null) {
mListener.onValueChange(currentValue);
}
}
第七部分 重难点部分,但是也很简单。注意true和false的返回,
/**
* 第七部分
* 处理触摸事件
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mScroller.forceFinished(true);
mLastX = (int) event.getX();
mMove = 0;
isMove = false;
break;
case MotionEvent.ACTION_MOVE:
mCurrentX = (int) event.getX();
mMove += (int) ((mLastX - mCurrentX) * dampNumber);
if (mMove < mTouchSlop) {
isMove = true;
}
if (currentValue > maxValue) {
return false;
}
if (currentValue < minValue) {
return false;
}
if (isMove) {
changeMoveAndValue();
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
isMove = false;
countVelocityTracker(event);
break;
}
mLastX = (int) event.getX();
return true;
}
第八部分,控件滑动。Scroller和VelocityTracker的使用
/**
* 第八部分处理滑动相关的事情
*/
private void changeMoveAndValue() {
int tValue = (int) (mMove / mCalibrationTailsDistance);
if (Math.abs(tValue) > 0) {
currentValue = currentValue + tValue;
mMove -= tValue * mCalibrationTailsDistance;
if (currentValue < minValue || currentValue > maxValue) {
currentValue = currentValue < minValue ? minValue : maxValue;
mMove = 0;
mScroller.forceFinished(true);
}
notifyValueChange(currentValue);
}
postInvalidate();
}
private void countMoveEnd() {
int roundMove = Math.round(mMove / mCalibrationTailsDistance);
currentValue = currentValue + roundMove;
if (currentValue < minValue) {
currentValue = minValue;
}
if (currentValue > maxValue) {
currentValue = maxValue;
}
mLastX = 0;
mMove = 0;
notifyValueChange(currentValue);
postInvalidate();
}
private int mLastScrollX;
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
mCurrentX = mScroller.getCurrX();
if (mScroller.getCurrX() == mScroller.getFinalX()) { // over
countMoveEnd();
} else {
mMove += (mLastScrollX - mCurrentX);
changeMoveAndValue();
}
mLastScrollX = mCurrentX;
}
}
private void countVelocityTracker(MotionEvent event) {
// mVelocityTracker.addMovement(event);
mVelocityTracker.computeCurrentVelocity(200);
float xVelocity = mVelocityTracker.getXVelocity();
if (Math.abs(xVelocity) > mMinVelocity) {
mScroller.fling(0, 0, (int) xVelocity, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
}
postInvalidate();
}
最后
/**
* 定义相关的监听器
*/
private OnValueChangeListener mListener;
public void setOnValueChangeListener(OnValueChangeListener mListener) {
this.mListener = mListener;
}
public void notifyValueChange(int value) {
if (null != mListener) {
mListener.onValueChange(value);
}
}
public interface OnValueChangeListener {
void onValueChange(int value);
}
实际上还有一部分是动画,但是本例中未曾用到,以后再说吧
demo