Android 繪制表盤

2018-08-02 17:55 更新

編寫:heray1990 - 原文: http://developer.android.com/training/wearables/watch-faces/drawing.html

配置完工程和添加了實現(xiàn)表盤服務(watch face service)的類之后,我們可以開始編寫初始化和繪制自定義表盤的代碼了。

這節(jié)課通過 Android SDK 中的 WatchFace 示例,來介紹系統(tǒng)是如何調(diào)用表盤服務的方法。這個示例位于 android-sdk/samples/android-21/wearable/WatchFace 目錄。這里描述服務實現(xiàn)的很多方面(例如初始化和檢測設(shè)備功能)可以應用到任意表盤,所以我們可以重用一些代碼到我們的表盤當中。

Figure 1. WatchFace 示例中的模擬和數(shù)字表盤

初始化表盤

當系統(tǒng)加載我們的服務時,我們應該分配和初始化表盤需要的大部分資源,包括加載位圖資源、創(chuàng)建定時器對象來運行自定義動畫、配置顏色風格和執(zhí)行其他運算。我們通常只執(zhí)行一次這些操作和重用它們的結(jié)果。這個習慣可以提高表盤的性能并且更容易地維護代碼。

初始化表盤,需要:

  1. 為自定義定時器、圖形對象和其它組件聲明變量。
  2. 在 Engine.onCreate() 方法中初始化表盤組件。
  3. 在 Engine.onVisibilityChanged() 方法中初始化自定義定時器。

下面的部分詳細介紹了上述幾個步驟。

聲明變量

當系統(tǒng)加載我們的服務時,我們初始化的那些資源需要在我們實現(xiàn)的不同點都可以被訪問,所以我們可以重用這些資源。我們可以通過在 WatchFaceService.Engine 實現(xiàn)中為這些資源聲明成員變量來達到上述目的。

為下面的組件聲明變量:

圖形對象

大部分表盤至少包含一個位圖用于表盤的背景,如創(chuàng)建實施策略描述的一樣。我們可以使用額外的位圖圖像來表示表盤的時鐘指針或者其它設(shè)計元素。

定時計時器

當時間變化時,系統(tǒng)每隔一分鐘會通知表盤一次,但一些表盤會根據(jù)自定義的時間間隔來運行動畫。在這種情況下,我們需要用一個按照所需頻率計數(shù)的自定義定時器來刷新表盤。

時區(qū)變化接收器

用戶可以在旅游的時候調(diào)整時區(qū),系統(tǒng)會廣播這個事件。我們的服務實現(xiàn)必須注冊一個廣播接收器,該廣播接收器用于接收時區(qū)改變或者更新時間的通知。

WatchFace 示例中的 AnalogWatchFaceService.Engine 類定義了上述變量(見下面的代碼)。自定義定時器實現(xiàn)為一個 Handler 實例,該 Handler 實例使用線程的消息隊列發(fā)送和處理延遲的消息。對于這個特定的表盤,自定義定時器每秒計數(shù)一次。當定時器計數(shù),handler 調(diào)用 invalidate() 方法,然后系統(tǒng)調(diào)用 onDraw() 方法重新繪制表盤。

private class Engine extends CanvasWatchFaceService.Engine {
    static final int MSG_UPDATE_TIME = 0;

    /* a time object */
    Time mTime;

    /* device features */
    boolean mLowBitAmbient;

    /* graphic objects */
    Bitmap mBackgroundBitmap;
    Bitmap mBackgroundScaledBitmap;
    Paint mHourPaint;
    Paint mMinutePaint;
    ...

    /* handler to update the time once a second in interactive mode */
    final Handler mUpdateTimeHandler = new Handler() {
        @Override
        public void handleMessage(Message message) {
            switch (message.what) {
                case MSG_UPDATE_TIME:
                    invalidate();
                    if (shouldTimerBeRunning()) {
                        long timeMs = System.currentTimeMillis();
                        long delayMs = INTERACTIVE_UPDATE_RATE_MS
                                - (timeMs % INTERACTIVE_UPDATE_RATE_MS);
                        mUpdateTimeHandler
                            .sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
                    }
                    break;
            }
        }
    };

    /* receiver to update the time zone */
    final BroadcastReceiver mTimeZoneReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            mTime.clear(intent.getStringExtra("time-zone"));
            mTime.setToNow();
        }
    };

    /* service methods (see other sections) */
    ...
}

