dcsimg
July 18, 2018
Hot Topics:

Building an Android Live Wallpaper

  • July 2, 2018
  • By Chunyen Liu
  • Send Email »
  • More Articles »

In this tutorial, we will turn our attention to experiment a fun feature called "Live Wallpaper" on Android devices. A live wallpaper is basically a service app showing the animated and interactive background for your device screen. It is similar to other Android apps and can use most of the available functionalities. Implemented like a typical Android service, it is responsible for showing a live background behind applications that would like to sit on top.

We start the tutorial by briefly introducing where you can locate the live wallpapers available on your device, what the key wallpaper APIs are, what the app setup looks like, and an example code walk-through. Because we intend to animate the device background, a simple yet innovative concept called "boids" simulating flocking creatures also will be introduced as a bonus for our live wallpaper example. As in all my previous tutorials, complete source codes are available in the References section.

Where to Locate Live Wallpapers on Your Mobile Device

On your Android device screen, you can do a long press on the empty space, and a list of icons will be displayed at the bottom. For example, we have "Wallpaper," "Widgets," "Settings," and so on. Yours could look slightly different due to a variety of Android platforms and devices in the market. Mine is running Oreo OS on a Nexus 6P. "Wallpapers" is our target interest. Once you click that icon, you will be presented with a list of static wallpapers, followed by live wallpapers ready for use on your device. Figure 1 shows the result on my Android phone.

Live Wallpapers on the Device
Figure 1: Live Wallpapers on the Device

Key API Packages and Setup for Live Wallpapers

The most important APIs used in live wallpapers are as follows:

  • android.app.WallpaperManager: This provides access to the system wallpaper. You can get the current wallpaper, get the desired dimensions for the wallpaper, set the wallpaper, and more.
  • android.service.wallpaper.WallpaperService: This is responsible for showing a live wallpaper, but the service object itself does not do much except for generating instances of WallpaperService.Engine as needed.

In the app's AnroidManifest.xml as in the following example, you need to enable the feature android.software.live_wallpaper. For the wallpaper service itself, you need the permission android.permission.BIND_WALLPAPER as well as claiming this is available through wallpaper service android.service.wallpaper.WallpaperService. We then specify the wallpaper is defined through a separate file called "mylwp_main.xml" inside the "xml" subfolder.

<manifest xmlns:android="http://schemas.android.com/apk/res
      /android"
   package="com.chunyenliu.tutorialonlwp">

   <uses-feature android:name="android.software.live_wallpaper"
      android:required="true" />

   <application
      android:allowBackup="true"
      android:icon="@mipmap/ic_launcher"
      android:label="@string/app_name"
      android:roundIcon="@mipmap/ic_launcher_round"
      android:supportsRtl="true"
      android:theme="@style/AppTheme">

      <service
         android:label="MyLWPService"
         android:name=".MyLWPService"
         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/mylwp_main" />
      </service>

      ...(code snipped)
        
   </application>
</manifest>

So, what info is in "mylwp_main.xml"? We can describe what this live wallpaper is all about as well as configure it through android:settingsActivity, how it should behave in the activity, "com.chunyenliu.tutorialonlwp.MyLWPSettings".

<wallpaper
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:thumbnail="@drawable/boids"
   android:description="@string/mylwp_description"
   android:settingsActivity="com.chunyenliu.tutorialonlwp
      .MyLWPSettings"
/>

Basic Concept for Flocking Creatures

Before we actually start to build our live wallpaper service, we want to share with you some useful knowledge. Because we plan to animate the background with flocking creatures for the live wallpaper, this is a great opportunity to introduce a cool and very simple concept created by the Academy Award winner, Craig Reynolds. He made a computer model of coordinated animal motion such as bird flocks and fish schools and called the generic simulated flocking creatures "boids." The basic flocking model consists of three simple steering behaviors which describe how an individual boid maneuvers based on the positions and velocities its nearby flockmates.

The complexity of boids arises from the interaction of individual agents adhering to a set of simple rules. The rules applied in the simplest boids world are: separation (steer to avoid crowding local flockmates), alignment (steer towards the average heading of local flockmates), and cohesion (steer to move toward the average position of local flockmates). Figure 2 shows the original concept for boids. Listing 1 is the implementation of boids based on some basic rules specified by the model. Please note that some rules are omitted due to time constraint and their complexity.

