Статьи

Создание Android Wear Watch Face

Одной из особенностей, которая делает Android таким особенным, является возможность настраивать каждый аспект пользовательского опыта. Когда Android Wear впервые был представлен на Google I / O 2014, многие разработчики и пользователи обнаружили, что это не совсем верно для умных часов, поскольку официальный API для создания циферблатов явно отсутствовал. Учитывая, что возможность создания пользовательских циферблатов была одной из ключевых задач пользователей, неудивительно, что разработчики обнаружили способ создания собственных циферблатов с недокументированным хаком в Android Wear.

К счастью, Google быстро дал всем понять, что официальный API уже в пути, и в декабре 2014 года этот API был наконец выпущен сообществу разработчиков. В этой статье вы узнаете об официальном API Watch Faces для Android Wear и создадите простой цифровой циферблат, который вы сможете расширить для своих собственных нужд. Реализация циферблатов может быть немного многословной, но вы можете найти пример приложения для этой статьи на GitHub .

Первое, что вам нужно сделать, чтобы создать свой собственный циферблат, — это настроить свой проект в Android Studio. При создании проекта выберите « Телефон и планшет» с минимальным SDK API 18, так как Android 4.3 — это самая низкая версия операционной системы для поддержки прилагаемых приложений Android Wear. Вам также нужно будет установить флажок « Износ» с выбранным минимальным SDK API 21. Вы можете увидеть пример того, как должен выглядеть экран ваших Target Android Devices .

Когда вы дойдете до двух экранов « Добавить активность» , выберите « Добавить без активности» для обоих экранов.

После того, как вы нажмете кнопку « Готово» , среда вашего проекта должна создать и иметь модуль для мобильных устройств и еще один для износа .

Android Wear реализует циферблаты с помощью WatchFaceService . В этой статье вы создадите расширение класса CanvasWatchFaceService , который является реализацией WatchFaceService который также предоставляет Canvas для рисования циферблата. Начните с создания нового класса Java в модуле износа в Android Studio, который расширяет CanvasWatchFaceService .

1
public class WatchFaceService extends CanvasWatchFaceService

Когда у вас будет свой класс, вам нужно будет создать внутренний класс WatchFaceEngine в исходных файлах этой статьи, который расширяет Engine . Это механизм циферблата, который обрабатывает системные события, такие как выключение экрана или переход в режим окружающей среды.

1
private class WatchFaceEngine extends Engine

Когда ваш код-заглушка для WatchFaceEngine находится внутри, вернитесь к внешнему классу и переопределите метод onCreateEngine для возврата вашего нового внутреннего класса. Это свяжет ваш сервис циферблата с кодом, который будет управлять дисплеем.

1
2
3
4
@Override
public Engine onCreateEngine() {
    return new WatchFaceEngine();
}

После того, как вы собрали сервис «голые кости», вы можете перейти к общим служебным задачам обновления ваших файлов AndroidManifest, чтобы ваш сервис был доступен для Android Wear. Имейте в виду, что ваш текущий код еще ничего не сделает. Мы вернемся к этому классу и доработаем двигатель после некоторой уборки проекта.

Откройте файл AndroidManifest.xml в модуле износа . Рядом с вершиной вы уже должны увидеть строку, которая говорит:

1
<uses-feature android:name=»android.hardware.type.watch» />

Ниже этой строки нам нужно добавить два необходимых разрешения для циферблата. Эти требования:

1
2
<uses-permission android:name=»com.google.android.permission.PROVIDE_BACKGROUND» />
<uses-permission android:name=»android.permission.WAKE_LOCK» />

Как только ваши разрешения будут установлены, вам нужно будет добавить узел для вашей службы в узел application с разрешением на BIND_WALLPAPER , несколько наборов meta-data содержащих эталонные изображения вашего циферблата для экрана выбора (в этом примере мы просто используя значок запуска) и intent-filter чтобы система знала, что ваш сервис предназначен для отображения циферблата.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
<service android:name=».service.CustomWatchFaceService»
    android:label=»Tuts+ Wear Watch Face»
    android:permission=»android.permission.BIND_WALLPAPER»>
 
    <meta-data
        android:name=»android.service.wallpaper»
        android:resource=»@xml/watch_face» />
    <meta-data
        android:name=»com.google.android.wearable.watchface.preview»
        android:resource=»@mipmap/ic_launcher» />
    <meta-data
        android:name=»com.google.android.wearable.watchface.preview_circular»
        android:resource=»@mipmap/ic_launcher» />
    <intent-filter>
        <action android:name=»android.service.wallpaper.WallpaperService» />
        <category android:name=»com.google.android.wearable.watchface.category.WATCH_FACE» />
    </intent-filter>
 
