Android動態壁紙解析
閱讀之前
- 建議 下載使用 Style動態壁紙 應用
- 文章后面會給出相應引用的鏈接
Android動態壁紙
動態壁紙是Android主屏幕中,可以動的、交互的背景。自Android 2.1開始支持。例如雙擊屏幕(Style中雙擊屏幕壁紙會變清晰)。相關的api在 android.service.wallpaper 包中。
動態壁紙應用實際上和其他應用是很相似的。下面我們一步一步來學習怎么創建一款動態壁紙應用。最終的實現效果如下:
如何創建動態壁紙應用
要創建壁紙應用,首先你需要在 /res/xml 文件夾下面創建一個XML文件。這個文件包含了這個應用的描述、圖標、以及應用指定的壁紙設置頁面等。在壁紙設置頁面會顯示這些信息(右下角)。
同時,你也需要創建一個 Service ,繼承自 WallpaperService 類。 WallpaperService 這個類是系統所有動態壁紙等基類。你必須實現 onCreateEngine() 方法,返回一個 android.service.wallpaper.WallpaperService.Engine 對象。這個對象處理動態壁紙生命周期中的事件,壁紙的動畫和繪制。 Engine 類定義了一些生命周期方法,例如: onCreate() , onSurfaceCreated() , onVisibilityChanged() , onOffsetsChanged() , onTouchEvent() 和 onCommand() 。
另外,這個 Service 需要 android.permission.BIND_WALLPAPER 權限,它必須被注冊到一個 IntentFilter 中,并且這個 IntentFilter 的action是 android.service.wallpaper.WallpaperService 。
打開壁紙設定的Intent
public void onClick(View view) {
Intent intent = new Intent(
WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER);
intent.putExtra(WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT,
new ComponentName(this, MyWallpaperService.class));
startActivity(intent);
}
上代碼
以下代碼可以在 這里 找到。
創建一個新的Project,可以選擇不要Activity。但是為了讓用戶直接跳轉到壁紙設置頁面,我們創建了一個 MainActivity 。讓用戶能夠對我們提供的壁紙進行設置,我們再創建一個 SettingActivity 。
在 /res/xml 文件夾下創建 wallpaper.xml ,當然名字可以自取。包含如下內容。注意 android:settingsActivity 的值,是剛才創建的 SettingActivity 的包名,可能你需要修改。
<?xml version="1.0" encoding="utf-8"?>
<wallpaper xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/normal_wallpaper_des"
android:settingsActivity="com.yalin.wallpaper.demo.SettingActivity"
android:thumbnail="@drawable/ic_launcher_round" />
這個文件包含了壁紙的描述和圖標,同時包含一個設置頁面(設置頁面是可選的)。
這個文件會在 AndroidManifest.xml 中用到。
創建一個 NormalWallpaperService 類,暫時不用實現里面的方法。
public class NormalWallpaperService extends WallpaperService {
@Override
public Engine onCreateEngine() {
return null;
}
}
同時在 AndroidManifest.xml 中聲明它。
<service
android:name=".normal.NormalWallpaperService"
android:enabled="true"
android:label="@string/wallpaper"
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/normal_wallpaper" />
</service>
我們還必須在 AndroidManifest.xml 中增加下面的代碼:
<uses-feature
android:name="android.software.live_wallpaper"
android:required="true" >
</uses-feature>
到此我們的基本配置已經OK了。下來我們來一步步實現動態壁紙的繪制。
我們創建一個 MyPoint 類,用來存儲我們繪制過的點。
public class MyPoint {
String text;
int x;
int y;
public MyPoint(String text, int x, int y) {
this.text = text;
this.x = x;
this.y = y;
}
}</code></pre>
在 /res/xml 文件夾下創建 prefs.xml 。用于對動態壁紙的設置。
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<CheckBoxPreference
android:key="touch"
android:title="Enable Touch" />
<EditTextPreference
android:key="numberOfCircles"
android:title="Number of Circles" />
</PreferenceScreen>
在我們創建的 SettingActivity 中增加如下代碼:
public class SettingActivity extends PreferenceActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.prefs01);
// add a validator to the "numberofCircles" preference so that it only
// accepts numbers
Preference circlePreference = getPreferenceScreen().findPreference(
"numberOfCircles");
// add the validator
circlePreference.setOnPreferenceChangeListener(numberCheckListener);
}
/**
* Checks that a preference is a valid numerical value
*/
Preference.OnPreferenceChangeListener numberCheckListener =
new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
// check that the string is an integer
if (newValue != null && newValue.toString().length() > 0
&& newValue.toString().matches("d*")) {
return true;
}
// If now create a message to the user
Toast.makeText(SettingActivity.this, "Invalid Input",
Toast.LENGTH_SHORT).show();
return false;
}
};
}</code></pre>
當然不能忘了在 AndroidManifest.xml 中注冊。
<activity
android:name=".SettingActivity"
android:exported="true"
android:label="@string/app_name">
</activity>
在我們的壁紙 Service 即 WallpaperService 中,實現 Engine 。完整代碼可以看 這里 。
public class NormalWallpaperService extends WallpaperService {
@Override
public Engine onCreateEngine() {
return new MyWallpaperEngine();
}
private class MyWallpaperEngine extends Engine {
private final Handler handler = new Handler();
private final Runnable drawRunner = new Runnable() {
@Override
public void run() {
draw();
}
};
......</code></pre>
最后我們在 MainActivity 中,增加按鈕讓用戶跳轉到壁紙設置頁面。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void setting(View view) {
Intent intent = new Intent(
WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER);
intent.putExtra(WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT,
new ComponentName(this, NormalWallpaperService.class));
startActivity(intent);
}
public void customSetting(View view) {
startActivity(new Intent(this, SettingActivity.class));
}
}
這樣,我們的第一個壁紙應用創建好了。每秒鐘會隨機畫一個圓。并且支持自定義設置,可以設置圓的最大數量、是否支持觸摸事件。
運行之后的效果圖:

GLWallpaperService
GL就是OpenGL,它是一個高性能的二維和三維的圖形繪制庫。這里我不再詳細的介紹,有興趣的同學可以戳 這里 。
GLWallpaperService是早年(Android 2.2時期,為什么不是2.1?因為2.2開始支持OpenGL2.0)一位美國同學開發的,這位同學自發布了這一款開源項目之后在開源界就默默無聞了。當然,你不要覺得代碼太老,沒人維護。可是它就是那么的好用,而且沒有問題。市面上的動態壁紙使用它的數不勝數。
為什么GLWallpaperService
知道什么是OpenGL,那么原因就很明了了。高性能、高性能、還是高性能。動態壁紙在主屏幕可見的時候就一直在繪制,那么用OpenGL是最適合不過了。
讓我們開始吧
開始之前,需要我們重復上面創建動態壁紙的幾個基本步驟。這里直接省略,同學們自己創建。
接下來重要的,當然是把代碼拿過來。代碼也是簡單,就一個類,直接放到項目里就行了。 還是在這里 。可以看到代碼的第一行寫著 2008年 ,你沒有看錯。
現在我們需要實現里面的兩個主要的類, Service 類和 GLSurfaceView.Renderer 類。這里的 Service 需要繼承 GLWallpaperService ,它的行為和Android的 WallpaperService 類似,都是需要實現 onCreateEngine() 這個方法。但是為了使用OpenGL,我們需要返回一個 GLEngine 對象。 GLEngine 里面有一個 GLThread 對象,渲染操作都會在 GLThread 中執行,從而保證了高效。
還是上代碼
我們還是由一個簡單的demo開始,篇幅原因,我就用最簡單的demo。
創建 MyRenderer 繼承自 GLSurfaceView.Renderer 。邏輯很簡單,就是用OpenGL畫個背景。
public class MyRenderer implements GLSurfaceView.Renderer {
public void onDrawFrame(GL10 gl) {
// Your rendering code goes here
gl.glClearColor(0.2f, 0.4f, 0.2f, 1f);
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
}
public void onSurfaceChanged(GL10 gl, int width, int height) {
}
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
}
/**
* Called when the engine is destroyed. Do any necessary clean up because
* at this point your renderer instance is now done for.
*/
public void release() {
}
}
創建 MyGLWallpaperService 繼承自 GLWallpaperService 。
public class MyGLWallpaperService extends GLWallpaperService {
@Override
public Engine onCreateEngine() {
MyEngine engine = new MyEngine();
return engine;
}
private class MyEngine extends GLEngine {
MyRenderer renderer;
public MyEngine() {
super();
// handle prefs, other initialization
renderer = new MyRenderer();
setRenderer(renderer);
setRenderMode(RENDERMODE_CONTINUOUSLY);
}
public void onDestroy() {
super.onDestroy();
if (renderer != null) {
renderer.release();
}
renderer = null;
}
}
}
demo創建好了,運行之前需要確保前面的基本配置都做好了。
接下來,我們著手實現最前面的效果。
先從 這里 拿到 Cube 類,放到工程中,它用OpenGL接口畫出一個立方體,并且每一面都是一張 Bitmap 。具體怎么繪制的,有興趣自己研究一下,這里不多介紹了。
創建一個 AdvanceRenderer 實現 GLSurfaceView.Renderer 。
public class AdvanceRenderer implements GLSurfaceView.Renderer {
private Cube cube;
private Context context;
private float z = -5.0f; // Depth Into The Screen
public AdvanceRenderer(Context context) {
this.cube = new Cube();
this.context = context;
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
gl.glEnable(GL10.GL_LIGHT0); // Enable Light 0
// Blending
gl.glColor4f(1.0f, 1.0f, 1.0f, 0.5f); // Full Brightness. 50% Alpha ( NEW )
gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE); // Set The Blending Function For Translucency ( NEW )
gl.glDisable(GL10.GL_DITHER); // Disable dithering
gl.glEnable(GL10.GL_TEXTURE_2D); // Enable Texture Mapping
gl.glShadeModel(GL10.GL_SMOOTH); // Enable Smooth Shading
gl.glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Black Background
gl.glClearDepthf(1.0f); // Depth Buffer Setup
gl.glEnable(GL10.GL_DEPTH_TEST); // Enables Depth Testing
gl.glDepthFunc(GL10.GL_LEQUAL); // The Type Of Depth Testing To Do
// Really Nice Perspective Calculations
gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_NICEST);
cube.loadGLTexture(gl, context);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
if (height == 0) { // Prevent A Divide By Zero By
height = 1; // Making Height Equal One
}
gl.glViewport(0, 0, width, height); // Reset The Current Viewport
gl.glMatrixMode(GL10.GL_PROJECTION); // Select The Projection Matrix
gl.glLoadIdentity(); // Reset The Projection Matrix
// Calculate The Aspect Ratio Of The Window
GLU.gluPerspective(gl, 45.0f, (float) width / (float) height, 0.1f, 100.0f);
gl.glMatrixMode(GL10.GL_MODELVIEW); // Select The Modelview Matrix
gl.glLoadIdentity(); // Reset The Modelview Matrix
}
@Override
public void onDrawFrame(GL10 gl) {
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
gl.glLoadIdentity(); // Reset The Current Modelview Matrix
// Check if the light flag has been set to enable/disable lighting
gl.glEnable(GL10.GL_LIGHTING);
// Check if the blend flag has been set to enable/disable blending
gl.glEnable(GL10.GL_BLEND); // Turn Blending On ( NEW )
gl.glDisable(GL10.GL_DEPTH_TEST); // Turn Depth Testing Off ( NEW )
// Drawing
gl.glTranslatef(0.0f, 0.0f, z); // Move z units into the screen
// Scale the Cube to 80 percent, otherwise it would be too large for the screen
gl.glScalef(0.8f, 0.8f, 0.8f);
cube.draw(gl, 0);
}
/**
* Called when the engine is destroyed. Do any necessary clean up because
* at this point your renderer instance is now done for.
*/
public void release() {
}
}
代碼中充斥著各種OpenGL的調用,看不懂沒關系,簡單理解成在繪制就行了。
接著,創建 AdvanceGLWallpaperService 繼承自 GLWallpaperService 。
public class AdvanceGLWallpaperService extends GLWallpaperService {
@Override
public Engine onCreateEngine() {
return new AdvanceEngine();
}
private class AdvanceEngine extends GLEngine {
AdvanceRenderer renderer;
public AdvanceEngine() {
super();
renderer = new AdvanceRenderer(AdvanceGLWallpaperService.this);
setRenderer(renderer);
setRenderMode(RENDERMODE_CONTINUOUSLY);
}
public void onDestroy() {
super.onDestroy();
if (renderer != null) {
renderer.release();
}
renderer = null;
}
}
}
目前兩個demo的Service基本沒有什么區別,區別在于 Renderer 。運行代碼,效果如下:

