Поиск по этому блогу

воскресенье, 5 февраля 2012 г.

Создаем живые обои для Android

Не буду разглагольствовать сильно много, а сразу буду говорить по делу.

Живые обои – это анимированные интерактивные обои для главного экрана Android, по сути они схожи с другими приложениями для Android и могут использовать тот же самый функционал API.
Чтобы создать собственные живые обои необходимо создать XML файл, содержащий описание приложения, кроме того в нем могут быть указано изображение предварительного просмотра и ссылка на активити настроек.




Любые живые обои создаются на основе сервиса WallpaperService который включает в себя ряд функций/методов которые мало чем отличаются от класса SurfaceView. Вот какие функции имеет класс WallpaperService:
  • onCreate()
  • onSurfaceCreated()
  • onVisibilityChanged()
  • onOffsetsChanged()
  • onTouchEvent()
  • onCommand()
Как и в разработке игр — в разработке обоев используется старый добрый Canvas на основе которого рисуются все возможные объекты. 

Для того что бы создать живые обои нужно обязательно в AndroidManifest'e указать наличие прав на «android.permission.BIND_WALLPAPER» и что наше приложение использует функционал android.software.live_wallpaper. Если этого не сделать, приложение сможет быть установлено на устройствах, которые не поддерживают живые обои, что не желательно. 

Создаем живые обои


Создаем проект — Eclipse — File — New — Android Project — NameProject. И убираем галочку «Create Activity», так как у нас нет активити — оно нам не нужно. Добавьте папку /res/xml и в него файл mywallpaper.xml. В этом файле будет содержаться описание ваших обоев и графика для предварительного просмотра. Этот файл ресурсов будет указан в файле AndroidManifest.xml. Если вы укажете атрибут android:thumbnail, указывающий на ресурс типа drawable, это даст возможность показать уменьшенное изображение ваших обоев.

Нам нужно внести изменения в манифест, открываем AndroidManifest, выделяем «Ctrl + A» и удаляем. Вставляем код который ниже:

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest 
        xmlns:android="http://schemas.android.com/apk/res/android"
        package="live.wallpaper"
        android:versionCode="1"
        android:versionName="1.0">
        <application 
                android:icon="@drawable/ic_launcher" 
                android:label="@string/app_name">
                <service 
                        android:name="LiveWallpaperService"
                        android:enabled="true"
                        android:icon="@drawable/ic_launcher"
                        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/mywallpaper" />
                </service>
                <activity 
                        android:label="@string/app_name" 
                        android:name=".LiveWallpaperSettings" 
                        android:theme="@android:style/Theme.Light.WallpaperSettings"
                        android:exported="true" 
                        android:configChanges="orientation"/>
        </application>
        <uses-sdk android:minSdkVersion="8" />
</manifest> 


Дальше нам нужно в созданный файл mywallpaper.xml внести данные о пред просмотре обоине которую пользователь будет устанавливать.
Вот как выглядит этот файл:

mywallpaper.xml
<?xml version="1.0" encoding="UTF-8"?>
<wallpaper 
        xmlns:android="http://schemas.android.com/apk/res/android"  
        android:thumbnail="@drawable/ic_launcher" 
        android:description="@string/description"
        android:settingsActivity="live.wallpaper.LiveWallpaperSettings"/>


Дальше берем вот эти изображения:



и закидываем в папку res/drawable, они нам понадобятся для красоты обоев :)

Создаем класс Bubble.java который будет задавать направление наших бульб.

package live.wallpaper;
import java.util.Random;
import android.graphics.Bitmap;
import android.graphics.Canvas;
public class Bubble
{
        /**Позиция по Х и У*/
        public int x;
        public int y;
        
        /**Выоста и ширина*/
        public int widht;
        public int height;
        
        /**Скорость*/
        public int speed;
        
        /**Угол полета*/
        public double angle;
        
        Bitmap bmp;
        LiveWallpaperPainting pm;
        
        /**Конструктор*/
        public Bubble(LiveWallpaperPainting pm, Bitmap bmp) {
                this.pm = pm;
                this.bmp = bmp;
                
                /**По "х" у нас будем появляться рандомно*/
                Random rnd = new Random(System.currentTimeMillis());
                this.y = 1000;
                this.x = rnd.nextInt(800);
                
                /**Скорость рандомная*/
                this.speed = rnd.nextInt(15 - 5) + 15;
                
                /**Задаем размер бульбашек*/
                this.widht = 75;
                this.height = 75;
                
                angle = getRandomAngle();
        }
        
