概述
本文只要说的是自定义一个刻度尺,正好学习下android自定义控件,之前写过一篇《Android自定义一个属于自己的时间钟表》,大家如果感兴趣可以去看下,好了不扯淡了,直接上效果:
看到这个效果以后估计好多新手会觉得不知道如何入手,但是要是大神看到了就会想用什么方式实现才是最好的。这就是差距啊,没办法像我这个菜鸟只好参考下其他实现方法,写了这个demo,让我们一起来看看实现思路。
我们来分步骤一步一步来实现:
1、绘制刻度尺及刻度值(高,中,低刻度)。
2、绘制底部线。
3、绘制中间箭头。
4、监听手势处理(处理范围越界,及选中)。
第一步:1、自定义View的属性,首先在res/values/ 下建立一个attrs.xml , 在里面定义我们的属性和声明我们的整个样式。
<declare-styleable name="RulerView"> <!--最大刻度的颜色--> <attr name="mMaxScaleColor" format="color"/> <!--中间刻度的颜色--> <attr name="mMidScaleColor" format="color"/> <!--最小刻度的颜色--> <attr name="mMinScaleColor" format="color"/> <!--底线的颜色--> <attr name="mBottomLineColor" format="color"/> <!--最大刻度的宽度--> <attr name="mMaxScaleWidth" format="dimension"/> <!--中间刻度的宽度--> <attr name="mMidScaleWidth" format="dimension"/> <!--最小刻度的宽度--> <attr name="mMinScaleWidth" format="dimension"/> <!--底线的宽度--> <attr name="mBottomLineWidth" format="dimension"/> <!--最大刻度的高度占控件的高度比例--> <attr name="mMaxScaleHeightRatio" format="float"/> <!--中间刻度的高度占控件的高度比例--> <attr name="mMidScaleHeightRatio" format="float"/> <!--最小刻度的高度占控件的高度比例--> <attr name="mMinScaleHeightRatio" format="float"/> <!--是否显示刻度值--> <attr name="isShowScaleValue" format="boolean"/> <!--是否刻度渐变 包括刻度值和刻度线及下面的线--> <attr name="isScaleGradient" format="boolean"/> <!--刻度值颜色--> <attr name="mScaleValueColor" format="color"/> <!--刻度值文字大小--> <attr name="mScaleValueSize" format="dimension"/> <!--刻度值间隔--> <attr name="mScaleSpace" format="dimension"/> <!-- 当前值--> <attr name="mCurrentValue" format="integer"/> <!--最大值--> <attr name="mMaxValue" format="integer"/> <!--最小值--> <attr name="mMinValue" format="integer"/> <!--刻度基数--> <attr name="mScaleBase" format="integer"/> <!--中间图标--> <attr name="mMiddleImg" format="reference"/> </declare-styleable>
通过这个attrs大家也看到,我们设置了还是蛮细的,所有的刻度及刻度值颜色大小都设定了,让控件设置更加的灵活。
2、下面创建一个class类为rulerview.class ,并设置main.xml中:
<com.dalong.rulerview.RulerView android:id="@+id/ruler2" android:layout_centerInParent="true" android:layout_width="match_parent" android:layout_height="wrap_content" app:mMaxValue="5000" app:mMinValue="1000" app:mScaleBase="100" app:mScaleSpace="10dp" app:mMaxScaleColor="@color/colorAccent" app:mMidScaleColor="@color/colorPrimary" app:mMinScaleColor="@color/colorPrimary" app:mBottomLineColor="@color/colorAccent" app:mMaxScaleHeightRatio="0.5" app:mMidScaleHeightRatio="0.3" app:mMinScaleHeightRatio="0.2" app:mMaxScaleWidth="2.5dp" app:mMidScaleWidth="2dp" app:mMinScaleWidth="2dp" app:mBottomLineWidth="2.5dp" app:mCurrentValue="1000" app:mScaleValueColor="@color/colorAccent" app:mScaleValueSize="12sp" app:mMiddleImg="@mipmap/icon_arrow" app:isScaleGradient="false" />
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<scaleNum;i++){ // 右面 xPosition=mDrawWidth/2f+i*mScaleSpace+distanceX; value=currValue+i; if(xPosition<=mDrawWidth && value>=(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<T> { /** * 当更改选择的时候回调方法 * @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