一、最近的一个项目中有遇到时间刻度盘的需求,在网上没找到合适的,于是自己就花点时间实现了,现在分享出来,效果如下图:
在介绍如何实现之前,先大概介绍一个这个时间刻度盘的功能:
1、显示当前时间,并且可以左右拖动至上一天或者下一天,
2、根据传入的时间块来绘制蓝色部分
二、代码实现
public class ScalePanel extends View { public interface OnValueChangeListener { public void onValueChange(float value); /** * value不再变化,终点 * * @param mCalendar * 刻度盘上当前时间 */ public void onValueChangeEnd(Calendar mCalendar); } public static final int MOD_TYPE_HALF = 2; public static final int MOD_TYPE_ONE = 10; private static final int ITEM_HALF_DIVIDER = 60; private static final int ITEM_MAX_HEIGHT = 10; private static final int TEXT_SIZE = 14; private float mDensity; /** * 当前刻度值 */ private int mValue = 12; private int mLineDivider = ITEM_HALF_DIVIDER; private float mLastX; /** * 记录刻度盘滑动的偏移量 */ private float mMove; private float mWidth, mHeight; private int mMinVelocity; private Scroller mScroller; private VelocityTracker mVelocityTracker; private OnValueChangeListener mListener; /** * 日期文字的宽度 */ float textWidth = 0; private TextPaint textPaint, dateAndTimePaint; private Paint linePaint; private boolean isNeedDrawableLeft, isNeedDrawableRight; private Calendar mCalendar; private Paint middlePaint, bgColorPaint; /** * */ private boolean isChangeFromInSide; public boolean isEnd; // 为了画背景色,从左向右画,记录下屏幕最左,最右处的时间点 private Calendar leftCalendar, rightCalendar; private List<TVideoFile> data; private int hour, minute, second; int gap = 12, indexWidth = 4, indexTitleWidth = 24, indexTitleHight = 10, shadow = 6; String color = "#FA690C"; String dateStr, timeStr; public ScalePanel(Context context, AttributeSet attrs) { super(context, attrs); mScroller = new Scroller(getContext()); mDensity = getContext().getResources().getDisplayMetrics().density; mMinVelocity = ViewConfiguration.get(getContext()) .getScaledMinimumFlingVelocity(); linePaint = new Paint(); linePaint.setStrokeWidth(2); linePaint.setColor(Color.parseColor("#464646")); bgColorPaint = new Paint(); bgColorPaint.setStrokeWidth(2); bgColorPaint.setColor(Color.parseColor("#00a3dd")); textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); textPaint.setTextSize(TEXT_SIZE * mDensity); dateAndTimePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); dateAndTimePaint.setTextSize(18 * mDensity); middlePaint = new Paint(); scaleUnit = mLineDivider * mDensity; mCalendar = Calendar.getInstance(); initDateAndTime(mCalendar); leftCalendar = Calendar.getInstance(); rightCalendar = Calendar.getInstance(); } /** * 根据时间来计算偏差,(minute*60+second)*scaleUnit/3600 */ private void initOffSet() { mMove = (minute * 60 + second) * scaleUnit / 3600; } private void initDateAndTime(Calendar mCalendar) { this.mCalendar = mCalendar; hour = mCalendar.get(Calendar.HOUR_OF_DAY); minute = mCalendar.get(Calendar.MINUTE); second = mCalendar.get(Calendar.SECOND); mValue = hour; initOffSet(); } /** * 通过设置calendar来设置刻度盘当前的时间 * * @param mCalendar */ public void setCalendar(Calendar mCalendar) { // 用户手指拖动刻度盘的时候,不接收外部的更新,以免冲突 if (!isChangeFromInSide) { initDateAndTime(mCalendar); initOffSet(); invalidate(); } } /** * 设置用于接收结果的监听器 * * @param listener */ public void setValueChangeListener(OnValueChangeListener listener) { mListener = listener; } /** * 获取当前刻度值 * * @return */ public float getValue() { return mValue; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { mWidth = getWidth(); mHeight = getHeight(); super.onLayout(changed, left, top, right, bottom); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawMiddleLine(canvas); drawScaleLine(canvas); } private float offsetPercent; private float scaleUnit; private boolean isChange = false; /** * 线条底部的位置 */ float lineBottom; /** * 线条顶部得到位置 */ float lineTop; /** * 从中间往两边开始画刻度线 * * @param canvas */ private void drawScaleLine(Canvas canvas) { canvas.save(); isNeedDrawableLeft = true; isNeedDrawableRight = true; float width = mWidth; float xPosition = 0; lineBottom = mHeight - getPaddingBottom(); lineTop = lineBottom - mDensity * ITEM_MAX_HEIGHT; if (data != null && data.size() > 0) { calulateDrawPosition(canvas); } //mValue的值控制在0~23之间 if (mValue > 0) { mValue = mValue % 24; } else if (mValue < 0) { mValue = mValue % 24 + 24; } if (mMove < 0) {//向左滑动 if (mValue == 0 && hour != 23) { mCalendar.set(Calendar.DAY_OF_MONTH, mCalendar.get(Calendar.DAY_OF_MONTH) - 1); } hour = mValue - 1; //滑到上一日23点 if (hour == -1) { hour = 23; } offsetPercent = 1 + mMove / scaleUnit; } else if (mMove >= 0) {//向右滑动, offsetPercent = mMove / scaleUnit; hour = mValue; //滑到次日0点, if (hour == 0 && !isChange) { //如果没有ischange,那么在hour==0时,day会重复加一 mCalendar.set(Calendar.DAY_OF_MONTH, mCalendar.get(Calendar.DAY_OF_MONTH) + 1); // 避免重复把day+1 isChange = true; } } if (hour != 0) { // 在hour切换成别的值的时候再把标志设为默认值 isChange = false; } countMinAndSecond(offsetPercent); drawTimeText(canvas); for (int i = 0; true; i++) { // 往右边开始画 xPosition = (width / 2 - mMove) + i * scaleUnit; if (isNeedDrawableRight && xPosition + getPaddingRight() < mWidth) {// 在view范围内画刻度 canvas.drawLine(xPosition, lineTop, xPosition, lineBottom, linePaint); textWidth = Layout.getDesiredWidth(int2Str(mValue + i), textPaint); canvas.drawText(int2Str(mValue + i), xPosition - (textWidth / 2), lineTop - 5, textPaint); } else { isNeedDrawableRight = false; } // 往左边开始画 if (i > 0) {// 防止中间的刻度画两遍 xPosition = (width / 2 - mMove) - i * scaleUnit; if (isNeedDrawableLeft && xPosition > getPaddingLeft()) { canvas.drawLine(xPosition, lineTop, xPosition, lineBottom, linePaint); textWidth = Layout.getDesiredWidth(int2Str(mValue - i), textPaint); canvas.drawText(int2Str(mValue - i), xPosition - (textWidth / 2), lineTop - 5, textPaint); } else { isNeedDrawableLeft = false; } } // 当不需要向左或者向右画的时候就退出循环,结束绘制操作 if (!isNeedDrawableLeft && !isNeedDrawableRight) { break; } } canvas.restore(); } /** * 还存在问题,如果data数据量过大,也就是用户搜索的时间跨度过大,这种方式肯定不行会卡死。 * 所以以后得通过获得当前回放所处的位置,然后选择前后一天左右的时间,这样数据量就不会太大 * 现在本着先做出来再优化的原则,记录下此问题,以后再做修改优化 * * @param canvas */ private void calulateDrawPosition(Canvas canvas) { // 距离和时间对应起来 ((mWidth/2/scaleUnit)*3600*1000) long timeOffset = (long) ((mWidth / 2 / scaleUnit) * 3600 * 1000); long middleTime = mCalendar.getTimeInMillis(); // 根据时间偏移算出左右的时间 leftCalendar.setTimeInMillis(middleTime - timeOffset); rightCalendar.setTimeInMillis(middleTime + timeOffset); // 找到时间开始点,然后顺序向右画,直到画到屏幕最右侧,关键是找到时间开始点 // 时间开始点就是从什么地方开始画背景色 for (int position = 0; position < data.size(); position++) { TVideoFile tVideoFile = data.get(position); Calendar startCalendar = tVideoFile.startTime; Calendar endCalendar = tVideoFile.endTime; if (leftCalendar.before(startCalendar) && rightCalendar.after(startCalendar)) { // 从start从开始画 drawBgColor(canvas, startCalendar, endCalendar, position); break; } else if (leftCalendar.after(startCalendar) && leftCalendar.before(endCalendar)) { // 从left从开始画 drawBgColor(canvas, leftCalendar, endCalendar, position); break; } } } /** * * @param canvas * @param start * 第一块背景色开始的位置 * @param distance * 第一块背景色的长度 * @param position * 第一块背景色所在时间片段在data中所处的position,下一块从position+1开始 */ public void drawBgColor(Canvas canvas, Calendar startTime, Calendar endTime, int position) { // 根据时间获得在刻度盘上具体的位置 float startPosition = getPositionByTime(startTime); float endPosition = getPositionByTime(endTime); drawBgColorRect(startPosition, lineTop, endPosition, lineBottom, canvas); for (int i = position + 1; i < data.size(); i++) { TVideoFile tVideoFile = data.get(i); Calendar startCalendar = tVideoFile.startTime; Calendar endCalendar = tVideoFile.endTime; startPosition = getPositionByTime(startCalendar); endPosition = getPositionByTime(endCalendar); if (startPosition <= mWidth) {// 只画屏幕屏幕区域以内的 drawBgColorRect(startPosition, lineTop, endPosition, lineBottom, canvas); } else { break; } } } /** * 画背景色 * * @param canvas */ private void drawBgColorRect(float left, float top, float right, float bottom, Canvas canvas) { canvas.drawRect(left, top, right, bottom, bgColorPaint); } /** * 根据时间获得在刻度盘上具体的位置 * * @param calendar * @return */ public float getPositionByTime(Calendar calendar) { long middleTime = mCalendar.getTimeInMillis(); float position = 0; long timeOffset = middleTime - calendar.getTimeInMillis(); if (timeOffset >= 0) { position = (float) (mWidth / 2 - (1.0 * timeOffset / 3600 / 1000) * scaleUnit); } else { position = (float) (mWidth / 2 - (1.0 * timeOffset / 3600 / 1000) * scaleUnit); } return position; } /** * 准备画背景色的数据 */ public void setTimeData(List<TVideoFile> data) { this.data = data; } /** * 画日期时间的文字 * * @param canvas */ private void drawTimeText(Canvas canvas) { mCalendar.set(Calendar.HOUR_OF_DAY, hour); mCalendar.set(Calendar.MINUTE, minute); mCalendar.set(Calendar.SECOND, second); timeStr = date2timeStr(mCalendar.getTime()); textWidth = Layout.getDesiredWidth(timeStr, textPaint); canvas.drawText(timeStr, mWidth / 2 + 15 * mDensity, 50, dateAndTimePaint); drawDateText(canvas); } private void drawDateText(Canvas canvas) { dateStr = date2DateStr(mCalendar.getTime()); textWidth = Layout.getDesiredWidth(dateStr, textPaint); canvas.drawText(dateStr, mWidth / 2 - textWidth - 35 * mDensity, 50, dateAndTimePaint); } /** * 计算分钟和秒钟 * @param percent * @return */ public int[] countMinAndSecond(float percent) { minute = (int) (3600 * percent / 60); second = (int) (3600 * percent % 60); return new int[] { minute, second }; } /** * 画中间的红色指示线、阴影等。指示线两端简单的用了两个矩形代替 * * @param canvas */ private void drawMiddleLine(Canvas canvas) { canvas.save(); middlePaint.setStrokeWidth(indexWidth); middlePaint.setColor(Color.parseColor(color)); canvas.drawLine(mWidth / 2, 0, mWidth / 2, mHeight, middlePaint); canvas.restore(); } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); int xPosition = (int) event.getX(); if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); switch (action) { case MotionEvent.ACTION_DOWN: mScroller.forceFinished(true); mLastX = xPosition; isChangeFromInSide = true; break; case MotionEvent.ACTION_MOVE: mMove += (mLastX - xPosition); changeMoveAndValue(); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: countMoveEnd(); countVelocityTracker(event); return false; default: break; } mLastX = xPosition; return true; } private void changeMoveAndValue() { float fValue = mMove / scaleUnit; int tValue = (int) fValue; //滑动超过一格以后,记录下当前刻度盘上的值 if (Math.abs(fValue) > 0) { mValue += tValue; //偏移量永远都小于一格 mMove -= tValue * scaleUnit; notifyValueChange(); postInvalidate(); } } private void countVelocityTracker(MotionEvent event) { mVelocityTracker.computeCurrentVelocity(1000, 1500); float xVelocity = mVelocityTracker.getXVelocity(); if (Math.abs(xVelocity) > mMinVelocity) { mScroller.fling(0, 0, (int) xVelocity, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0); } else { notifyChangeOver(); } } private void countMoveEnd() { mLastX = 0; notifyValueChange(); postInvalidate(); } private void notifyValueChange() { if (null != mListener) { mListener.onValueChange(mValue); } } private void notifyChangeOver() { if (null != mListener) { mListener.onValueChangeEnd(mCalendar); } isChangeFromInSide = false; } @Override public void computeScroll() { super.computeScroll(); if (mScroller.computeScrollOffset()) { if (mScroller.getCurrX() == mScroller.getFinalX()) { // over countMoveEnd(); notifyChangeOver(); } else { int xPosition = mScroller.getCurrX(); mMove += (mLastX - xPosition); changeMoveAndValue(); mLastX = xPosition; } } } public String int2Str(int i) { if (i > 0) { i = i % 24; } else if (i < 0) { i = i % 24 + 24; } String str = String.valueOf(i); if (str.length() == 1) { return "0" + str + ":00"; } else if (str.length() == 2) { return str + ":00"; } return ""; } public String date2DateStr(Date date) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); return dateFormat.format(date); } public String date2timeStr(Date date) { SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss"); return dateFormat.format(date); } }我提供了setCalendar方法供外界来设置刻度盘的当前时间,并且提供了onValueChange(float value)和onValueChangeEnd(Calendar mCalendar)来分别提供实时监听和滑动结束的监听,如果想要绘制时间块的背景色可以这样
public class MainActivity extends Activity implements OnValueChangeListener { /** * 时间刻度盘 */ private ScalePanel scalePanel; List<TVideoFile> data = new ArrayList<TVideoFile>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initData(); scalePanel = (ScalePanel) findViewById(R.id.scalePanel); scalePanel.setValueChangeListener(this); Calendar mCalendar = Calendar.getInstance(); //设置时间块数据 scalePanel.setTimeData(data); //设置当前时间 scalePanel.setCalendar(mCalendar); } private void initData() { for (int hourOffset = -5; Math.abs(hourOffset) <= 5; hourOffset++) { addTimeBloack(hourOffset); } } private void addTimeBloack(int hourOffset) { TVideoFile file = new TVideoFile(); Calendar startTime = Calendar.getInstance(); startTime.set(Calendar.HOUR_OF_DAY, startTime.get(Calendar.HOUR_OF_DAY) + hourOffset); startTime.set(Calendar.MINUTE, 0); file.startTime = startTime; Calendar endTime = Calendar.getInstance(); endTime.set(Calendar.HOUR_OF_DAY, endTime.get(Calendar.HOUR_OF_DAY) + hourOffset); endTime.set(Calendar.MINUTE, 50); file.endTime = endTime; data.add(file); } @Override public void onValueChange(float value) { } @Override public void onValueChangeEnd(Calendar mCalendar) { } }
具体的实现可以细看代码和注释,代码中有些关于scroller的使用我没有做任何说明,如果你对scroller的使用还不是很熟悉,可以阅读下这篇文章Android开发之Scroller的使用详解
如果有不明白的地方可以和我讨论。
最后留下demo,如有需要可以看看,欢迎留下你宝贵的意见。