雛形已經出來了,可是它還不能跟著手勢滾動。那么下面我們來處理觸摸事件。
首先,我們需要在 AdvanceGLWallpaperService 中的 AdvanceEngine 中實現 onCreate(SurfaceHolder surfaceHolder) 方法,并且通過 setTouchEventsEnabled(true) 設置它能夠接受觸摸事件。 同時實現 onTouchEvent(MotionEvent event) 方法來處理觸摸事件。
private class AdvanceEngine extends GLEngine {
AdvanceRenderer renderer;
public AdvanceEngine() {
super();
renderer = new AdvanceRenderer(AdvanceGLWallpaperService.this);
setRenderer(renderer);
setRenderMode(RENDERMODE_CONTINUOUSLY);
}
@Override
public void onCreate(SurfaceHolder surfaceHolder) {
super.onCreate(surfaceHolder);
// Add touch events
setTouchEventsEnabled(true);
}
@Override
public void onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
renderer.onTouchEvent(event);
}
@Override
public void onDestroy() {
super.onDestroy();
if (renderer != null) {
renderer.release();
}
renderer = null;
}
}
觸摸事件我們是交給 Renderer 處理的。 Renderer 中的實現如下:
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
// If a touch is moved on the screen
if (event.getAction() == MotionEvent.ACTION_MOVE) {
// Calculate the change
float dx = x - oldX;
float dy = y - oldY;
// Define an upper area of 10% on the screen
int upperArea = 0;
// Zoom in/out if the touch move has been made in the upper
if (y < upperArea) {
z -= dx * TOUCH_SCALE / 2;
// Rotate around the axis otherwise
} else {
xrot += dy * TOUCH_SCALE;
yrot += dx * TOUCH_SCALE;
}
}
// Remember the values
oldX = x;
oldY = y;
// We handled the event
return true;
}
可以看到, Renderer 中僅僅是通過觸摸的位置設置了它的一些變量。前面說過動態壁紙會不停的繪制,因此在不斷根據這些變量進行繪制,變量一改變,繪制的位置、方向等等就改變了,從而達到了動態的效果。用戶看來就是跟著自己的手勢動了起來。
另外,上一個demo中我們繪制時沒有對這些變量進行處理,現在我們加上兩句代碼。
@Override
public void onDrawFrame(GL10 gl) {
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
gl.glLoadIdentity(); // Reset The Current Modelview Matrix
// Check if the light flag has been set to enable/disable lighting
gl.glEnable(GL10.GL_LIGHTING);
// Check if the blend flag has been set to enable/disable blending
gl.glEnable(GL10.GL_BLEND); // Turn Blending On ( NEW )
gl.glDisable(GL10.GL_DEPTH_TEST); // Turn Depth Testing Off ( NEW )
// Drawing
gl.glTranslatef(0.0f, 0.0f, z); // Move z units into the screen
// Scale the Cube to 80 percent, otherwise it would be too large for the screen
gl.glScalef(0.8f, 0.8f, 0.8f);
// Rotate around the axis based on the rotation matrix (rotation, x, y, z)
gl.glRotatef(xrot, 1.0f, 0.0f, 0.0f); // X
gl.glRotatef(yrot, 0.0f, 1.0f, 0.0f); // Y
cube.draw(gl, 0);
}
跟前面的對比發現,還真只加了兩句代碼。聰明的你能看出是哪兩句么?
運行效果:

What fk
同學們可能會問,最上面的效果是不需要觸摸就自動動的,現在的效果不一樣啊。
其實仔細想一想,觸摸我們都解決了,自動的難道會難么?這個就當留了個課后作業給大家。
提示:有幾句代碼為給注釋掉了。
結論
前一篇文章 講述Android的架構方面的知識,很多同學說根本看不懂。想當年我語文高考87分,差三分及格,以后我們還是多上代碼吧。
當然寫這篇文章的目的不是為了讓大家都去寫動態壁紙應用,因為已經有一款非常優秀的了,沒錯,那就是 Style , Style , Style 。
這是一個典型的OpenGL應用場景,通過這篇文章大家也能對動態壁紙開發有一定的了解。我更希望的是,大家能動手將代碼跑起來,動手的過程就是強化學習的過程。
引用
OpenGL (需KX上網)
Android動態壁紙支持 (需KX上網)
感謝各位,感謝開源!
來自:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2017/0725/8242.html