</service>

После завершения манифеста износа вам нужно будет открыть файл AndroidManifest.xml в мобильном модуле и добавить два разрешения, которые мы использовали в модуле износа для PROVIDE_BACKGROUND и WAKE_LOCK , поскольку Android Wear требует, чтобы и модуль износа, и мобильные модули запрашивали те же разрешения для износа APK для установки на часы пользователя. После заполнения обоих файлов манифеста вы можете вернуться к CustomWatchFaceService.java, чтобы начать реализацию движка.

Объект Engine связанный с вашим сервисом, — это то, что движет вашим циферблатом. Он обрабатывает таймеры, отображает ваш пользовательский интерфейс, входит и выходит из окружающего режима и получает информацию о дисплее физических часов. Короче говоря, именно здесь происходит волшебство.

Первое, что вы захотите сделать, это реализовать набор переменных-членов в вашем движке, чтобы отслеживать состояния устройства, интервалы таймера и атрибуты для вашего дисплея.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
//Member variables
private Typeface WATCH_TEXT_TYPEFACE = Typeface.create( Typeface.SERIF, Typeface.NORMAL );
 
private static final int MSG_UPDATE_TIME_ID = 42;
private long mUpdateRateMs = 1000;
 
private Time mDisplayTime;
 
private Paint mBackgroundColorPaint;
private Paint mTextColorPaint;
 
private boolean mHasTimeZoneReceiverBeenRegistered = false;
private boolean mIsInMuteMode;
private boolean mIsLowBitAmbient;
 
private float mXOffset;
private float mYOffset;
 
private int mBackgroundColor = Color.parseColor( «black» );
private int mTextColor = Color.parseColor( «red» );

Как вы можете видеть, мы определяем TypeFace который мы будем использовать для текста наших цифровых часов, а также цвет фона и цвета текста на циферблате. Объект Time используется для, как вы уже догадались, отслеживания текущего времени устройства. mUpdateRateMs используется для управления таймером, который нам потребуется реализовать для обновления нашего циферблата каждую секунду (отсюда значение 1000 миллисекунд для mUpdateRateMs ), поскольку стандартный WatchFaceService отслеживает время только с шагом в одну минуту. mXOffset и mYOffset определяются после того, как движок узнает физическую форму часов, так что наш циферблат можно нарисовать, не слишком близко к верхней или левой части экрана, или отрезать закругленным углом. Три логических значения используются для отслеживания различных состояний устройства и приложения.

Следующий объект, который вам нужно будет определить, — это широковещательный приемник, который обрабатывает ситуацию, когда пользователь может путешествовать и меняет часовые пояса. Этот ресивер просто очищает сохраненный часовой пояс и сбрасывает время отображения.

1
2
3
4
5
6
7
final BroadcastReceiver mTimeZoneBroadcastReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        mDisplayTime.clear( intent.getStringExtra( «time-zone» ) );
        mDisplayTime.setToNow();
    }
};

После того, как ваш приемник определен, последний объект, который вам нужно будет создать в верхней части вашего движка, — это Handler который заботится об обновлении циферблата каждую секунду. Это необходимо из-за ограничений WatchFaceService обсужденных выше. Если ваш собственный циферблат нужно обновлять каждую минуту, вы можете смело игнорировать этот раздел.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
private final Handler mTimeHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        switch( msg.what ) {
            case MSG_UPDATE_TIME_ID: {
                invalidate();
                if( isVisible() && !isInAmbientMode() ) {
                    long currentTimeMillis = System.currentTimeMillis();
                    long delay = mUpdateRateMs — ( currentTimeMillis % mUpdateRateMs );
                    mTimeHandler.sendEmptyMessageDelayed( MSG_UPDATE_TIME_ID, delay );
                }
                break;
            }
        }
    }
};

