«

Android开发之时间刻度盘

时间:2024-3-2 17:47     作者:韩俊     分类: 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,如有需要可以看看,欢迎留下你宝贵的意见。

标签: android

热门推荐