sexta-feira, 22 de outubro de 2010

Android Live Wallpaper

Olá povo,

Engraçado, esse post aqui eu jurava que já tinha escrito, mas quando pesquisei aqui no blog, vi que tinha ficado só na imaginação :) Então vamos lá. Hoje quero falar um pouquinho de um recurso muito bacana que foi introduzido no Android 2.1: os papéis de parede animados ou simplesmente live wallpapers.

Os live wallpapers são diferentes dos papéis de parede comuns pois são dinâmicos e permitem interação com o usuário. Com isso, ao invés de termos uma imagem parada, eles podem exibir animações e até mesmo mudar de comportamento de acordo com o hora do dia, posição do telefone (baseado no acelerômetro), entre outros.

Temos alguns exemplos bem bacanas que já vêm no próprio Android. O live wallpaper "Água" mostra algumas folhas secas descendo na tela como se estivesse sobre a água, e quando o usuário toca na área de trabalho do telefone faz-se uma onda como se o usuário estivesse tocando sobre água.
Outro exemplo interessante é o "Grama". Ele mostra um gramado balançando com o vento. Com o céu ao fundo, ele muda de cor de acordo com a hora do dia.



Vou apresentar um exemplo simples de um papél de parede animado que mostrará o texto passando da tela e a cor da tela mudando aleatoriamente quando o usuário tocar na tela.
A estrutura do projeto ficará como abaixo:


Vamos começar de baixo pra cima. Vejamos com está o AndroidManifest.xml.


<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="ngvl.android.wallpaper"
android:versionCode="1"
android:versionName="1.0">

<application
android:icon="@drawable/icon"
android:label="@string/app_name">

<service
android:name=".MeuPapelDeParede"
android:permission="android.permission.BIND_WALLPAPER">

<intent-filter>
<action
android:name="android.service.wallpaper.WallpaperService" />
</intent-filter>

<meta-data
android:name="android.service.wallpaper"
android:resource="@xml/meupapeldeparede" />
</service>

<activity
android:name=".MeuPapelDeParedeConfig"
android:exported="true">
</activity>
</application>

<uses-sdk android:minSdkVersion="7" />
<uses-feature
android:name="android.software.live_wallpaper" />
</manifest>

Notem que temos um serviço e uma atividade declaradas no manifest. O serviço é o papel de parede em si. Ele exige a permissão BIND_WALLPAPER para poder defini-lo como wallpaper do telefone. Além disso ele tem a uma ação (action) específica para obedecer à chamadas do S.O. para o pel de parede animado. A tag meta-data referencia o arquivo de configuração do wallpaper, que está em res/xml/meupapeldeparede.xml. Esse arquivo vai definir a tela de configuração do papel de parede que é a atividade declarada no nosso manifest.

<wallpaper
xmlns:android="http://schemas.android.com/apk/res/android"
android:thumbnail="@drawable/icon"
android:settingsActivity="ngvl.android.wallpaper.MeuPapelDeParedeConfig"
/>

Essa tela de configuração permitirá ao usuário configurar o texto que aparecerá no papel de parede e armazenará automaticamente em uma SharedPreference.

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

<EditTextPreference
android:key="texto_do_papel_de_parede"
android:title="Texto do Papel de Parede" />
</PreferenceScreen>



public class MeuPapelDeParedeConfig
extends PreferenceActivity {

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

getPreferenceManager().setSharedPreferencesName(
MeuPapelDeParede.SHARED_PREFS_NAME);

addPreferencesFromResource(R.xml.tela_de_config);
}
}

Agora vamos ver a classe que faz o trabalho legal :)

