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;
}
}
在/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;
}
};
}
当然不能忘了在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();
}
};
......
最后我们在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(需翻墙)
Android动态壁纸支持(需翻墙)
感谢各位,感谢开源!