初始化表盤組件

在為位圖資源、色彩風格和其它每次重新繪制表盤都會重用的組件聲明成員變量之后,在系統(tǒng)加載服務時初始化這些組件。只初始化這些組件一次,然后重用它們以提升性能和電池使用時間。

在 Engine.onCreate() 方法中,初始化下面的組件:

  • 加載背景圖片。
  • 創(chuàng)建風格和色彩來繪制圖形對象。
  • 分配一個對象來保存時間。
  • 配置系統(tǒng) UI。

在 AnalogWatchFaceService 類的 Engine.onCreate() 方法初始化這些組件的代碼如下:

@Override
public void onCreate(SurfaceHolder holder) {
    super.onCreate(holder);

    /* configure the system UI (see next section) */
    ...

    /* load the background image */
    Resources resources = AnalogWatchFaceService.this.getResources();
    Drawable backgroundDrawable = resources.getDrawable(R.drawable.bg);
    mBackgroundBitmap = ((BitmapDrawable) backgroundDrawable).getBitmap();

    /* create graphic styles */
    mHourPaint = new Paint();
    mHourPaint.setARGB(255, 200, 200, 200);
    mHourPaint.setStrokeWidth(5.0f);
    mHourPaint.setAntiAlias(true);
    mHourPaint.setStrokeCap(Paint.Cap.ROUND);
    ...

    /* allocate an object to hold the time */
    mTime = new Time();
}

當系統(tǒng)初始化表盤時,只會加載背景位圖一次。圖形風格被 Paint 類實例化。然后我們在 Engine.onDraw() 方法中使用這些風格來繪制表盤的組件,如繪制表盤描述的那樣。

初始化自定義定時器

作為表盤開發(fā)者,我們通過使定時器按照要求的頻率計數(shù),來決定設(shè)備在交互模式時多久更新一次表盤。這使得我們可以創(chuàng)建自定義的動畫和其它視覺效果。

Note: 在環(huán)境模式下,系統(tǒng)不會可靠地調(diào)用自定義定時器。關(guān)于在環(huán)境模式下更新表盤的內(nèi)容,請看在環(huán)境模式下更新表盤。

聲明變量部分介紹了一個 AnalogWatchFaceService 類定義的每秒計數(shù)一次的定時器例子。在 Engine.onVisibilityChanged() 方法里,如果滿足如下兩個條件,則啟動自定義定時器:

  • 表盤可見的。
  • 設(shè)備處于交互模式。

如果有必要,AnalogWatchFaceService 會調(diào)度下一個定時器進行計數(shù):

private void updateTimer() {
    mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
    if (shouldTimerBeRunning()) {
        mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
    }
}

private boolean shouldTimerBeRunning() {
    return isVisible() && !isInAmbientMode();
}

該自定義定時器每秒計數(shù)一次,如聲明變量介紹的一樣。

在 Engine.onVisibilityChanged() 方法中,按要求啟動定時器并為時區(qū)的變化注冊接收器:

@Override
public void onVisibilityChanged(boolean visible) {
    super.onVisibilityChanged(visible);

    if (visible) {
        registerReceiver();

        // Update time zone in case it changed while we weren't visible.
        mTime.clear(TimeZone.getDefault().getID());
        mTime.setToNow();
    } else {
        unregisterReceiver();
    }

    // Whether the timer should be running depends on whether we're visible and
    // whether we're in ambient mode), so we may need to start or stop the timer
    updateTimer();
}

當表盤可見時,onVisibilityChanged() 方法為時區(qū)變化注冊了接收器,并且如果設(shè)備在交互模式,則啟動自定義定時器。當表盤不可見,這個方法停止自定義定時器并且注銷檢測時區(qū)變化的接收器。下面是registerReceiver() 和 unregisterReceiver() 方法的實現(xiàn):

private void registerReceiver() {
    if (mRegisteredTimeZoneReceiver) {
        return;
    }
    mRegisteredTimeZoneReceiver = true;
    IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
    AnalogWatchFaceService.this.registerReceiver(mTimeZoneReceiver, filter);
}

private void unregisterReceiver() {
    if (!mRegisteredTimeZoneReceiver) {
        return;
    }
    mRegisteredTimeZoneReceiver = false;
    AnalogWatchFaceService.this.unregisterReceiver(mTimeZoneReceiver);
}

