為了賬號安全,請及時綁定郵箱和手機立即綁定

自定義View合輯(1)-時鐘

2019.05.04 19:11 1594瀏覽

為了加強對自定義 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);
    }
點擊查看更多內容

本文首次發布于慕課網 ,轉載請注明出處,謝謝合作

2人點贊

若覺得本文不錯,就分享一下吧!

評論

相關文章推薦

正在加載中
意見反饋 幫助中心 APP下載
官方微信

舉報

0/150
提交
取消
lpl竞猜