Boids
Figure 2: Boids

class Boids {
   int mTotal = 0;
   int mWidth = 0;
   int mHeight = 0;
   public static float mBoidDist = 50.0f;
   public static float mMaxBoidSpeed = 12.0f;
   Boid[] mBoidList = null;
   int mCount = 5;
   int mMaxCount = 5;
   Position mPlace = new Position();
   float mPlaceFactor = 1.0f;
   float mBoxMinX = 1000, mBoxMaxX = 0, mBoxMinY = 1000,
      mBoxMaxY = 0;
   int mStates = 4;

   public Boids(int n, int w, int h, int st) {
      mTotal = n;
      mWidth = w;
      mHeight = h;
      mStates = st;

      init();
   }

   public void init() {
      mBoidList = null;
      mBoidList = new Boid[mTotal];

      for (int i = 0; i < mTotal; i++) {
         mBoidList[i] = new Boid(mStates);

         mBoidList[i].mPosition.x = (int)(200 * Math.random());
         mBoidList[i].mPosition.y = (int)(300 * Math.random());

         while (0 == mBoidList[i].mVelocity.x)
            mBoidList[i].mVelocity.x = (int)(2 * mMaxBoidSpeed *
               Math.random()) - mMaxBoidSpeed;

         while (0 == mBoidList[i].mVelocity.y)
            mBoidList[i].mVelocity.y = (int)(2 * mMaxBoidSpeed *
               Math.random()) - mMaxBoidSpeed;
         }
      }

   public void moveToNext() {
      for (int i = 0; i < mTotal; i++) {
         Velocity v1;
         Velocity v2;
         if (mCount <= mMaxCount && mPlace != null) {
            v1 = ruleTendToPlace(i, mPlaceFactor);
            v2 = new Velocity();
         } else {
            v1 = ruleFlyToCentroid(i, 0.05f);
            v2 = ruleKeepSmallDistance(i, mBoidDist);
         }
         float w1 = 1.0f;
         float w2 = 1.0f;

         Velocity v3 = ruleMatchNearVelocity(i, 0.125f);
         float w3 = 1.0f;

         mBoidList[i].mVelocity.x += (w1 * v1.x + w2 * v2.x + w3 *
            v3.x);
         mBoidList[i].mVelocity.y += (w1 * v1.y + w2 * v2.y + w3 *
            v3.y);

         ruleLimitVelocity(i, mMaxBoidSpeed);

         mBoidList[i].mPosition.x += mBoidList[i].mVelocity.x;
         mBoidList[i].mPosition.y += mBoidList[i].mVelocity.y;

         ruleBoundPosition(i, (float)(0.7f * mMaxBoidSpeed *
            Math.random() + 0.3f * mMaxBoidSpeed));

         mBoidList[i].changeState();
         }

         if (mCount <= mMaxCount)
            mCount ++;
         }

   private void getBoundingBox() {
      mBoxMinX = mWidth; mBoxMaxX = 0;
      mBoxMinY = mHeight; mBoxMaxY = 0;
      for (int i = 0; i < mTotal; i++) {
         if (mBoidList[i].mPosition.x < mBoxMinX) mBoxMinX =
            mBoidList[i].mPosition.x;
         if (mBoidList[i].mPosition.x > mBoxMaxX) mBoxMaxX =
            mBoidList[i].mPosition.x;
         if (mBoidList[i].mPosition.y < mBoxMinY) mBoxMinY =
            mBoidList[i].mPosition.y;
         if (mBoidList[i].mPosition.y > mBoxMaxY) mBoxMaxY =
            mBoidList[i].mPosition.y;
      }
   }

   // Set a target place
   public void setTargetPlace(float x, float y, float f) {
      mPlace.x = x;
      mPlace.y = y;

      // If the touched point is within the boids, disperse
      // them away.
      // If it is outside the boids, make them follow it.
      getBoundingBox();
      if (x >= mBoxMinX && x <= mBoxMaxX && y >= mBoxMinY
            && y <= mBoxMaxY) {
         mPlaceFactor = -f;
         mCount  = 0;
         mMaxCount = 50;
      } else {		
         mPlaceFactor = f;
         mCount  = 0;
         mMaxCount = (int)(Math.abs((mBoxMinX + mBoxMaxX) /
            2.0f - x) + Math.abs((mBoxMinY + mBoxMaxY) /
            2.0f - y));
         }
   }