在環(huán)境模式下更新表盤

在環(huán)境模式下,系統(tǒng)每分鐘調(diào)用一次 Engine.onTimeTick() 方法。通常在這種模式下,每分鐘更新一次表盤已經(jīng)足夠了。為了在環(huán)境模式下更新表盤,我們必須使用一個在初始化自定義定時器介紹的自定義定時器。

在環(huán)境模式下,大部分表盤實現(xiàn)在 Engine.onTimeTick() 方法中簡單地銷毀畫布來重新繪制表盤:

@Override
public void onTimeTick() {
    super.onTimeTick();

    invalidate();
}

配置系統(tǒng) UI

表盤不應該干涉系統(tǒng) UI 組件,在 Accommodate System UI Element 中有介紹。如果我們的表盤背景比較亮或者在屏幕的底部附近顯示了信息,那么我們可能要配置 notification cards 的尺寸或者啟用背景保護。

當表盤在動的時候,Android Wear 允許我們配置系統(tǒng) UI 的下面幾個方面:

  • 指定第一個 notification card 離屏幕有多遠。
  • 指定系統(tǒng)是否將時間繪制在表盤上。
  • 在環(huán)境模式下,顯示或者隱藏 notification card。
  • 用純色背景保護系統(tǒng)指針。
  • 指定系統(tǒng)指針的位置。

為了配置這些方面的系統(tǒng) UI,需要創(chuàng)建一個 WatchFaceStyle 實例并且將其傳進 Engine.setWatchFaceStyle()方法。

下面是 AnalogWatchFaceService 類配置系統(tǒng) UI 的方法:

@Override
public void onCreate(SurfaceHolder holder) {
    super.onCreate(holder);

    /* configure the system UI */
    setWatchFaceStyle(new WatchFaceStyle.Builder(AnalogWatchFaceService.this)
            .setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
            .setBackgroundVisibility(WatchFaceStyle
                                    .BACKGROUND_VISIBILITY_INTERRUPTIVE)
            .setShowSystemUiTime(false)
            .build());
    ...
}

上述的代碼將 card 配置成一行高,card 的背景只會簡單地顯示和只用于中斷的 notification,不會顯示系統(tǒng)時間(因為表盤會繪制自己的時間)。

我們可以在表盤實現(xiàn)的任意時刻配置系統(tǒng)的 UI 風格。例如,如果用戶選擇了白色背景,我們可以為系統(tǒng)指針添加背景保護。

更多關(guān)于配置系統(tǒng) UI 的內(nèi)容,請見 WatchFaceStyle 類的 API 參考文檔。

獲得設(shè)備屏幕信息

當系統(tǒng)確定了設(shè)備屏幕的屬性時,系統(tǒng)會調(diào)用 Engine.onPropertiesChanged() 方法,例如設(shè)備是否使用低比特率的環(huán)境模式和屏幕是否需要燒毀保護。

下面的代碼介紹如何獲得這些屬性:

@Override
public void onPropertiesChanged(Bundle properties) {
    super.onPropertiesChanged(properties);
    mLowBitAmbient = properties.getBoolean(PROPERTY_LOW_BIT_AMBIENT, false);
    mBurnInProtection = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION,
            false);
}

當繪制表盤時,我們應該考慮這些設(shè)備屬性。

  • 對于使用低比特率環(huán)境模式的設(shè)備,屏幕在環(huán)境模式下為每種顏色提供更少的比特,所以當設(shè)備切換到環(huán)境模式時,我們應該禁用抗鋸齒和位圖濾鏡。
  • 對于要求燒毀保護的設(shè)備,在環(huán)境模式下避免使用大塊的白色像素,并且不要將內(nèi)容放在離屏幕邊緣 10 個像素范圍內(nèi),因為系統(tǒng)會周期地改變內(nèi)容以避免像素燒毀。

更多關(guān)于低比特率環(huán)境模式和燒毀保護的內(nèi)容,請見 Optimize for Special Screens。更多關(guān)于如何禁用位圖濾鏡的內(nèi)容,請見位圖濾鏡

響應兩種模式間的變化

當設(shè)備在環(huán)境模式和交互模式之間轉(zhuǎn)換時,系統(tǒng)會調(diào)用 Engine.onAmbientModeChanged() 方法。我們的服務實現(xiàn)應該對在兩種模式間切換作出必要的調(diào)整,然后調(diào)用 invalidate() 方法來重新繪制表盤。

