Android開發之時間刻度盤
阿新 • • 發佈:2019-01-31
一、最近的一個專案中有遇到時間刻度盤的需求,在網上沒找到合適的,於是自己就花點時間實現了,現在分享出來,效果如下圖:
在介紹如何實現之前,先大概介紹一個這個時間刻度盤的功能:
1、顯示當前時間,並且可以左右拖動至上一天或者下一天,
2、根據傳入的時間塊來繪製藍色部分
二、程式碼實現
我提供了setCalendar方法供外界來設定刻度盤的當前時間,並且提供了onValueChange(float value)和onValueChangeEnd(Calendar mCalendar)來分別提供實時監聽和滑動結束的監聽,如果想要繪製時間塊的背景色可以這樣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); } }
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,如有需要可以看看,歡迎留下你寶貴的意見。