Android开发之时间刻度盘

一、最近的一个项目中有遇到时间刻度盘的需求,在网上没找到合适的,于是自己就花点时间实现了,现在分享出来,效果如下图:

技术分享

在介绍如何实现之前,先大概介绍一个这个时间刻度盘的功能:

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,如有需要可以看看,欢迎留下你宝贵的意见。


郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。