概述
本文只要说的是自定义一个刻度尺,正好学习下android自定义控件,之前写过一篇《Android自定义一个属于自己的时间钟表》,大家如果感兴趣可以去看下,好了不扯淡了,直接上效果:
看到这个效果以后估计好多新手会觉得不知道如何入手,但是要是大神看到了就会想用什么方式实现才是最好的。这就是差距啊,没办法像我这个菜鸟只好参考下其他实现方法,写了这个demo,让我们一起来看看实现思路。
我们来分步骤一步一步来实现:
1、绘制刻度尺及刻度值(高,中,低刻度)。
2、绘制底部线。
3、绘制中间箭头。
4、监听手势处理(处理范围越界,及选中)。
第一步:1、自定义View的属性,首先在res/values/ 下建立一个attrs.xml , 在里面定义我们的属性和声明我们的整个样式。
通过这个attrs大家也看到,我们设置了还是蛮细的,所有的刻度及刻度值颜色大小都设定了,让控件设置更加的灵活。
2、下面创建一个class类为rulerview.class ,并设置main.xml中:
3、在自定义View的构造方法中,获得我们的自定义的样式
// 默认刻度模式
public static final int MOD_TYPE_SCALE = 5;
//刻度基数 每个刻度代表多少 默认为1
public int mScaleBase=1;
//最大刻度的颜色
public int mMaxScaleColor;
//中间刻度的颜色
public int mMidScaleColor;
//最小刻度的颜色
public int mMinScaleColor;
//底部线的颜色
public int mBottomLineColor;
//最大刻度的宽度
public float mMaxScaleWidth;
//中间刻度的宽度
public float mMidScaleWidth;
//最小刻度的宽度
public float mMinScaleWidth;
//底线的宽度
public float mBottomLineWidth;
//最大刻度的高度占控件的高度比例
public float mMaxScaleHeightRatio;
//中间刻度的高度占控件的高度比例
public float mMidScaleHeightRatio;
//最小刻度的高度占控件的高度比例
public float mMinScaleHeightRatio;
//是否显示刻度值
public boolean isShowScaleValue;
//是否刻度渐变
public boolean isScaleGradient;
//刻度值颜色
public int mScaleValueColor;
//刻度值文字大小
public float mScaleValueSize;
//当前值
public int mCurrentValue;
//最大值
public int mMaxValue;
//最小值
public int mMinValue;
//中间图片
private Bitmap mMiddleImg;
//刻度线画笔
private Paint mScalePaint;
// 刻度值画笔
private TextPaint mScaleValuePaint;
//中间图片画笔
private Paint mMiddleImgPaint;
private float mTpDesiredWidth;
//最大刻度高度
private int mMaxScaleHeight;
//中间刻度高度
private int mMidScaleHeight;
//最小刻度高度
private int mMinScaleHeight;
// 滚动偏移量
private int scrollingOffset;
//间隔S
private int mScaleSpace=20;
// 滚动器
private RulerViewScroller scroller;
// 是否执行滚动
private boolean isScrollingPerformed;
public RulerView(Context context) {
this(context,null);
}
public RulerView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public RulerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray=context.obtainStyledAttributes(attrs,R.styleable.RulerView);
mMaxScaleColor=typedArray.getColor(R.styleable.RulerView_mMaxScaleColor, Color.BLACK);
mMidScaleColor=typedArray.getColor(R.styleable.RulerView_mMidScaleColor, Color.BLACK);
mMinScaleColor=typedArray.getColor(R.styleable.RulerView_mMinScaleColor, Color.BLACK);
mMaxScaleColor=typedArray.getColor(R.styleable.RulerView_mMaxScaleColor, Color.BLACK);
mScaleValueColor=typedArray.getColor(R.styleable.RulerView_mScaleValueColor, Color.BLACK);
mBottomLineColor=typedArray.getColor(R.styleable.RulerView_mBottomLineColor, Color.BLACK);
mMaxScaleWidth = typedArray.getDimensionPixelSize(R.styleable.RulerView_mMaxScaleWidth, 15);
mMidScaleWidth = typedArray.getDimensionPixelSize(R.styleable.RulerView_mMidScaleWidth, 12);
mMinScaleWidth = typedArray.getDimensionPixelSize(R.styleable.RulerView_mMinScaleWidth, 10);
mBottomLineWidth = typedArray.getDimensionPixelSize(R.styleable.RulerView_mBottomLineWidth, 15);
mScaleValueSize = typedArray.getDimensionPixelSize(R.styleable.RulerView_mScaleValueSize, 12);
mScaleSpace = typedArray.getDimensionPixelSize(R.styleable.RulerView_mScaleSpace, 20);
mMaxScaleHeightRatio = typedArray.getFloat(R.styleable.RulerView_mMaxScaleHeightRatio, 0.3f);
mMidScaleHeightRatio = typedArray.getFloat(R.styleable.RulerView_mMidScaleHeightRatio, 0.2f);
mMinScaleHeightRatio = typedArray.getFloat(R.styleable.RulerView_mMinScaleHeightRatio, 0.1f);
isShowScaleValue = typedArray.getBoolean(R.styleable.RulerView_isShowScaleValue, true);
isScaleGradient = typedArray.getBoolean(R.styleable.RulerView_isScaleGradient, true);
mMaxValue = typedArray.getInteger(R.styleable.RulerView_mMaxValue, 100);
mMinValue = typedArray.getInteger(R.styleable.RulerView_mMinValue, 0);
mScaleBase = typedArray.getInteger(R.styleable.RulerView_mScaleBase, 1);
mCurrentValue = typedArray.getInteger(R.styleable.RulerView_mCurrentValue, 0);
setCurrentValue(mCurrentValue);
mMiddleImg = BitmapFactory.decodeResource(getResources(),
typedArray.getResourceId(R.styleable.RulerView_mMiddleImg,R.drawable.ruler_mid_arraw));
typedArray.recycle();
mScalePaint=new Paint(Paint.ANTI_ALIAS_FLAG);
mScalePaint.setStyle(Paint.Style.STROKE);
mScalePaint.setAntiAlias(true);
mScaleValuePaint=new TextPaint(Paint.ANTI_ALIAS_FLAG);
mScaleValuePaint.setColor(mScaleValueColor);
mScaleValuePaint.setTextSize(mScaleValueSize);
mScaleValuePaint.setTextAlign(Paint.Align.CENTER);
mTpDesiredWidth = Layout.getDesiredWidth("0", mScaleValuePaint);
mMiddleImgPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
mMiddleImgPaint.setStyle(Paint.Style.STROKE);
mMiddleImgPaint.setAntiAlias(true);
scroller=new RulerViewScroller(context,scrollingListener);
}
4、我们重写onMesure:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measureWidthSize(widthMeasureSpec),measureHeightSize(heightMeasureSpec));
}
private int measureHeightSize(int heightMeasureSpec) {
int result;
int mode=MeasureSpec.getMode(heightMeasureSpec);
int size=MeasureSpec.getSize(heightMeasureSpec);
if(mode==MeasureSpec.EXACTLY){
result=size;
}else{
result=(int) (mMiddleImg.getHeight() + getPaddingTop() + getPaddingBottom() + 2 * mScaleValuePaint.getTextSize());
if(mode==MeasureSpec.AT_MOST){
result=Math.min(result,size);
}
}
return result;
}
private int measureWidthSize(int widthMeasureSpec) {
int result;
int mode=MeasureSpec.getMode(widthMeasureSpec);
int size=MeasureSpec.getSize(widthMeasureSpec);
if(mode==MeasureSpec.EXACTLY){
result=size;
}else{
result=400;
if(mode==MeasureSpec.AT_MOST){
result=Math.min(result,size);
}
}
return result;
}
5、设置下三种刻度尺的高度
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w == 0 || h == 0)
return;
/**
* 在这里根据控件高度设置三中刻度线的高度
*/
int mHeight = h - getPaddingTop() - getPaddingBottom();
mMaxScaleHeight = (int) (mHeight*mMaxScaleHeightRatio);
mMidScaleHeight = (int) (mHeight*mMidScaleHeightRatio);
mMinScaleHeight = (int) (mHeight*mMinScaleHeightRatio);
}
6、绘制刻度尺
/**
* 绘制刻度线
* @param canvas
* @param mDrawWidth
* @param mDrawHeight
*/
private void drawScaleLine(Canvas canvas, int mDrawWidth, int mDrawHeight) {
int scaleNum= (int) (Math.ceil(mDrawWidth/2f/mScaleSpace))+2;
int distanceX = scrollingOffset;
int currValue = mCurrentValue;
drawScaleLine(canvas,scaleNum,distanceX,currValue,mDrawWidth,mDrawHeight);
}
/**
* 绘制刻度线
* @param canvas
* @param scaleNum
* @param distanceX
* @param currValue
* @param mDrawWidth
* @param mDrawHeight
*/
private void drawScaleLine(Canvas canvas, int scaleNum, int distanceX, int currValue, int mDrawWidth, int mDrawHeight) {
int dy = (int) (mDrawHeight - mTpDesiredWidth - mScaleValuePaint.getTextSize()) - getPaddingBottom();
int value;
float xPosition;
for (int i=0;i=(mMinValue/mScaleBase)&&value<=(mMaxValue/mScaleBase)){
drawScaleLine(canvas, value, xPosition, dy, scaleNum, i, mDrawHeight);
}
//绘制右面线
if(value<(mMaxValue/mScaleBase)&&value>=(mMinValue/mScaleBase))
drawBottomLine(canvas,getAlpha(scaleNum, i),xPosition-mMaxScaleWidth/2, dy, xPosition+mScaleSpace+mMaxScaleWidth/2, dy);
//左面
xPosition=mDrawWidth/2f-i*mScaleSpace+distanceX;
value=currValue-i;
if(xPosition>getPaddingLeft() && value>=(mMinValue/mScaleBase)&&value<=(mMaxValue/mScaleBase)){
drawScaleLine( canvas, value, xPosition, dy, scaleNum, i, mDrawHeight);
}
//绘制左面线
if(value>=(mMinValue/mScaleBase) && value<(mMaxValue/mScaleBase))
drawBottomLine(canvas,getAlpha(scaleNum, i),xPosition-mMaxScaleWidth/2, dy, xPosition+mScaleSpace+mMaxScaleWidth/2, dy);
}
}
/**
* 绘制底部线
* @param canvas
* @param alpha
* @param sx
* @param sy
* @param ex
* @param ey
*/
private void drawBottomLine(Canvas canvas,int alpha,float sx,float sy,float ex,float ey){
mScalePaint.setColor(mBottomLineColor);
mScalePaint.setStrokeWidth(mBottomLineWidth);
mScalePaint.setAlpha(alpha);
canvas.drawLine(sx, sy, ex, ey, mScalePaint);
}
/**
* 绘制刻度尺 左 右
* @param canvas
* @param value
* @param xPosition
* @param dy
* @param scaleNum
* @param i
* @param mDrawHeight
*/
public void drawScaleLine(Canvas canvas,int value, float xPosition,int dy,int scaleNum,int i,int mDrawHeight){
if (value % MOD_TYPE_SCALE == 0) {
if(value % (MOD_TYPE_SCALE*2)==0){//大刻度
drawScaleLine(canvas,mMaxScaleWidth,mMaxScaleColor,getAlpha(scaleNum, i),
xPosition,dy,xPosition,dy - mMaxScaleHeight);
if (isShowScaleValue) {
mScaleValuePaint.setAlpha(getAlpha(scaleNum, i));
canvas.drawText(String.valueOf(value*mScaleBase), xPosition, mDrawHeight - mTpDesiredWidth, mScaleValuePaint);
}
}else{//中刻度
drawScaleLine(canvas,mMidScaleWidth,mMidScaleColor,getAlpha(scaleNum, i),
xPosition,dy,xPosition,dy-mMidScaleHeight);
}
}else{// 小刻度
drawScaleLine(canvas,mMinScaleWidth,mMinScaleColor,getAlpha(scaleNum, i),
xPosition,dy,xPosition,dy-mMinScaleHeight);
}
}
/**
* 绘制刻度尺刻度
* @param canvas
* @param strokeWidth
* @param scaleColor
* @param alpha
* @param sx
* @param sy
* @param ex
* @param ey
*/
private void drawScaleLine(Canvas canvas,float strokeWidth,int scaleColor,int alpha,float sx,float sy,float ex,float ey){
mScalePaint.setStrokeWidth(strokeWidth);
mScalePaint.setColor(scaleColor);
mScalePaint.setAlpha(alpha);
canvas.drawLine(sx, sy, ex, ey, mScalePaint);
}
其实上面就是绘制刻度尺的核心办法,主要实现思想就是以中心为开始点向左右绘制刻度线。其中也有一些细节需要大家慢慢去思索的,比如这里包含高,中 ,低的三种刻度线,他们在绘制的时候高度,颜色,宽度都是不一样的设置。怎么区分,我这里也写的比较的清楚。这里我就不在提了。
7、绘制中间箭头图片
/**
* 绘制中间图片
* @param canvas
* @param mDrawWidth
* @param mDrawHeight
*/
private void drawMiddleImg(Canvas canvas, int mDrawWidth, int mDrawHeight) {
int left = (mDrawWidth - mMiddleImg.getWidth()) / 2;
int top = (int) (mScaleValuePaint.getTextSize() / 2);
canvas.drawBitmap(mMiddleImg, left, top, mMiddleImgPaint);
}
以上写完就已经可以实现刻度尺了,但是刻度尺是无法拖动的,效果如下:
下面主要就是需要如何实现拖动的效果,其实这个才是最难的。
这里单独创建一个滑动控制类:
public class RulerViewScroller {
//滚动的时间
public static final int SCROLLING_DURATION = 400;
//用于滚动的最小增量
public static final int MIN_DELTA_FOR_SCROLLING = 1;
//Listener
private ScrollingListener listener;
//上下文
private Context context;
// Scrolling
private GestureDetector gestureDetector;
private Scroller scroller;
private int lastScrollX;
private float lastTouchedX;
private boolean isScrollingPerformed;
private final int MESSAGE_SCROLL = 0;
private final int MESSAGE_JUSTIFY = 1;
public RulerViewScroller(Context context, ScrollingListener listener) {
this.listener = listener;
this.context = context;
gestureDetector = new GestureDetector(context, gestureListener);
gestureDetector.setIsLongpressEnabled(false);
scroller = new Scroller(context);
scroller.setFriction(0.05f);
}
/**
* 手势监听
*/
private GestureDetector.SimpleOnGestureListener gestureListener = new GestureDetector.SimpleOnGestureListener() {
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return true;
}
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
lastScrollX = 0;
scroller.fling(0, lastScrollX, (int) -velocityX, 0, -0x7FFFFFFF, 0x7FFFFFFF, 0, 0);
setNextMessage(MESSAGE_SCROLL);
return true;
}
};
/**
* 手势处理
* @param event
* @return
*/
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastTouchedX = event.getX();
scroller.forceFinished(true);
clearMessages();
break;
case MotionEvent.ACTION_MOVE:
int distanceX = (int) (event.getX() - lastTouchedX);
if (distanceX != 0) {
startScrolling();
listener.onScroll(distanceX);
lastTouchedX = event.getX();
}
break;
}
//当手指离开控件时
if (!gestureDetector.onTouchEvent(event) && event.getAction() == MotionEvent.ACTION_UP) {
justify();
}
return true;
}
/**
* 发送下一步消息,清楚之前的消息
* @param message
*/
private void setNextMessage(int message) {
clearMessages();
animationHandler.sendEmptyMessage(message);
}
/**
* 清楚所有的what的消息列表
*/
private void clearMessages() {
animationHandler.removeMessages(MESSAGE_SCROLL);
animationHandler.removeMessages(MESSAGE_JUSTIFY);
}
/**
* 滚动
* @param distance 距离
* @param time 时间
*/
public void scroll(int distance, int time) {
scroller.forceFinished(true);
lastScrollX = 0;
scroller.startScroll(0, 0, distance, 0, time != 0 ? time : SCROLLING_DURATION);
setNextMessage(MESSAGE_SCROLL);
startScrolling();
}
/**
* 动画处理handler
*/
private Handler animationHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
scroller.computeScrollOffset();
int currX = scroller.getCurrX();
int delta = lastScrollX - currX;
lastScrollX = currX;
if (delta != 0) {
listener.onScroll(delta);
}
// 滚动是不是完成时,涉及到最终Y,所以手动完成
if (Math.abs(currX - scroller.getFinalX()) < MIN_DELTA_FOR_SCROLLING) {
lastScrollX = scroller.getFinalX();
scroller.forceFinished(true);
}
if (!scroller.isFinished()) {
animationHandler.sendEmptyMessage(msg.what);
} else if (msg.what == MESSAGE_SCROLL) {
justify();
} else {
finishScrolling();
}
return true;
}
});
/**
* 滚动停止时待校验
*/
private void justify() {
listener.onJustify();
setNextMessage(MESSAGE_JUSTIFY);
}
/**
* 开始滚动
*/
private void startScrolling() {
if (!isScrollingPerformed) {
isScrollingPerformed = true;
listener.onStarted();
}
}
/**
* 滚动结束
*/
void finishScrolling() {
if (isScrollingPerformed) {
listener.onFinished();
isScrollingPerformed = false;
}
}
/**
* 滚动监听器接口
*/
public interface ScrollingListener {
/**
* 正在滚动中回调
* @param distance 滚动的距离
*/
void onScroll(int distance);
/**
* 启动滚动时调用的回调函数
*/
void onStarted();
/**
* 校验完成后 执行完毕后回调
*/
void onFinished();
/**
* 滚动停止时待校验
*/
void onJustify();
}
然后在activity中:
/**
* 滚动回调接口
*/
RulerViewScroller.ScrollingListener scrollingListener = new RulerViewScroller.ScrollingListener() {
/**
* 滚动开始
*/
@Override
public void onStarted() {
isScrollingPerformed = true;
//滚动开始
if (null != onWheelListener) {
onWheelListener.onScrollingStarted(RulerView.this);
}
}
/**
* 滚动中
* @param distance 滚动的距离
*/
@Override
public void onScroll(int distance) {
doScroll(distance);
}
/**
* 滚动结束
*/
@Override
public void onFinished() {
if (outOfRange()) {
return;
}
if (isScrollingPerformed) {
//滚动结束
if (null != onWheelListener) {
onWheelListener.onScrollingFinished(RulerView.this);
}
isScrollingPerformed = false;
}
scrollingOffset = 0;
invalidate();
}
/**
* 验证滚动是否在正确位置
*/
@Override
public void onJustify() {
if (outOfRange()) {
return;
}
if (Math.abs(scrollingOffset) > RulerViewScroller.MIN_DELTA_FOR_SCROLLING) {
if (scrollingOffset < -mScaleSpace / 2) {
scroller.scroll(mScaleSpace + scrollingOffset, 0);
} else if (scrollingOffset > mScaleSpace / 2) {
scroller.scroll(scrollingOffset - mScaleSpace, 0);
} else {
scroller.scroll(scrollingOffset, 0);
}
}
}
};
/**
* 超出左右范围
* @return
*/
private boolean outOfRange() {
//这个是越界后需要回滚的大小值
int outRange = 0;
if (mCurrentValue < mMinValue/mScaleBase) {
outRange = (mCurrentValue - mMinValue/mScaleBase) * mScaleSpace;
} else if (mCurrentValue > mMaxValue/mScaleBase) {
outRange = (mCurrentValue - mMaxValue/mScaleBase) * mScaleSpace;
}
if (0 != outRange) {
scrollingOffset = 0;
scroller.scroll(-outRange, 100);
return true;
}
return false;
}
/**
* 滚动中回调最新值
* @param delta
*/
private void doScroll(int delta) {
scrollingOffset += delta;
int offsetCount = scrollingOffset / mScaleSpace;
if (0 != offsetCount) {
// 显示在范围内
int oldValueIndex = Math.min(Math.max(mMinValue, mCurrentValue*mScaleBase), mMaxValue);
mCurrentValue -= offsetCount;
scrollingOffset -= offsetCount * mScaleSpace;
if (null != onWheelListener) {
//回调通知最新的值
int valueIndex = Math.min(Math.max(mMinValue, mCurrentValue*mScaleBase), mMaxValue);
onWheelListener.onChanged(this, oldValueIndex + "",valueIndex+"");
}
}
invalidate();
}
private float mDownFocusX;
private float mDownFocusY;
private boolean isDisallowIntercept;
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) {
return true;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownFocusX = event.getX();
mDownFocusY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
if (!isDisallowIntercept && Math.abs(event.getY() - mDownFocusY) < Math.abs(event.getX() - mDownFocusX)) {
isDisallowIntercept = true;
if (getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(true);
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(false);
}
isDisallowIntercept = false;
break;
}
return scroller.onTouchEvent(event);
}
private OnRulerViewScrollListener onWheelListener;
/**
* 添加滚动回调
* @param listener the listener
*/
public void setScrollingListener(OnRulerViewScrollListener listener) {
onWheelListener = listener;
}
public interface OnRulerViewScrollListener {
/**
* 当更改选择的时候回调方法
* @param rulerView 状态更改的view
* @param oldValue 当前item的旧值
* @param newValue 当前item的新值
*/
void onChanged(RulerView rulerView, T oldValue, T newValue);
/**
* 滚动启动时调用的回调方法
* @param rulerView
*/
void onScrollingStarted(RulerView rulerView);
/**
* 滚动结束时调用的回调方法
* @param rulerView
*/
void onScrollingFinished(RulerView rulerView);
}
其中有些参考了网上实现方式并进行拓展以后就实现了下面的效果。
这里附上github:https://github.com/dalong982242260/AndroidRulerView