Как стать автором
Обновить

Пишем живые обои с часами

Время на прочтение4 мин
Количество просмотров24K
ClockНедавно я решил разобраться, как делать живые обои для андроида, а разбираться лучше всего на хорошем примере — таком, который потом пригодится. Я всегда хотел удобные часы. Для андроида уже существует достаточное количество подобных обоев, но обычно они рисуют большие часы в каком-нибудь фиксированном углу экрана и перекрываются виджетами, которых у меня немало. Мне хотелось, чтобы часы были разбросаны по всему экрану, тогда вероятность увидеть их будет выше.

Живые обои для андроида представляют собой сервис (фоновый процесс, в терминах платформы), причем в SDK уже существует готовый класс для живых обоев, в котором нужно переопределить несколько методов, чтобы получить результат.

Для начала в манифесте описывается, что приложение будет предоставлять живые обои:
<service 
    android:name="LiveWallpaperService"
    android:enabled="true"
    android:icon="@drawable/icon"
    android:label="@string/app_name"
    android:permission="android.permission.BIND_WALLPAPER">
    <intent-filter android:priority="1" >
        <action android:name="android.service.wallpaper.WallpaperService" />
    </intent-filter>
    <meta-data 
      android:name="android.service.wallpaper" 
      android:resource="@xml/wallpaper" />
</service>


Самые важные строки здесь android:permission и intent-filter

Потом, собственно, кодирование. Мы создадим свой класс-наследник WallpaperService и переопределим его методы onCreate, onDestroy, onVisibilityChanged, onSurfaceChanged, onSurfaceCreated, onSurfaceDestroyed

Эти методы будут реагировать на системные события. Главное их назначение — включать и выключать отрисовку обоев, когда меняется их видимость, чтобы экономить батарею.
Фактически, нас интересуют только:
@Override
public void onVisibilityChanged(boolean visible) {
	if (visible) {
		painting.resumePainting();
	} else {
		// remove listeners and callbacks here
		painting.pausePainting();
	}
}

@Override
public void onDestroy() {
	super.onDestroy();
	// remove listeners and callbacks here
	painting.stopPainting();
}
@Override
public void onSurfaceChanged(SurfaceHolder holder, int format, int width, int height) {
	super.onSurfaceChanged(holder, format, width, height);
	painting.setSurfaceSize(width, height);
	Log.d("Clock", "Changed");
}

@Override
public void onSurfaceCreated(SurfaceHolder holder) {
	super.onSurfaceCreated(holder);
	painting.start();
}

@Override
public void onSurfaceDestroyed(SurfaceHolder holder) {
	super.onSurfaceDestroyed(holder);
	boolean retry = true;
	painting.stopPainting();
	while (retry) {
		try {
			painting.join();
			retry = false;
		} catch (InterruptedException e) {}
	}
}


Саму отрисовку мы реализуем в отдельном потоке, чтобы реакции на пользовательские действия были своевременными и плавными. Для этого создадим еще одни класс-наследник Thread. Объект painting в примере выше и является этим потоком:
public ClockWallpaperPainting(SurfaceHolder surfaceHolder, Context context, int fontColor, int backgroundColor) {
	this.surfaceHolder = surfaceHolder;
	this.context_ = context;

	clockFont_ = Typeface.createFromAsset(context.getAssets(), "digital_font.ttf");

	textPainter_.setTypeface(clockFont_);
	textPainter_.setTextSize(30);
	setPreferences(fontColor, backgroundColor);
		
	this.wait_ = true;
}

@Override
public void run() {
	this.run_ = true;
	Canvas c = null;
	while (run_) {
		try {
			c = this.surfaceHolder.lockCanvas(null);
			synchronized (this.surfaceHolder) {
				doDraw( c );
			}
		} finally {
			if (c != null) {
				this.surfaceHolder.unlockCanvasAndPost( c );
			}
		}
		// pause if no need to animate
		synchronized (this) {
			if (wait_) {
				try {
					wait();
				} catch (Exception e) {}
			}
		}
	}
}

public void pausePainting() {
	this.wait_ = true;
	synchronized(this) {
		this.notify();
	}
}

public void resumePainting() {
	this.wait_ = false;
	synchronized(this) {
		this.notify();
	}
}

public void stopPainting() {
	this.run_ = false;
	synchronized(this) {
		this.notify();
	}
}


Как видно, мы просто бесконечно выполняем метод doDraw(), периодически ставя его на паузу, когда рабочий стол не виден.

Сам метод выглядит примерно так:
private void doDraw(Canvas canvas) {
	Date currentTime = new Date();

	//Create text for clock
	String clockText;
	if (DateFormat.is24HourFormat(context_)) {
		if (currentTime.getSeconds()%2 == 0)
			clockText = DateFormat.format("kk:mm", currentTime).toString();
		else
			clockText = DateFormat.format("kk mm", currentTime).toString();
	} else {
		if (currentTime.getSeconds()%2 == 0)
			clockText = DateFormat.format("h:mmaa", currentTime).toString();
		else
			clockText = DateFormat.format("h mmaa", currentTime).toString();
	}
	
	int textSize = (int)textPainter_.getTextSize();
	//Erase Background
	canvas.drawRect(0, 0, width_, height_, backgroundPainter_);
	
	int stepX = (width_ - textSize) / (CLOCKS_COUNT + 1);
	int stepY = (height_ - textSize*CLOCKS_COUNT) / (CLOCKS_COUNT + 1);

	for (int i=1; i <= CLOCKS_COUNT; ++i)
		canvas.drawText(clockText,
				stepX/2 + stepX * (i-1) - absoluteOffset_,
				height_/20 + stepY * i + textSize*(i-1),
				textPainter_);
}


В сервисе мы переопределим еще один метод, чтобы обои реагировали на смену рабочих столов:
@Override
public void onOffsetsChanged(float xOffset, float yOffset, float xStep, float yStep, int xPixels, int yPixels) {
	painting.setOffset(xOffset);
	Log.d("Clock", String.format("off:%f-%f step:%f - %f px: %d - %d", xOffset, yOffset, xStep, yStep, xPixels, yPixels));
}


Как сделать отрисовку в отдельном потоке, отлично расписано в этом примере.

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

<PreferenceScreen 
	xmlns:android="http://schemas.android.com/apk/res/android"
    android:title="@string/settings_title">

    <ListPreference
	    android:key="preference_font_color"
	    android:title="@string/preference_font_color_title"
	    android:summary="@string/preference_font_color_summary"
	    android:entries="@array/color_names"
	    android:entryValues="@array/color_values" />

    <ListPreference
	    android:key="preference_background_color"
	    android:title="@string/preference_background_color_title"
	    android:summary="@string/preference_background_color_summary"
	    android:entries="@array/color_names"
	    android:entryValues="@array/color_values" />

</PreferenceScreen>


Вот так это выглядит в итоге:
image
Скачать можно тут.

Чтобы написать свои обои, начать можно отсюда
Теги:
Хабы:
+73
Комментарии37

Публикации

Истории

Работа

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн