Одной из особенностей, которая делает Android таким особенным, является возможность настраивать каждый аспект пользовательского опыта. Когда Android Wear впервые был представлен на Google I / O 2014, многие разработчики и пользователи обнаружили, что это не совсем верно для умных часов, поскольку официальный API для создания циферблатов явно отсутствовал. Учитывая, что возможность создания пользовательских циферблатов была одной из ключевых задач пользователей, неудивительно, что разработчики обнаружили способ создания собственных циферблатов с недокументированным хаком в Android Wear.
К счастью, Google быстро дал всем понять, что официальный API уже в пути, и в декабре 2014 года этот API был наконец выпущен сообществу разработчиков. В этой статье вы узнаете об официальном API Watch Faces для Android Wear и создадите простой цифровой циферблат, который вы сможете расширить для своих собственных нужд. Реализация циферблатов может быть немного многословной, но вы можете найти пример приложения для этой статьи на GitHub .
1. Настройка вашей IDE
Первое, что вам нужно сделать, чтобы создать свой собственный циферблат, — это настроить свой проект в Android Studio. При создании проекта выберите « Телефон и планшет» с минимальным SDK API 18, так как Android 4.3 — это самая низкая версия операционной системы для поддержки прилагаемых приложений Android Wear. Вам также нужно будет установить флажок « Износ» с выбранным минимальным SDK API 21. Вы можете увидеть пример того, как должен выглядеть экран ваших Target Android Devices .
Когда вы дойдете до двух экранов « Добавить активность» , выберите « Добавить без активности» для обоих экранов.
После того, как вы нажмете кнопку « Готово» , среда вашего проекта должна создать и иметь модуль для мобильных устройств и еще один для износа .
2. Создание службы Watch Watch
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. Имейте в виду, что ваш текущий код еще ничего не сделает. Мы вернемся к этому классу и доработаем двигатель после некоторой уборки проекта.
3. Обновление файлов AndroidManifest
Откройте файл 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, чтобы начать реализацию движка.
4. Запустите свой двигатель
Объект Engine
связанный с вашим сервисом, — это то, что движет вашим циферблатом. Он обрабатывает таймеры, отображает ваш пользовательский интерфейс, входит и выходит из окружающего режима и получает информацию о дисплее физических часов. Короче говоря, именно здесь происходит волшебство.
Шаг 1: Определение необходимых значений и переменных
Первое, что вы захотите сделать, это реализовать набор переменных-членов в вашем движке, чтобы отслеживать состояния устройства, интервалы таймера и атрибуты для вашего дисплея.
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
которая обновляется каждую минуту.
Шаг 2: Инициализация двигателя
Теперь, когда ваши переменные и объекты объявлены, пришло время инициализировать циферблат. 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 ) );
}
|
Шаг 3: Обработка состояния устройства
Далее необходимо реализовать различные методы из класса 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 );
}
}
|
Шаг 4: Сотрудничество с носимым оборудованием
Когда ваш сервис связан с 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 );
}
}
|
Шаг 5: Сохранение батареи в окружающем и приглушенном режимах
После обработки начальных состояний устройства вы захотите реализовать 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();
}
|
Шаг 6: Рисование циферблата
После того, как все ваши непредвиденные обстоятельства пройдены, пришло время, наконец, вытянуть циферблат. 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 или информации с телефона пользователя, и возможности кажутся бесконечными. Проявите творческий подход к своим циферблатам и наслаждайтесь.