Реализация Handler довольно проста. Сначала проверяется идентификатор сообщения. Если совпадает MSG_UPDATE_TIME_ID , он продолжает делать недействительным текущее представление для перерисовки. После того, как представление было признано недействительным, Handler проверяет, виден ли экран и не находится ли он в окружающем режиме. Если он виден, он отправляет повторный запрос через секунду. Причина, по которой мы повторяем действие в Handler тогда, когда циферблат виден, а не в окружающем режиме, заключается в том, что он может потреблять немного энергии, чтобы обновляться каждую секунду. Если пользователь не смотрит на экран, мы просто возвращаемся к реализации WatchFaceService которая обновляется каждую минуту.

Теперь, когда ваши переменные и объекты объявлены, пришло время инициализировать циферблат. Engine есть метод onCreate который следует использовать для создания объектов и других задач, которые могут занимать значительное количество времени и энергии. Вы также можете установить несколько флагов для WatchFaceStyle здесь, чтобы контролировать, как система взаимодействует с пользователем, когда ваш циферблат активен.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@Override
public void onCreate(SurfaceHolder holder) {
    super.onCreate(holder);
 
    setWatchFaceStyle( new WatchFaceStyle.Builder( CustomWatchFaceService.this )
                    .setBackgroundVisibility( WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE )
                    .setCardPeekMode( WatchFaceStyle.PEEK_MODE_VARIABLE )
                    .setShowSystemUiTime( false )
                    .build()
    );
 
    mDisplayTime = new Time();
 
    initBackground();
    initDisplayText();
}

Для примера приложения вы будете использовать setWatchFaceStyle чтобы задать фон ваших карточек уведомлений, чтобы кратко показать, установлен ли тип карточки как прерывистый. Вы также установите режим просмотра, чтобы карточки уведомлений занимали столько места, сколько необходимо.

Наконец, вы захотите указать системе не показывать время по умолчанию, поскольку вы будете отображать его самостоятельно. Хотя это лишь некоторые из доступных вариантов, вы можете найти еще больше информации в официальной документации для объекта WatchFaceStyle.Builder .

После того, как ваш WatchFaceStyle был установлен, вы можете инициализировать mDisplayTime как новый объект Time .

initBackground и initDisplayText выделяют два объекта Paint которые вы определили в верхней части движка. Затем цвет фона и текста будет установлен, а тексту будут заданы шрифт и размер шрифта, а также включен сглаживание.

01
02
03
04
05
06
07
08
09
10
11
12
private void initBackground() {
    mBackgroundColorPaint = new Paint();
    mBackgroundColorPaint.setColor( mBackgroundColor );
}
 
private void initDisplayText() {
    mTextColorPaint = new Paint();
    mTextColorPaint.setColor( mTextColor );
    mTextColorPaint.setTypeface( WATCH_TEXT_TYPEFACE );
    mTextColorPaint.setAntiAlias( true );
    mTextColorPaint.setTextSize( getResources().getDimension( R.dimen.text_size ) );
}

Далее необходимо реализовать различные методы из класса Engine , которые запускаются при изменении состояния устройства. Мы начнем с onVisibilityChanged метода onVisibilityChanged , который вызывается, когда пользователь скрывает или показывает циферблат.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public void onVisibilityChanged( boolean visible ) {
    super.onVisibilityChanged(visible);
 
    if( visible ) {
        if( !mHasTimeZoneReceiverBeenRegistered ) {
 
            IntentFilter filter = new IntentFilter( Intent.ACTION_TIMEZONE_CHANGED );
            CustomWatchFaceService.this.registerReceiver( mTimeZoneBroadcastReceiver, filter );
 
            mHasTimeZoneReceiverBeenRegistered = true;
        }
 
        mDisplayTime.clear( TimeZone.getDefault().getID() );
        mDisplayTime.setToNow();
    } else {
        if( mHasTimeZoneReceiverBeenRegistered ) {
            CustomWatchFaceService.this.unregisterReceiver( mTimeZoneBroadcastReceiver );
            mHasTimeZoneReceiverBeenRegistered = false;
        }
    }
 
    updateTimer();
}