        /**Движение объектов*/
        public void update() {
                y -= Math.abs(speed * Math.cos(angle));
                x -= Math.abs(speed * Math.sin(angle));
        }
        
        /**Задаем рандомный угол полета*/
        private int getRandomAngle() {
        Random rnd = new Random(System.currentTimeMillis());
        return rnd.nextInt(1) * 90 + 90 / 2 + rnd.nextInt(15) + 5;
    }
        
        /**Рисуем*/
        public void onDraw(Canvas c) {
                update();
                c.drawBitmap(bmp, x, y, null);
        }
        
        /**Проверка на столкновения*/
    public boolean isCollition(float x2, float y2) {
          return x2 > x && x2 < x + widht && y2 > y && y2 < y + height;
    }
}


Думаю из комментариев понятно что делаю функции и что какая переменная обозначает, если не понятно пишите в комментарии я объясню что к чему.
Далее создаем класс LiveWallpaperPainting.java который будет рисовать все что токо захотим. Он будет иметь следующий код:

package live.wallpaper;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
public class LiveWallpaperPainting extends Thread implements Runnable{

        private SurfaceHolder surfaceHolder;
        private Context context;
        
        /** Состояние потоков */
        private boolean wait;
        private boolean run;
        
        /** Выосота и ширина сцены */
        private int width;
        private int height;
        
        /**Скоре, достижение :)*/
        private int score = 0;

        /**Список бульбашек который будет бесконечным*/
        public List<Bubble> bubble = new ArrayList<Bubble>();
        
        /**Это если захотишь сделать что бы выводило спрайт после взрыва*/
        //private List<Boms> temps = new ArrayList<Boms>();

        //private Bitmap blood;
        
        /**Позиция нажатия на экран*/
        private float posX;
        private float posY;
        
        /**Объект рисовалки наших достижений*/
        private Paint mScorePaint;
        
        /**Фоновый рисунок*/
        private Bitmap bg;
        
        /**Бульбаки*/
        private Bitmap bubbles;
  
        /**Конструктор*/
        public LiveWallpaperPainting(SurfaceHolder surfaceHolder, Context context, int radius) {
                this.surfaceHolder = surfaceHolder;
                this.context = context;

                /**Запускаем поток*/
                this.wait = true;
                
                /*Рисуем всякое разное*/
                bubbles = BitmapFactory.decodeResource(context.getResources(), R.drawable.bubble);
                //blood = BitmapFactory.decodeResource(context.getResources(), R.drawable.blood1);
                bg = BitmapFactory.decodeResource(context.getResources(), R.drawable.bg);
                
             // стили для вывода счета
                mScorePaint = new Paint();
                mScorePaint.setTextSize(20);
                mScorePaint.setStrokeWidth(1);
                mScorePaint.setStyle(Style.FILL);
                mScorePaint.setColor(Color.WHITE);
        }

