自定义View合辑(1)-时钟

作者: 业志陈 | 来源:发表于2019-05-04 19:23 被阅读14次

    为了加强对自定义 View 的认知以及开发能力,我计划这段时间陆续来完成几个难度从易到难的自定义 View,并简单的写几篇博客来进行介绍,所有的代码也都会开源,也希望读者能给个 star 哈
    GitHub 地址:https://github.com/leavesC/CustomView
    也可以下载 Apk 来体验下:https://www.pgyer.com/CustomView

    先看下效果图:

    ClockView 的逻辑并不算复杂,重点在于时钟刻度以及三根指示针的绘制,然后设定一个定时任务每秒刷新绘制即可

    一、确定宽高

    为 View 设定其默认大小为 DEFAULT_SIZE

        //View的默认大小,dp
        private static final int DEFAULT_SIZE = 320;
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int defaultSize = dp2px(DEFAULT_SIZE);
            int widthSize = getSize(widthMeasureSpec, defaultSize);
            int heightSize = getSize(heightMeasureSpec, defaultSize);
            widthSize = heightSize = Math.min(widthSize, heightSize);
            setMeasuredDimension(widthSize, heightSize);
        }
    
        protected int getSize(int measureSpec, int defaultSize) {
            int mode = MeasureSpec.getMode(measureSpec);
            int size = 0;
            switch (mode) {
                case MeasureSpec.AT_MOST: {
                    size = Math.min(MeasureSpec.getSize(measureSpec), defaultSize);
                    break;
                }
                case MeasureSpec.EXACTLY: {
                    size = MeasureSpec.getSize(measureSpec);
                    break;
                }
                case MeasureSpec.UNSPECIFIED: {
                    size = defaultSize;
                    break;
                }
            }
            return size;
        }
    

    二、初始化画笔

    在构造函数中初始化绘制表盘以及文本的画笔

        public ClockView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            initClockPaint();
            initTextPaint();
            time = new Time();
            timerHandler = new TimerHandler(this);
        }
    
        private void initClockPaint() {
            clockPaint = new Paint();
            clockPaint.setStyle(Paint.Style.STROKE);
            clockPaint.setAntiAlias(true);
            clockPaint.setStrokeWidth(aroundStockWidth);
        }
    
        private void initTextPaint() {
            textPaint = new Paint();
            textPaint.setStyle(Paint.Style.FILL);
            textPaint.setAntiAlias(true);
            textPaint.setStrokeWidth(12);
            textPaint.setTextAlign(Paint.Align.CENTER);
            textPaint.setTextSize(textSize);
        }
    

    三、绘制

    在此处是通过不断转换 Canvas 的坐标系来完成刻度以及指示针的绘制,这相比通过数学计算来计算各个刻度的位置要简单得多

        @Override
        protected void onDraw(Canvas canvas) {
            //中心点的横纵坐标
            float pointWH = getWidth() / 2.0f;
            //内圆的半径
            float radiusIn = pointWH - aroundStockWidth;
    
            canvas.translate(pointWH, pointWH);
    
            //绘制表盘
            if (aroundStockWidth > 0) {
                clockPaint.setStrokeWidth(aroundStockWidth);
                clockPaint.setStyle(Paint.Style.STROKE);
                clockPaint.setColor(aroundColor);
                canvas.drawCircle(0, 0, pointWH - aroundStockWidth / 2.0f, clockPaint);
            }
            clockPaint.setStyle(Paint.Style.FILL);
            clockPaint.setColor(Color.WHITE);
            canvas.drawCircle(0, 0, radiusIn, clockPaint);
    
            //绘制小短线
            canvas.save();
            canvas.rotate(-90);
            float longLineLength = radiusIn / 16.0f;
            float longStartY = radiusIn - longLineLength;
            float longStopY = longStartY - longLineLength;
            float longStockWidth = 2;
            float temp = longLineLength / 4.0f;
            float shortStartY = longStartY - temp;
            float shortStopY = longStopY + temp;
            float shortStockWidth = longStockWidth / 2.0f;
            clockPaint.setColor(Color.BLACK);
            float degrees = 6;
            for (int i = 0; i <= 360; i += degrees) {
                if (i % 30 == 0) {
                    clockPaint.setStrokeWidth(longStockWidth);
                    canvas.drawLine(0, longStartY, 0, longStopY, clockPaint);
                } else {
                    clockPaint.setStrokeWidth(shortStockWidth);
                    canvas.drawLine(0, shortStartY, 0, shortStopY, clockPaint);
                }
                canvas.rotate(degrees);
            }
            canvas.restore();
    
            //绘制时钟数字
            if (textSize > 0) {
                float x, y;
                for (int i = 1; i <= 12; i += 1) {
                    textPaint.getTextBounds(String.valueOf(i), 0, String.valueOf(i).length(), rect);
                    float textHeight = rect.height();
                    float distance = radiusIn - 2 * longLineLength - textHeight;
                    double tempVa = i * 30.0f * Math.PI / 180.0f;
                    x = (float) (distance * Math.sin(tempVa));
                    y = (float) (-distance * Math.cos(tempVa));
                    canvas.drawText(String.valueOf(i), x, y + textHeight / 3, textPaint);
                }
            }
    
            canvas.rotate(-90);
    
            clockPaint.setStrokeWidth(2);
            //绘制时针
            canvas.save();
            canvas.rotate(hour / 12.0f * 360.0f);
            canvas.drawLine(-30, 0, radiusIn / 2.0f, 0, clockPaint);
            canvas.restore();
            //绘制分针
            canvas.save();
            canvas.rotate(minute / 60.0f * 360.0f);
            canvas.drawLine(-30, 0, radiusIn * 0.7f, 0, clockPaint);
            canvas.restore();
            //绘制秒针
            clockPaint.setColor(Color.parseColor("#fff2204d"));
            canvas.save();
            canvas.rotate(second / 60.0f * 360.0f);
            canvas.drawLine(-30, 0, radiusIn * 0.85f, 0, clockPaint);
            canvas.restore();
            //绘制中心小圆点
            clockPaint.setStyle(Paint.Style.FILL);
            clockPaint.setColor(clockCenterColor);
            canvas.drawCircle(0, 0, radiusIn / 20.0f, clockPaint);
        }
    

    四、动画效果

    onDraw 方法只是完成了 View 的绘制,此处还需要思考如何令时钟“动”起来。本 Demo 是通过 Handler 来设定定时任务的,当 View 处于可见状态时就每隔一秒主动刷新界面

    为了避免内存泄漏问题,此处通过弱引用的形式来引用 ClockView

        private static final int MSG_INVALIDATE = 10;
    
        private static final class TimerHandler extends Handler {
    
            private WeakReference<ClockView> clockViewWeakReference;
    
            private TimerHandler(ClockView clockView) {
                clockViewWeakReference = new WeakReference<>(clockView);
            }
    
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case MSG_INVALIDATE: {
                        Log.e(TAG, "定时任务被触发...");
                        ClockView view = clockViewWeakReference.get();
                        if (view != null) {
                            view.onTimeChanged();
                            view.invalidate();
                            sendEmptyMessageDelayed(MSG_INVALIDATE, 1000);
                        }
                        break;
                    }
                }
            }
        }
    
        private void onTimeChanged() {
            time.setToNow();
            minute = time.minute;
            hour = time.hour + minute / 60.0f;
            second = time.second;
        }
    

    五、适用多时区

    为了在系统时区改变时能够进行相应的时间变化,此处还需要监听系统的 Intent.ACTION_TIMEZONE_CHANGED 广播

      private final BroadcastReceiver timerBroadcast = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                if (action != null) {
                    switch (action) {
                        //监听时区的变化
                        case Intent.ACTION_TIMEZONE_CHANGED: {
                            time = new Time(TimeZone.getTimeZone(intent.getStringExtra("time-zone")).getID());
                            break;
                        }
                    }
                }
            }
        };
    

    在 View 可见的时候注册广播,不可见的时候就解除注册

        @Override
        protected void onDetachedFromWindow() {
            super.onDetachedFromWindow();
            Log.e(TAG, "onDetachedFromWindow");
            stopTimer();
            unregisterTimezoneAction();
        }
    
        @Override
        protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
            super.onVisibilityChanged(changedView, visibility);
            Log.e(TAG, "onVisibilityChanged visibility: " + visibility);
            if (visibility == View.VISIBLE) {
                registerTimezoneAction();
                startTimer();
            } else {
                stopTimer();
                unregisterTimezoneAction();
            }
        }
    
        private void startTimer() {
            Log.e(TAG, "startTimer 开启定时任务");
            timerHandler.removeMessages(MSG_INVALIDATE);
            timerHandler.sendEmptyMessage(MSG_INVALIDATE);
        }
    
        private void stopTimer() {
            Log.e(TAG, "stopTimer 停止定时任务");
            timerHandler.removeMessages(MSG_INVALIDATE);
        }
    
        private void registerTimezoneAction() {
            IntentFilter filter = new IntentFilter();
            filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
            getContext().registerReceiver(timerBroadcast, filter);
        }
    
        private void unregisterTimezoneAction() {
            getContext().unregisterReceiver(timerBroadcast);
        }
    

    相关文章

      网友评论

        本文标题:自定义View合辑(1)-时钟

        本文链接:https://www.haomeiwen.com/subject/jgqwnqtx.html