   public void setTargetNone() {
      // If direction is "following", stop it.
      if (mPlaceFactor > 0) {
         mCount = mMaxCount - 10;
      }
   }

   public int getTotal() {
      return mTotal;
   }

   public Boid[] getBoids() {
      return mBoidList;
   }

   // RULE: fly to center of other boids
   private Velocity ruleFlyToCentroid(int id, float factor) {
      Velocity v = new Velocity();
      Position p = new Position();

      for (int i = 0; i < mTotal; i++) {
         if (i != id) {
            p.x += mBoidList[i].mPosition.x;
            p.y += mBoidList[i].mPosition.y;
         }
      }

      if (mTotal > 2) {
         p.x /= (mTotal - 1);
         p.y /= (mTotal - 1);
      }

      v.x = (p.x -  mBoidList[id].mPosition.x) * factor;
      v.y = (p.y -  mBoidList[id].mPosition.y) * factor;

         return v;
      }

   // RULE: keep a small distance from other boids
   private Velocity ruleKeepSmallDistance(int id, float dist) {
      Velocity v = new Velocity();

      for (int i = 0; i < mTotal; i++) {
         if (i != id) {
            if (Math.abs(mBoidList[id].mPosition.x -
               mBoidList[i].mPosition.x) + 
               Math.abs(mBoidList[id].mPosition.y -
               mBoidList[i].mPosition.y) < dist) {
                  v.x -= (mBoidList[i].mPosition.x -
                mBoidList[id].mPosition.x);
                  v.y -= (mBoidList[i].mPosition.y -
               mBoidList[id].mPosition.y);
               }
            }
         }

      return v;
   }

   // RULE: match velocity with near boids
   private Velocity ruleMatchNearVelocity(int id, float factor) {
      Velocity v = new Velocity();

      for (int i = 0; i < mTotal; i++) {
         if (i != id) {
            v.x += mBoidList[i].mVelocity.x;
            v.y += mBoidList[i].mVelocity.y;
         }
      }

      if (mTotal > 2) {
         v.x /= (mTotal - 1);
         v.y /= (mTotal - 1);
      }

      v.x = (v.x -  mBoidList[id].mVelocity.x) * factor;
      v.y = (v.y -  mBoidList[id].mVelocity.y) * factor;

      return v;
      }

   // RULE: consider wind speed
   private Velocity ruleReactToWind() {
      Velocity v = new Velocity();

      v.x = 1.0f;
      v.y = 0.0f;

      return v;
   }

   // RULE: tend to a place
   private Velocity ruleTendToPlace(int id, float factor) {
      Velocity v = new Velocity();

      v.x = (mPlace.x - mBoidList[id].mPosition.x) * factor;
      v.y = (mPlace.y - mBoidList[id].mPosition.y) * factor;

      return v;
   }

   // RULE: limit the velocity
   private void ruleLimitVelocity(int id, float vmax) {

      float vv = (float)Math.sqrt(mBoidList[id].mVelocity.x *
         mBoidList[id].mVelocity.x +
         mBoidList[id].mVelocity.y * mBoidList[id].mVelocity.y);

      if (vv > vmax) {
         mBoidList[id].mVelocity.x = (mBoidList[id].mVelocity.x /
            vv) * vmax;
         mBoidList[id].mVelocity.y = (mBoidList[id].mVelocity.y /
            vv) * vmax;
      }
   }

   // RULE: bound the position
   private void ruleBoundPosition(int id, float initv) {
      float pad = 10.0f;

      if (mBoidList[id].mPosition.x < pad) {
         mBoidList[id].mVelocity.x = initv;
      } else if (mBoidList[id].mPosition.x > mWidth - 2.0f * pad) {
         mBoidList[id].mVelocity.x = -initv;
      }

      if (mBoidList[id].mPosition.y < pad) {
         mBoidList[id].mVelocity.y = initv;
      } else if (mBoidList[id].mPosition.y > mHeight - 5.0f * pad) {
         mBoidList[id].mVelocity.y = -initv;
      }
   }

   // TODO: scatter the flock b, negating three previous rules