下面的代碼介紹了這個方法如何在 WatchFace 示例的 AnalogWatchFaceService 類中實現(xiàn):

@Override
public void onAmbientModeChanged(boolean inAmbientMode) {

    super.onAmbientModeChanged(inAmbientMode);

    if (mLowBitAmbient) {
        boolean antiAlias = !inAmbientMode;
        mHourPaint.setAntiAlias(antiAlias);
        mMinutePaint.setAntiAlias(antiAlias);
        mSecondPaint.setAntiAlias(antiAlias);
        mTickPaint.setAntiAlias(antiAlias);
    }
    invalidate();
    updateTimer();
}

這個例子對一些圖形風格做出了調(diào)整和銷毀畫布,使得系統(tǒng)可以重新繪制表盤。

繪制表盤

繪制自定義的表盤,系統(tǒng)調(diào)用帶有 Canvas 實例和繪制表盤所在的 bounds 兩個參數(shù)的 Engine.onDraw() 方法。bounds 參數(shù)說明任意內(nèi)插的區(qū)域,如一些圓形設(shè)備底部的“下巴”。我們可以像下面介紹的一樣來使用畫布繪制表盤:

  1. 如果是首次調(diào)用 onDraw() 方法,縮放背景來匹配它。
  2. 檢查設(shè)備處于環(huán)境模式還是交互模式。
  3. 處理任何圖形計算。
  4. 在畫布上繪制背景位圖。
  5. 使用 Canvas 類中的方法繪制表盤。

在 WatchFace 示例中的 AnalogWatchFaceService 類按照如下這些步驟來實現(xiàn) onDraw() 方法:

@Override
public void onDraw(Canvas canvas, Rect bounds) {
    // Update the time
    mTime.setToNow();

    int width = bounds.width();
    int height = bounds.height();

    // Draw the background, scaled to fit.
    if (mBackgroundScaledBitmap == null
        || mBackgroundScaledBitmap.getWidth() != width
        || mBackgroundScaledBitmap.getHeight() != height) {
        mBackgroundScaledBitmap = Bitmap.createScaledBitmap(mBackgroundBitmap,
                                         width, height, true /* filter */);
    }
    canvas.drawBitmap(mBackgroundScaledBitmap, 0, 0, null);

    // Find the center. Ignore the window insets so that, on round watches
    // with a "chin", the watch face is centered on the entire screen, not
    // just the usable portion.
    float centerX = width / 2f;
    float centerY = height / 2f;

    // Compute rotations and lengths for the clock hands.
    float secRot = mTime.second / 30f * (float) Math.PI;
    int minutes = mTime.minute;
    float minRot = minutes / 30f * (float) Math.PI;
    float hrRot = ((mTime.hour + (minutes / 60f)) / 6f ) * (float) Math.PI;

    float secLength = centerX - 20;
    float minLength = centerX - 40;
    float hrLength = centerX - 80;

    // Only draw the second hand in interactive mode.
    if (!isInAmbientMode()) {
        float secX = (float) Math.sin(secRot) * secLength;
        float secY = (float) -Math.cos(secRot) * secLength;
        canvas.drawLine(centerX, centerY, centerX + secX, centerY +
                        secY, mSecondPaint);
    }

    // Draw the minute and hour hands.
    float minX = (float) Math.sin(minRot) * minLength;
    float minY = (float) -Math.cos(minRot) * minLength;
    canvas.drawLine(centerX, centerY, centerX + minX, centerY + minY,
                    mMinutePaint);
    float hrX = (float) Math.sin(hrRot) * hrLength;
    float hrY = (float) -Math.cos(hrRot) * hrLength;
    canvas.drawLine(centerX, centerY, centerX + hrX, centerY + hrY,
                    mHourPaint);
}

這個方法根據(jù)現(xiàn)在的時間計算時鐘指針的位置和使用在 onCreate() 方法中初始化的圖形風格將時鐘指針繪制在背景位圖之上。其中,秒針只會在交互模式下繪制出來,環(huán)境模式不會顯示。

更多的關(guān)于用 Canvas 實例繪制的內(nèi)容,請見 Canvas and Drawables

在 Android SDK 的 WatchFace 示例包括附加的表盤,我們可以用作如何實現(xiàn) onDraw() 方法的例子。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號