        /**
         * Ставим на паузу анимацию 
         */
        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();
                }
        }

        
        /**Рисуем в потоке все наши рисунки*/
        public void run() {
                this.run = true;
                Canvas c = null;
                while (run) {
                        try {
                                c = this.surfaceHolder.lockCanvas(null);
                                synchronized (this.surfaceHolder) {
                                        Thread.sleep(50);
                                        bubble.add(new Bubble(this, bubbles));
                                        doDraw©;
                                }
                        } catch (InterruptedException e) {
                                                        e.printStackTrace();
                                                } finally {
                                if (c != null) {
                                        this.surfaceHolder.unlockCanvasAndPost©;
                                }
                        }
                        // pause if no need to animate
                        synchronized (this) {
                                if (wait) {
                                        try {
                                                wait();
                                        } catch (Exception e) {}
                                }
                        }
                }
        }

        /**
         * Растягиваем картинку под размер экрана
         */
        public void setSurfaceSize(int width, int height) {
                this.width = width;
                this.height = height;
                synchronized(this) {
                        this.notify();
                        bg = Bitmap.createScaledBitmap(bg, width, height, true);
                }
        }
        
        /**
         * Обрабатываем нажатия на экран
         */
        public boolean doTouchEvent(MotionEvent event) {
                posX = event.getX();
            posY = event.getY();
            synchronized (surfaceHolder) {
                   for (int i = bubble.size() - 1; i >= 0; i--) {
                          Bubble sprite = bubble.get(i);
                          if (sprite.isCollition(posX, posY)) {
                                    bubble.remove(sprite);
                                    score++;
                                //temps.add(new Boms(temps, this, blood, posX, posY));
                                break;
                          }
                   }
            }
            return true;
      }
        
        /**
         * Рисуем на сцене в потоке
         */
        private void doDraw(Canvas canvas) {
                        canvas.drawColor(Color.WHITE);
                        canvas.drawBitmap(bg, 0,0, null);
                        /*for (int i = temps.size() - 1; i >= 0; i--) {
                    temps.get(i).onDraw(canvas);
                        }*/
                        
                        for(Bubble bub: bubble) {
                                if(bub.y >= 0 || bub.x <= 0)
                                        bub.onDraw(canvas);
                                else
                                        bubble.remove(this);
                        }
                        
                    canvas.drawText("Score: " + score, 50, 70, mScorePaint);
        }        
        }


И так если Вы заметили то код мало отличается от кода по созданию игр, единственное отличие это то что мы рисуем сразу в потоке не раскошеливаясь на создание сцены с SurfaceView. 

Я забыл упомянуть что обоинау нас будет выглядеть вот так:


Теперь я думаю понятно что будет представлять из себя этот класс. В нем мы рисуем фон, рисуем бульбы и рисуем количество хлопнутых Вами бульбашек. 

Дальше создаем класс LiveWallpaperService, это наследник класса WallpaperService про которого я говорил в начале статьи. Он имеет следующий вид:

LiveWallpaperService.java
package live.wallpaper;
import java.util.ArrayList;
import java.util.List;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.service.wallpaper.WallpaperService;
import android.service.wallpaper.WallpaperService.Engine;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
public class LiveWallpaperService extends WallpaperService {

    public static final String PREFERENCES = "net.androgames.blog.sample.livewallpaper";
    public static final String PREFERENCE_RADIUS = "preference_radius";
    
    @Override
    public Engine onCreateEngine() {
            return new SampleEngine();
    }

    @Override
    public void onCreate() {
            super.onCreate();
    }

    @Override
    public void onDestroy() {
            super.onDestroy();
    }

    public class SampleEngine extends Engine implements SharedPreferences.OnSharedPreferenceChangeListener {

            private LiveWallpaperPainting painting;
            private SharedPreferences prefs;
            
            SampleEngine() {
                    SurfaceHolder holder = getSurfaceHolder();
                    prefs = LiveWallpaperService.this.getSharedPreferences(PREFERENCES, 0);
                    prefs.registerOnSharedPreferenceChangeListener(this);
                    painting = new LiveWallpaperPainting(holder, getApplicationContext(), 
                                    Integer.parseInt(prefs.getString(PREFERENCE_RADIUS, "10")));
            }

            public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
            }

            @Override
            public void onCreate(SurfaceHolder surfaceHolder) {
                    super.onCreate(surfaceHolder);
                    setTouchEventsEnabled(true);
            }

            @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);
            }

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


            @Override
            public void onVisibilityChanged(boolean visible) {
                    if (visible) {
                            painting.resumePainting();
                    } else {
                            // remove listeners and callbacks here
                            painting.pausePainting();
                    }
            }
            
            @Override
            public void onSurfaceDestroyed(SurfaceHolder holder) {
                    super.onSurfaceDestroyed(holder);
                    boolean retry = true;
                    painting.stopPainting();
                    while (retry) {
                            try {
                                    painting.join();
                                    retry = false;
                            } catch (InterruptedException e) {}
                    }
            }


            @Override
            public void onTouchEvent(MotionEvent event) {
                    super.onTouchEvent(event);
                    painting.doTouchEvent(event);
            }
            
    }
}


Методы OnCreate, OnDestroy, onVisibilityChanged, onSurfaceChanged, onSurfaceCreated и onSurfaceDestroyed вызываются, когда запускаем обои. Только с помощью этих методов, живые обои могут быть анимироваными.

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