   // TODO: add mPerching status when below the threshold height
}

class Boid {
   public Position mPosition = null;
   public Velocity mVelocity = null;
   public boolean mPerching = false;
   public int mState = 0;
   public int mStates = 4;

   public Boid() {
      mPosition = new Position();
      mVelocity = new Velocity();
   }

   public Boid(int s) {
      mPosition = new Position();
      mVelocity = new Velocity();
      initStates(s);
   }

   public void initStates(int s) {
      mStates = s;
      mState = (int)(mStates * Math.random());
   }

   public void changeState() {
      mState = (mState + 1) % mStates;
   }
}

class Position {
   public float x = 0;
   public float y = 0;
}

class Velocity {
   public float x = 0;
   public float y = 0;
}

Listing 1: Implementation of Boids

Live Wallpaper Example with Boids

In Listing 2, the core of the live wallpaper service is the engine instantiated by WallpaperService.onCreateEngine(). The engine contains a background thread, "mDrawThread," that can be removed based on the visibility or availability of the live wallpaper to save the battery consumption. "drawFrame()" is where most of the work is done and how a frame is animated and displayed. In this example, we essentially set up a static background with a bitmap image. On top of it, we send out 30 boids (bats in our example, with each animation sequence in Figure 3) and animate them according to the basic flocking rules to simulate their behavior. When you poke the flocking boids, they will start spreading away. When you touch a location outside the boids, they will try to flock to it.

Figure 4 shows our example in the wallpaper list. Figure 5 has the gear icon enabled because we define a configuration activity through android:settingsActivity in the file "mylwp_main.xml" mentioned previously.

public class MyLWPService extends WallpaperService {
   static int mWallpaperWidth;
   static int mWallpaperHeight;
   static int mViewWidth;
   static int mViewHeight;
   private Bitmap mSceneBitmap = null;
   private int mFrameRate = 20;
   private int mBoidCount = 30;
   private Bitmap mBoidSpriteSheet = null;
   private int mSpriteWidth = 1;
   private int mSpriteHeight = 1;
   private int mSpriteRow = 1;
   private int mSpriteCol = 1;

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

