前言
魅族手机的闹钟应用中有个倒计时,这个控件还是蛮有趣的。左边是魅族闹钟,右边是我们最终实现的效果,虽然有些细节还需优化,不过基本上已经达到了想要的效果,我们先来就来看看如何实现吧。
分析
确定宽高
对一个Android自定义控件来说,一般都经过三个步骤
- onLayout()
- onMeasure()
- onDraw()
onLayout明确子控件在父控件中的位置(本控件不需要重写),onMeasure是确定控件的大小(宽、高),而onDraw是我们重点关注的方法,我们需要在这个方法中写入显示View的逻辑代码。
对于本控件,控件的高度 应该等于细线的高度(mLineHeight)加上数字的高度(mFontHeight),当然为了好看,中间需要设上一些边距(mPadding),因此本控件的高度应该为 mFontHeight + mLineHeight + 10 + mPadding,测量代码如下
1 2 3 4 5 6 7 8 9 10 11
| private int measureHeight(int heightMeasureSpec) { int mode = MeasureSpec.getMode(heightMeasureSpec); int size = MeasureSpec.getSize(heightMeasureSpec); switch (mode) { case MeasureSpec.EXACTLY: return size ; case MeasureSpec.AT_MOST: return Math.min(size, mFontHeight + mLineHeight + 10 + mPadding) ; } return size ; }
|
同样地,控件的宽度其实就是0~1000的间隔,测量代码如下
1 2 3 4 5 6 7 8 9 10 11 12
| private int measureWidth(int widthMeasureSpec) { int mode = MeasureSpec.getMode(widthMeasureSpec); int size = MeasureSpec.getSize(widthMeasureSpec); switch (mode) { case MeasureSpec.EXACTLY: return size ; case MeasureSpec.AT_MOST: int result = getPaddingLeft() + mContentWidth + getPaddingRight() ; return Math.min(size, result) ; } return size ; }
|
画刻度尺
重点在于刻度尺的计算。思路是先draw上头的数字,然后再draw下边的线条,判断位置确定是否需要draw上头的数字即可。其实就是坐标的计算。代码如下
1 2 3 4 5 6 7 8 9 10
| int startX = mPadding; int stopX = mPadding; int stopY =mHeight - mPadding; for (int i = 0 ; i<=mContentWidth ; i += mFontWidth) if (i % (mFontWidth *10) == 0) { canvas.drawLine(startX + i, mFontHeight + mPadding + 5 , stopX + i, stopY, mTextPaint); canvas.drawText(i + "", startX + i, mFontHeight + mPadding, mTextPaint); } else if (i % mFontWidth == 0) { canvas.drawLine(startX + i, mFontHeight + mPadding + 10, stopX + i, stopY, mPaint); }
|
让View动起来
Android本身提供了移动View的API,因此让View动起来也是不难的。两种思路
- 监听Touch事件,当Touch坐标变化时,计算坐标位置,不断调用scrollTo(x,0)达到变换坐标的目的
- 监听Touch事件,记录上次横坐标和本次横坐标的差值,然后调用scrollBy(delta, 0) 即可移动
其实两种方法本质上都是一样的。ScrollBy其实也是调用了scrollTo方法。本文采用方法二。其代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mLastX = (int) event.getX(); break; case MotionEvent.ACTION_MOVE: int x = (int) event.getX(); int deltaX = x - mLastX; scrollBy(-deltaX, 0); mLastX = x; break; } return true; }
|
当然了,我们是不能让View无限移动的,因此需要重写scrollBy方法,限制View不能超过边界。
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Override public void scrollBy(int x, int y) { super.scrollBy(x, y); if (x < 0) { if (getScrollX() < -getCenter() + mPadding) { scrollTo(-getCenter() + mPadding, 0); } } else if (x >0) { if (mContentWidth - getScrollX() + x < getCenter()) { scrollTo(mContentWidth - getCenter() + mFontWidth, 0); } } }
|
当超过边界时,直接调用scrollTo,让View停留在特定的位置即可。需要注意的一点是,View往左滑动时,ScrollX的值是负的。
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
| package com.nancyyihao.demo; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.text.TextPaint; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.widget.Scroller; public class RulerView extends View { private static final String TAG = RulerView.class.getSimpleName() ; private TextPaint mTextPaint; private Paint mPaint ; private int mWidth; private int mHeight; private int mPadding = 10; private Scroller mScroller ; private int mLastX; private int mContentWidth = 1000; private int mLineHeight = 50; private int mFontHeight; private int mFontWidth = 10; private onValueChangedListener mValueChangedListener; public interface onValueChangedListener { void onValueChanged(int newValue); } public RulerView(Context context) { super(context); init(context); } public RulerView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public RulerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context); } public void setOnValueChangedListener(onValueChangedListener listener) { this.mValueChangedListener = listener ; } private void init(Context context) { mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.LINEAR_TEXT_FLAG); mTextPaint.setTextAlign(Paint.Align.CENTER); mTextPaint.setColor(Color.parseColor("#FF4081")); mTextPaint.setTextSize(30); mTextPaint.setStrokeWidth(2f); Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics(); mFontHeight = Math.round(Math.abs(fontMetrics.top) + Math.abs(fontMetrics.bottom)) ; mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); mPaint.setColor(Color.DKGRAY); mPaint.setStrokeWidth(2f); mPaint.setTextSize(30); mPaint.setTextAlign(Paint.Align.CENTER); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { mWidth = measureWidth(widthMeasureSpec); mHeight = measureHeight(heightMeasureSpec); setMeasuredDimension(mWidth , mHeight ); } private int measureWidth(int widthMeasureSpec) { int mode = MeasureSpec.getMode(widthMeasureSpec); int size = MeasureSpec.getSize(widthMeasureSpec); switch (mode) { case MeasureSpec.EXACTLY: return size ; case MeasureSpec.AT_MOST: int result = getPaddingLeft() + mContentWidth + getPaddingRight() ; return Math.min(size, result) ; } return size ; } private int measureHeight(int heightMeasureSpec) { int mode = MeasureSpec.getMode(heightMeasureSpec); int size = MeasureSpec.getSize(heightMeasureSpec); switch (mode) { case MeasureSpec.EXACTLY: return size ; case MeasureSpec.AT_MOST: return Math.min(size, mFontHeight + mLineHeight + 10 + mPadding) ; } return size ; }
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int startX = mPadding; int stopX = mPadding; int stopY =mHeight - mPadding; for (int i = 0 ; i<=mContentWidth ; i += mFontWidth) if (i % (mFontWidth *10) == 0) { canvas.drawLine(startX + i, mFontHeight + mPadding + 5 , stopX + i, stopY, mTextPaint); canvas.drawText(i + "", startX + i, mFontHeight + mPadding, mTextPaint); } else if (i % mFontWidth == 0) { canvas.drawLine(startX + i, mFontHeight + mPadding + 10, stopX + i, stopY, mPaint); } } private int calcValue() { return ( getCenter() + getScrollX() - mPadding) ; } private int getCenter() { return (getRight() - getLeft()) / 2 ; } @Override public void scrollBy(int x, int y) { super.scrollBy(x, y); if (x < 0) { if (getScrollX() < -getCenter() + mPadding) { scrollTo(-getCenter() + mPadding, 0); } } else if (x >0) { if (mContentWidth - getScrollX() + x < getCenter()) { scrollTo(mContentWidth - getCenter() + mFontWidth, 0); } } } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mLastX = (int) event.getX(); break; case MotionEvent.ACTION_MOVE: int x = (int) event.getX(); int deltaX = x - mLastX; scrollBy(-deltaX, 0); mLastX = x; if (mValueChangedListener != null) mValueChangedListener.onValueChanged(calcValue()); break; } return true; } }
|
总结
整体上还是比较粗糙,原形虽然有了,但是还需要优化。
参考