Когда этот метод вызывается, он проверяет, является ли циферблат видимым или нет. Если циферблат виден, он смотрит, зарегистрирован ли BroadcastReceiver который вы определили в верхней части Engine . Если это не так, метод создает IntentFilter для действия IntentFilter и регистрирует BroadcastReceiver для его прослушивания.

Если циферблат не виден, этот метод проверяет, можно ли отменить регистрацию BroadcastReceiver . Как только BroadcastReceiver обработан, updateTimer чтобы вызвать аннулирование циферблата и перерисовать циферблат. updateTimer останавливает любые ожидающие действия Handler и проверяет, следует ли отправлять другое.

1
2
3
4
5
6
private void updateTimer() {
    mTimeHandler.removeMessages( MSG_UPDATE_TIME_ID );
    if( isVisible() && !isInAmbientMode() ) {
        mTimeHandler.sendEmptyMessage( MSG_UPDATE_TIME_ID );
    }
}

Когда ваш сервис связан с Android Wear, onApplyWindowInsets . Это используется, чтобы определить, является ли устройство, на котором работает ваш циферблат, округленным или квадратным. Это позволяет изменить циферблат часов в соответствии с оборудованием.

Когда этот метод вызывается в примере приложения, он просто проверяет форму устройства и изменяет смещение x, используемое для рисования циферблата, чтобы убедиться, что ваш циферблат виден на устройстве.

01
02
03
04
05
06
07
08
09
10
11
12
@Override
public void onApplyWindowInsets(WindowInsets insets) {
    super.onApplyWindowInsets(insets);
 
    mYOffset = getResources().getDimension( R.dimen.y_offset );
 
    if( insets.isRound() ) {
        mXOffset = getResources().getDimension( R.dimen.x_offset_round );
    } else {
        mXOffset = getResources().getDimension( R.dimen.x_offset_square );
    }
}

Следующий метод, который вам нужно будет переопределить, это onPropertiesChanged . Этот метод вызывается, когда определяются аппаратные свойства устройства износа, например, если устройство поддерживает защиту от выгорания или режим окружения с низкой разрядностью.

В этом методе вы проверяете, применяются ли эти атрибуты к устройству, на котором работает ваш циферблат, и сохраняете их в переменной-члене, определенной в верхней части вашего Engine .

1
2
3
4
5
6
7
8
@Override
public void onPropertiesChanged( Bundle properties ) {
    super.onPropertiesChanged( properties );
 
    if( properties.getBoolean( PROPERTY_BURN_IN_PROTECTION, false ) ) {
        mIsLowBitAmbient = properties.getBoolean( PROPERTY_LOW_BIT_AMBIENT, false );
    }
}

После обработки начальных состояний устройства вы захотите реализовать onAmbientModeChanged и onInterruptionFilterChanged . Как следует из названия, onAmbientModeChanged вызывается, когда устройство входит или выходит из окружающего режима.

Если устройство находится в режиме окружающей среды, вам нужно изменить цвет вашего циферблата на черно-белый, чтобы помнить о батарее пользователя. Когда устройство возвращается из окружающего режима, вы можете сбросить цвета циферблата. Вы также должны помнить о сглаживании для устройств, которые запрашивают поддержку окружения с низким битом. После того, как все переменные флага установлены, вы можете заставить циферблат аннулировать и перерисовать, а затем проверить, должен ли запускаться секундный таймер.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@Override
public void onAmbientModeChanged(boolean inAmbientMode) {
    super.onAmbientModeChanged(inAmbientMode);
 
    if( inAmbientMode ) {
        mTextColorPaint.setColor( Color.parseColor( «white» ) );
    } else {
        mTextColorPaint.setColor( Color.parseColor( «red» ) );
    }
 
    if( mIsLowBitAmbient ) {
        mTextColorPaint.setAntiAlias( !inAmbientMode );
    }
 
    invalidate();
    updateTimer();
}