      // Get wallpaper width and height
      WallpaperManager wpm = WallpaperManager.getInstance
         (getApplicationContext());
      mWallpaperWidth = wpm.getDesiredMinimumWidth();
      mWallpaperHeight = wpm.getDesiredMinimumHeight();
   }

   @Override
   public Engine onCreateEngine() {
      return new MyEngine();
   }

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

   void setSceneBackground() {
      Bitmap b = BitmapFactory.decodeResource(getResources(),
         R.drawable.bg);

      if (null != mSceneBitmap)
         mSceneBitmap.recycle();

    Matrix m = new Matrix();
      m.setScale((float)mWallpaperWidth / (float)b.getWidth(),
         (float)mWallpaperHeight / (float)b.getHeight());
      mSceneBitmap = Bitmap.createBitmap(b, 0, 0,
             b.getWidth(), b.getHeight(), m, true);

      b.recycle();
   }

   void setSprites() {
      mBoidSpriteSheet = BitmapFactory.decodeResource
         (getResources(), R.drawable.bats);
      mSpriteWidth = 100;
      mSpriteHeight = 50;
      mSpriteRow = mBoidSpriteSheet.getHeight() / mSpriteHeight;
      mSpriteCol = mBoidSpriteSheet.getWidth() / mSpriteWidth;
   }

   class MyEngine extends Engine  {
      private final Handler mHandler = new Handler();
      private float mOffset = 0.0f;
      private final Paint mPaint = new Paint();
      private Boids mBoids = null;
      private final Runnable mDrawThread = new Runnable() {
         public void run() {
            drawFrame();

            if (mBoids != null)
               mBoids.moveToNext();

            try {
               Thread.sleep(50);
            } catch (Exception e) {
         }
      }
   };
      private boolean mVisible;

      MyEngine() {
         setSceneBackground();
         setSprites();

         mBoids = new Boids(mBoidCount, mWallpaperWidth,
            mWallpaperHeight, mSpriteRow * mSpriteCol);
      }

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

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

      @Override
      public void onVisibilityChanged(boolean visible) {
         mVisible = visible;
         if (visible) {
            drawFrame();
         } else {
            mHandler.removeCallbacks(mDrawThread);
         }
      }

      @Override
      public void onSurfaceChanged(SurfaceHolder holder,
            int format, int width, int height) {
         super.onSurfaceChanged(holder, format, width, height);

         mViewWidth = width;
         mViewHeight = height;

         drawFrame();
      }

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

      @Override
      public void onSurfaceDestroyed(SurfaceHolder holder) {
         super.onSurfaceDestroyed(holder);
         mVisible = false;
         mHandler.removeCallbacks(mDrawThread);
      }

       @Override
      public void onOffsetsChanged(float xOffset, float yOffset,
         float xStep, float yStep, int xPixels, int yPixels) {
            super.onOffsetsChanged(xOffset, yOffset,xStep, yStep,
            xPixels, yPixels);

            mOffset = 1.0f * xPixels;

            drawFrame();
      }

      @Override
      public void onTouchEvent(MotionEvent event) {
         if (event.getAction() == MotionEvent.ACTION_MOVE ||
            event.getAction() == MotionEvent.ACTION_DOWN) {
            mBoids.setTargetPlace(event.getX(), event.getY(),
               0.50f);
         } else {
            mBoids.setTargetNone();
         }

         super.onTouchEvent(event);
      }

      void drawFrame() {
         final SurfaceHolder holder = getSurfaceHolder();
         final Rect frame = holder.getSurfaceFrame();
         final int width = frame.width();
         final int height = frame.height();
         Canvas c = null;

         try {
            c = holder.lockCanvas();
            if (c != null) {

               c.drawBitmap(mSceneBitmap, mOffset, 00.0f, null);

               if (mBoids != null && mBoidSpriteSheet != null) {
                  Boid[] bb = mBoids.getBoids();
                  for (int i = 0; i < mBoids.getTotal(); i++) {
                     int yy = (bb[i].mState / mSpriteCol) *
                        mSpriteHeight;
                     int xx = (bb[i].mState % mSpriteCol) *
                        mSpriteWidth;
                     Rect src = new Rect(xx, yy, xx + mSpriteWidth,
                        yy + mSpriteHeight);
                     Rect dest = new Rect((int)bb[i].mPosition.x,
                        (int)bb[i].mPosition.y,
                        (int)bb[i].mPosition.x + mSpriteWidth,
                        (int)bb[i].mPosition.y + mSpriteHeight);
                        c.drawBitmap(mBoidSpriteSheet, src, dest,
                           null);
                   }
               }
            }
         } finally {
            if (c != null) holder.unlockCanvasAndPost(c);
         }

         mHandler.removeCallbacks(mDrawThread);
            if (mVisible) {
               mHandler.postDelayed(mDrawThread, 1000 /
               mFrameRate);
         }
      }
   } // MyEngine
}

Listing 2: Live Wallpaper with Boids

Animation Sequence for Each Boid
Figure 3: Animation Sequence for Each Boid

Boids Example in LiveWallpaper List
Figure 4: Boids Example in LiveWallpaper List

Boids Example in Action
Figure 5: Boids Example in Action

Conclusion

A fun Android feature called live wallpaper was briefly introduced in this tutorial. It is a service that makes your device background animated and can respond to user interaction. We also mentioned the key live wallpaper APIS, the basic setup, and an example with the innovative concept called "boids," simulating flocking creatures. You can find many more nicely done live wallpapers in Google Play and sample projects through Google's repositories. Sources codes for the tutorial examples are available for download in the References section.

References

About the Author

Chunyen Chunyen Liu has been a software veteran in Taiwan and the United States. He is a published author of 50+ articles and 100+ tiny apps, a software patentee, technical reviewer, and programming contest winner by ACM/IBM/SUN. He holds advanced degrees in Computer Science with 20+ US graduate-level classes. On the non-technical side, he is enthusiastic about the Olympic sport of table tennis, being a USA certified umpire, certified coach, certified referee, and categorized event winner at State Games and the US Open.





Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Enterprise Development Update

Don't miss an article. Subscribe to our newsletter below.

By submitting your information, you agree that developer.com may send you developer offers via email, phone and text message, as well as email offers about other products and services that developer believes may be of interest to you. developer will process your information in accordance with the Quinstreet Privacy Policy.

Sitemap

Thanks for your registration, follow us on our social networks to keep up-to-date