Купил я значит себе часы на android wear, и думал что они будут полезные мне. На деле совершенно безсмысленная хрень, на деле — фитнес трекер с функцией приема звонков и уведомлений. На них конечно можно еще играть в игры которые адаптированы под маленький экранчик, но это выглядит дико, когда в метро или на улице идет дядька и втыкает в часы на руке. Дико в общем.
Посмотрев на экраны часов которые предоставленны в google play, я подумал что лучше я буду использовать стандартные циферблаты которые предустановленны в часах. Но потом меня осенило, я же типа программист, я могу сам запедалить себе циферблат который мне будет подходить, ну и в общем я решил попробовать и сделать себе какую-то красоту на часы. Так как у меня вкуса нет вообще, и дизайнер из меня никакой экран я сделал очень простой, ну и тут я вам покажу основные функции. У нас часы будут уметь показывать время, дату и уровень батареи.
Рисовать мы будем с помощью canvas'a, в CanvasWatchFaceService. Для этого вам нужно создать проект специальный для часов. Вам создастся проект в котором у вас будет пример с аналоговыми часами. Стираем все по самое
public class WatchFaceService extends CanvasWatchFaceService { }
Теперь у нас есть пустой класс, мы разобьем логику часов на две части, в первой у нас будет вся логика относящаяся к работе часов в фоне, и во второй у нас будет вся отрисовка с помощью canvas'a на экране. Так и понятней и логичней как по мне.
Сначало создадим dimens и colors файлы, они нам понадобятся для установки размеров текста на экране, и их цветов. А дальше приведу код который реализует нам наш циферблат.
dimens.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="time_size">46dp</dimen>
<dimen name="date_size">20dp</dimen>
</resources>
colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primaryColorBlue">#3498db</color>
<color name="primaryBackgroundColor">#1a1d1d</color>
</resources>
Изначально хотел сделать циферблат какого-то темного серого цвета, но потом в итоге переиграл и решил сделать черного, так что на параметер primaryBackgroundColor можете не обращать внимания, но если захотите поменять на свой цвет то можете просто изменить его тут и потом в коде :)
Теперь наш AndroidManifest файл, он немного отличается от тех что мы обычно имеем когда делаем обычное приложение.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.project.androidwearexample">
<uses-feature android:name="android.hardware.type.watch" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@android:style/Theme.DeviceDefault">
<service
android:name=".WatchFaceService"
android:label="@string/my_analog_name"
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="@drawable/icon" />
<meta-data
android:name="com.google.android.wearable.watchface.preview_circular"
android:resource="@drawable/icon" />
<intent-filter>
<action android:name="android.service.wallpaper.WallpaperService" />
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
</intent-filter>
</service>
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
</application>
</manifest>
Тут у нас есть один uses-feature который указывает что это приложение будет для часов, так же у нас есть еще uses-permission который позволяет разблокировать экран часов при повороте. Тут же мы указываем в параметре service, что мы запускаем WatchFaceService сразу после запуска приложения на часах, устанавливаем что оно работает как обои для андроида, устанавливаем превью, иконку и указываем что можем принимать уведомления на экране.
Теперь нам нужно написать наш WatchFaceService который будет собственно нашим циферблатом.
WatchFaceService.java
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.BatteryManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.wearable.watchface.CanvasWatchFaceService;
import android.support.wearable.watchface.WatchFaceStyle;
import android.util.Log;
import android.view.SurfaceHolder;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.wearable.DataApi;
import com.google.android.gms.wearable.DataEventBuffer;
import com.google.android.gms.wearable.DataItemBuffer;
import com.google.android.gms.wearable.Wearable;
import java.util.concurrent.TimeUnit;
public class WatchFaceService extends CanvasWatchFaceService {
private static final long TICK_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(1);
@Override
public Engine onCreateEngine() {
return new SimpleEngine();
}
private class SimpleEngine extends CanvasWatchFaceService.Engine implements
GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener {
private static final String ACTION_TIME_ZONE = "time-zone";
private static final String TAG = "SimpleEngine";
private WatchFaceView watchFace;
private Handler timeTick;
private GoogleApiClient googleApiClient;
@Override
public void onCreate(SurfaceHolder holder) {
super.onCreate(holder);
setWatchFaceStyle(new WatchFaceStyle.Builder(WatchFaceService.this)
.setCardPeekMode(WatchFaceStyle.PEEK_MODE_SHORT)
.setAmbientPeekMode(WatchFaceStyle.AMBIENT_PEEK_MODE_HIDDEN)
.setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
.setShowSystemUiTime(false)
.build());
timeTick = new Handler(Looper.myLooper());
startTimerIfNecessary();
watchFace = WatchFaceView.newInstance(WatchFaceService.this);
googleApiClient = new GoogleApiClient.Builder(WatchFaceService.this)
.addApi(Wearable.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
}
private void startTimerIfNecessary() {
timeTick.removeCallbacks(timeRunnable);
if (isVisible() && !isInAmbientMode()) {
timeTick.post(timeRunnable);
}
}
private final Runnable timeRunnable = new Runnable() {
@Override
public void run() {
onSecondTick();
if (isVisible() && !isInAmbientMode()) {
timeTick.postDelayed(this, TICK_PERIOD_MILLIS);
}
}
};
private void onSecondTick() {
invalidateIfNecessary();
}
private void invalidateIfNecessary() {
if (isVisible() && !isInAmbientMode()) {
invalidate();
}
}
@Override
public void onVisibilityChanged(boolean visible) {
super.onVisibilityChanged(visible);
if (visible) {
registerTimeZoneReceiver();
googleApiClient.connect();
} else {
unregisterTimeZoneReceiver();
releaseGoogleApiClient();
}
startTimerIfNecessary();
}
private void releaseGoogleApiClient() {
if (googleApiClient != null && googleApiClient.isConnected()) {
Wearable.DataApi.removeListener(googleApiClient, onDataChangedListener);
googleApiClient.disconnect();
}
}
private void unregisterTimeZoneReceiver() {
unregisterReceiver(timeZoneChangedReceiver);
unregisterReceiver(mBatInfoReceiver);
}
private void registerTimeZoneReceiver() {
IntentFilter timeZoneFilter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
registerReceiver(timeZoneChangedReceiver, timeZoneFilter);
IntentFilter batteryFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
registerReceiver(mBatInfoReceiver, batteryFilter);
}
private BroadcastReceiver timeZoneChangedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
watchFace.updateTimeZoneWith(intent.getStringExtra(ACTION_TIME_ZONE));
}
}
};
private BroadcastReceiver mBatInfoReceiver = new BroadcastReceiver(){
@Override
public void onReceive(Context arg0, Intent intent) {
watchFace.updateBattery(String.valueOf(intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0) + "%"));
}
};
@Override
public void onDraw(Canvas canvas, Rect bounds) {
super.onDraw(canvas, bounds);
watchFace.draw(canvas, bounds);
}
@Override
public void onTimeTick() {
super.onTimeTick();
invalidate();
}
@Override
public void onAmbientModeChanged(boolean inAmbientMode) {
super.onAmbientModeChanged(inAmbientMode);
}
@Override
public void onConnected(Bundle bundle) {
Log.d(TAG, "connected GoogleAPI");
Wearable.DataApi.addListener(googleApiClient, onDataChangedListener);
Wearable.DataApi.getDataItems(googleApiClient).setResultCallback(onConnectedResultCallback);
}
private final DataApi.DataListener onDataChangedListener = new DataApi.DataListener() {
@Override
public void onDataChanged(DataEventBuffer dataEvents) {
dataEvents.release();
invalidateIfNecessary();
}
};
private final ResultCallback<DataItemBuffer> onConnectedResultCallback = new ResultCallback<DataItemBuffer>() {
@Override
public void onResult(DataItemBuffer dataItems) {
dataItems.release();
invalidateIfNecessary();
}
};
@Override
public void onConnectionSuspended(int i) {
Log.e(TAG, "suspended GoogleAPI");
}
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
Log.e(TAG, "connectionFailed GoogleAPI");
}
@Override
public void onDestroy() {
timeTick.removeCallbacks(timeRunnable);
releaseGoogleApiClient();
super.onDestroy();
}
}
}
Если в кратце, то тут мы имеем стандартный код для запуска работы наших часов, что бы у нас они умели тикать, считывать время, дату, уровень батареи, можем тут же назначить получение погоды для определенного города и получение количества шагов из приложения которое у вас идет по дефолту в часах. В нашем случае мы запускаем два бродкаст ресивера, один у нас получает дату и время, второй уровень батареи и потом эти данные в onReceive мы передаем во второй класс, который мы напишем далее, в нем как я ранее говорил мы будем рисовать часы.
Этот класс мы вызываем в onCreate, можете посмотреть как оно там реализовано.
WatchFaceView.java
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.text.format.Time;
public class WatchFaceView {
private static final String TIME_FORMAT_WITHOUT_SECONDS = "%02d:%02d";
private static final String TIME_FORMAT_WITH_SECONDS = TIME_FORMAT_WITHOUT_SECONDS + ":%02d";
private static final String DATE_FORMAT = "%02d.%02d.%d";
private final Paint timePaint;
private final Paint datePaint;
private final Paint batteryPaint;
private final Paint backgroundPaint;
private final Paint secondStickPaint;
private final Time time;
private boolean shouldShowSeconds = true;
private String batteryText = "100%";
private int mWidth;
private int mHeight;
public static WatchFaceView newInstance(Context context) {
Paint timePaint = new Paint();
timePaint.setColor(context.getResources().getColor(R.color.primaryColorBlue));
timePaint.setTextSize(context.getResources().getDimension(R.dimen.time_size));
timePaint.setAntiAlias(true);
Paint datePaint = new Paint();
datePaint.setColor(context.getResources().getColor(R.color.primaryColorBlue));
datePaint.setTextSize(context.getResources().getDimension(R.dimen.date_size));
datePaint.setAntiAlias(true);
Paint backgroundPaint = new Paint();
backgroundPaint.setColor(context.getResources().getColor(R.color.black));
Paint batteryPaint = new Paint();
batteryPaint.setColor(context.getResources().getColor(R.color.primaryColorBlue));
batteryPaint.setTextSize(context.getResources().getDimension(R.dimen.date_size));
batteryPaint.setAntiAlias(true);
Paint secondStickPaint = new Paint();
secondStickPaint.setColor(context.getResources().getColor(R.color.primaryColorBlue));
secondStickPaint.setStrokeWidth(3);
secondStickPaint.setAntiAlias(true);
return new WatchFaceView(timePaint, datePaint, batteryPaint, secondStickPaint, backgroundPaint, new Time());
}
WatchFaceView(Paint timePaint, Paint datePaint, Paint batteryPaint, Paint secondStickPaint, Paint backgroundPaint, Time time) {
this.timePaint = timePaint;
this.datePaint = datePaint;
this.backgroundPaint = backgroundPaint;
this.batteryPaint = batteryPaint;
this.secondStickPaint = secondStickPaint;
this.time = time;
}
public void draw(Canvas canvas, Rect bounds) {
mWidth = canvas.getWidth();
mHeight = canvas.getHeight();
float mCenterX = mWidth / 2f;
float mCenterY = mHeight / 2f;
time.setToNow();
canvas.drawRect(0, 0, bounds.width(), bounds.height(), backgroundPaint);
String timeText = String.format(shouldShowSeconds ? TIME_FORMAT_WITH_SECONDS : TIME_FORMAT_WITHOUT_SECONDS, time.hour, time.minute, time.second);
float timeXOffset = computeXOffset(timeText, timePaint, bounds);
float timeYOffset = bounds.centerY();
canvas.drawText(timeText, timeXOffset, timeYOffset, timePaint);
String dateText = String.format(DATE_FORMAT, time.monthDay, (time.month + 1), time.year);
float dateXOffset = computeXOffset(dateText, datePaint, bounds);
float dateYOffset = computeYOffset(dateText, datePaint);
canvas.drawText(dateText, dateXOffset, timeYOffset + dateYOffset, datePaint);
float batteryXOffset = computeXOffset(batteryText, batteryPaint, bounds);
float batteryYOffset = computeYOffset(batteryText, batteryPaint);
canvas.drawText(batteryText, batteryXOffset, timeYOffset + dateYOffset + batteryYOffset, batteryPaint);
float mSecondHandLength = mCenterX;
float secondsRotation = time.second * 6f;
canvas.rotate(secondsRotation, mCenterX, mCenterY);
canvas.drawLine(mCenterX, mCenterY - 120, mCenterX, mCenterY - mSecondHandLength, secondStickPaint);
}
private float computeXOffset(String text, Paint paint, Rect watchBounds) {
float centerX = watchBounds.exactCenterX();
float timeLength = paint.measureText(text);
return centerX - (timeLength / 2.0f);
}
private float computeYOffset(String dateText, Paint datePaint) {
Rect textBounds = new Rect();
datePaint.getTextBounds(dateText, 0, dateText.length(), textBounds);
return textBounds.height() + 10.0f;
}
public void updateTimeZoneWith(String timeZone) {
time.clear(timeZone);
time.setToNow();
}
public void updateBattery(String batteryText) {
this.batteryText = batteryText;
}
}
Вот наш класс который рисует наш циферблат. В конструкторе мы задаем все нужные цвета для текста, толщину текста и т.д. Дальше у нас следует метод draw. в нем мы рисуем, с нужными отступами, нужный текст по центру экрана, и тут же мы рисуем секундную стрелку которая тикает по кругу экрана. А дальше у нас просто идет метод вычисления центра экрана по X и Y и сеттеры для задавания времени, даты и уровня батареи. Вот и весь код который нам нужен.
Для компиляции нам нужно в настройках компилятора убрать в Launch Options — Default activity, и поменять его на nothing. Тогда вы сможете скомпилировать проект под часы, иначе просто студия не сможет запустить компиляцию, так как у нас не будет активити для стартового запуска.
Исходники:
Комментариев нет:
Отправить комментарий