onInterruptionFilterChanged вызывается, когда пользователь вручную изменяет настройки прерывания на своем носителе. Когда это произойдет, вам нужно будет проверить, не отключен ли звук на устройстве, а затем соответствующим образом изменить пользовательский интерфейс. В этой ситуации вы измените прозрачность циферблата, установите параметр « Handler на обновление только каждую минуту, если устройство отключено, а затем перерисоваете циферблат.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
@Override
public void onInterruptionFilterChanged(int interruptionFilter) {
    super.onInterruptionFilterChanged(interruptionFilter);
 
    boolean isDeviceMuted = ( interruptionFilter == android.support.wearable.watchface.WatchFaceService.INTERRUPTION_FILTER_NONE );
    if( isDeviceMuted ) {
        mUpdateRateMs = TimeUnit.MINUTES.toMillis( 1 );
    } else {
        mUpdateRateMs = DEFAULT_UPDATE_RATE_MS;
    }
 
    if( mIsInMuteMode != isDeviceMuted ) {
        mIsInMuteMode = isDeviceMuted;
        int alpha = ( isDeviceMuted ) ?
        mTextColorPaint.setAlpha( alpha );
        invalidate();
        updateTimer();
    }
}

Когда ваше устройство находится в окружающем режиме, таймер Handler будет отключен. Ваш циферблат по-прежнему может обновляться с текущим временем каждую минуту с помощью встроенного метода onTimeTick для аннулирования Canvas .

1
2
3
4
5
6
@Override
public void onTimeTick() {
    super.onTimeTick();
 
    invalidate();
}

После того, как все ваши непредвиденные обстоятельства пройдены, пришло время, наконец, вытянуть циферблат. CanvasWatchFaceService использует стандартный объект Canvas , поэтому вам нужно добавить onDraw к вашему onDraw и вручную нарисовать циферблат.

В этом уроке мы просто нарисуем текстовое представление времени, хотя вы можете изменить свой onDraw чтобы легко поддерживать аналоговый циферблат. В этом методе вы захотите убедиться, что отображаете правильное время, обновив объект Time а затем вы можете начать применять циферблат часов.

1
2
3
4
5
6
7
8
9
@Override
public void onDraw(Canvas canvas, Rect bounds) {
    super.onDraw(canvas, bounds);
 
    mDisplayTime.setToNow();
 
    drawBackground( canvas, bounds );
    drawTimeText( canvas );
}

drawBackground применяет сплошной цвет к фону устройства Wear.

1
2
3
private void drawBackground( Canvas canvas, Rect bounds ) {
    canvas.drawRect( 0, 0, bounds.width(), bounds.height(), mBackgroundColorPaint );
}

drawTimeText создает текст времени, который будет отображаться с помощью нескольких вспомогательных методов, а затем применяет его к холсту в точках смещения x и y, которые вы определили в onApplyWindowInsets .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
private void drawTimeText( Canvas canvas ) {
    String timeText = getHourString() + «:» + String.format( «%02d», mDisplayTime.minute );
    if( isInAmbientMode() || mIsInMuteMode ) {
        timeText += ( mDisplayTime.hour < 12 ) ?
    } else {
        timeText += String.format( «:%02d», mDisplayTime.second);
    }
    canvas.drawText( timeText, mXOffset, mYOffset, mTextColorPaint );
}
 
private String getHourString() {
    if( mDisplayTime.hour % 12 == 0 )
        return «12»;
    else if( mDisplayTime.hour <= 12 )
        return String.valueOf( mDisplayTime.hour );
    else
        return String.valueOf( mDisplayTime.hour — 12 );
}

После того, как вы реализовали методы рисования циферблата, у вас должны быть все базовые знания, необходимые для того, чтобы выйти и создать свои собственные циферблаты. Приятная особенность циферблатов Android Wear в том, что это просто царапает поверхность того, что возможно.

Вы можете добавить сопутствующие действия по настройке на часы или на телефон, заменить циферблат на основе Canvas реализацией OpenGL или получить собственный класс из WatchFaceService для удовлетворения ваших потребностей.

Добавьте к этому, что вы можете получить доступ к другим API или информации с телефона пользователя, и возможности кажутся бесконечными. Проявите творческий подход к своим циферблатам и наслаждайтесь.