public class MeuPapelDeParede
extends WallpaperService {

// Contante da SharedPreference do WallPaper
public static final String SHARED_PREFS_NAME =
"meupapeldeparede_config";

public static final String PREF_KEY =
"texto_do_papel_de_parede";

// Handler que ficará recebendo requisições
// para redesenhar a tela
private final Handler mHandler = new Handler();

// X e Y do texto e a cor de background
private int x = 200, y = 100, bg = 0xF00;

// texto do papel de parede
private String texto;

// Retorna a Engine que pinta a tela e
// trata eventos de toque
// Utilizado pelo framework
public Engine onCreateEngine() {
return new MinhaEngine();
}

// Engine para desenhar o papel de parede
private class MinhaEngine extends Engine implements
SharedPreferences.OnSharedPreferenceChangeListener{

// Pincel pra desenhar na tela
private final Paint mPaint;

// Flag que indica se wallpaper está visível
private boolean mVisible;

public MinhaEngine() {
// Configura o pincel para a cor branca
// e coloca anti-alias pra tirar o "serrilhado"
mPaint = new Paint();
mPaint.setColor(0xffffffff);
mPaint.setAntiAlias(true);

// Se registra para ser notificado
// caso o usuário mudar o texto
SharedPreferences prefs =
MeuPapelDeParede.this.getSharedPreferences(
SHARED_PREFS_NAME, 0);

prefs.registerOnSharedPreferenceChangeListener(
this);

onSharedPreferenceChanged(prefs, null);
}

public void onCreate(SurfaceHolder surfaceHolder){
super.onCreate(surfaceHolder);
// Habilita eventos de touch
setTouchEventsEnabled(true);
}

public void onDestroy() {
super.onDestroy();
// Suspende qualquer "pintura" pendente
mHandler.removeCallbacks(threadPintura);
}

public void onVisibilityChanged(boolean visible){
// Se o display estiver apagado, não fica
// pintando a tela evita desperdiçar
// processamento
mVisible = visible;
if (visible) {
pintaPapelDeParede();
} else {
mHandler.removeCallbacks(threadPintura);
}
}

public void onSurfaceDestroyed(SurfaceHolder sh){
super.onSurfaceDestroyed(holder);
// Ao destruir o "canvas" suspende todas
// as threads pendentes
mVisible = false;
mHandler.removeCallbacks(threadPintura);
}

public void onTouchEvent(MotionEvent event) {
// Captura o X e o Y de onde o usuário
// tocou na tela.
// Gera uma cor de BG aleatóra.
if (event.getAction() ==
MotionEvent.ACTION_DOWN) {

x = (int) event.getX();
y = (int) event.getY();
bg = new Random().nextInt(0xffffff);
}
super.onTouchEvent(event);
}

// Thread que solicita a pintura da tela
private final Runnable threadPintura =
new Runnable() {
public void run() {
pintaPapelDeParede();
}
};

// Método que pinta cada frame da
// animação do papel de parede
void pintaPapelDeParede() {
final SurfaceHolder holder =
getSurfaceHolder();

Canvas c = null;
try {
c = holder.lockCanvas();
if (c != null) {
desentaTexto(c);
}
} finally {
if (c != null)
holder.unlockCanvasAndPost(c);
}

// Depois de efetuar a pintura, agenda o handler
// pra pintar o próximo frame.
// Dessa forma, teremos 40 frames por segundo
mHandler.removeCallbacks(threadPintura);
if (mVisible) {
mHandler.postDelayed(threadPintura, 1000/25);
}
}

// Desenha o texto na tela
private void desentaTexto(Canvas c) {
c.save();

c.drawColor(0xff000000 | bg);

float textWidth = 0;
float[] widths = new float[texto.length()];

mPaint.setTextSize(20);
mPaint.getTextWidths(texto, widths);

for (int i = 0; i < widths.length; i++) {
textWidth += widths[i];
}

c.drawText(texto, x, y, mPaint);
x -= 5;
if (x <= -textWidth) {
x = c.getWidth();
}

c.restore();
}

// Se o usuário mudar o texto a ser exibido,
// atualiza o texto do wallpaper
public void onSharedPreferenceChanged(
SharedPreferences prefs, String key) {
texto = prefs.getString(PREF_KEY, "Texto");

}
}
}


Pronto! Agora pra testar, basta ir na tela principal pressionar Menu > Wallpaper > Live wallpapers. Nosso papel de barede deve aparecer na lista. Abaixo temos a figuras do nosso wallpaper em execução.



4br4ç05,
nglauber

3 comentários:

Higgor Leimig disse...

Muito bom, já deu uma luz incrivel de como fazer o meu. Parabéns

Anônimo disse...

Olá Glauber, gostei muito do seu tutorial, muito bom, mas tenho uma dúvida: Você poderia me explicar o metodo desentaTexto, principalmente as linhas:

for (int i = 0; i < widths.length; i++) {
textWidth += widths[i];
}

x -= 5;
if (x <= -textWidth) {
x = c.getWidth();

Nelson Glauber de Vasconcelos Leal disse...

Oi Anônimo,

A instrução FOR é para pegar o tamanho do texto. E o IF é para checar se o texto já saiu todo da tela. Em caso positivo, x recebe a largura da tela para que o texto comece a ser desenhado da borda direita da tela.

4br4ç05,
nglauber