Android动态壁纸解析
TomGrantham
7年前
<h2>阅读之前</h2> <ul> <li>建议 <a href="/misc/goto?guid=4959750917859281067" rel="nofollow,noindex">下载使用</a> <em>Style动态壁纸</em> 应用</li> <li>文章后面会给出相应引用的链接</li> </ul> <h2>Android动态壁纸</h2> <p>动态壁纸是Android主屏幕中,可以动的、交互的背景。自Android 2.1开始支持。例如双击屏幕(Style中双击屏幕壁纸会变清晰)。相关的api在 android.service.wallpaper 包中。</p> <p>动态壁纸应用实际上和其他应用是很相似的。下面我们一步一步来学习怎么创建一款动态壁纸应用。最终的实现效果如下:</p> <p><img src="https://simg.open-open.com/show/d05c3cc17e6a019b80f70208a42af3f7.gif"></p> <p>如何创建动态壁纸应用</p> <p>要创建壁纸应用,首先你需要在 /res/xml 文件夹下面创建一个XML文件。这个文件包含了这个应用的描述、图标、以及应用指定的壁纸设置页面等。在壁纸设置页面会显示这些信息(右下角)。 <img src="https://simg.open-open.com/show/e24926230c09725c9410e4689699ab0f.png"></p> <p>同时,你也需要创建一个 Service ,继承自 WallpaperService 类。 WallpaperService 这个类是系统所有动态壁纸等基类。你必须实现 onCreateEngine() 方法,返回一个 android.service.wallpaper.WallpaperService.Engine 对象。这个对象处理动态壁纸生命周期中的事件,壁纸的动画和绘制。 Engine 类定义了一些生命周期方法,例如: onCreate() , onSurfaceCreated() , onVisibilityChanged() , onOffsetsChanged() , onTouchEvent() 和 onCommand() 。</p> <p>另外,这个 Service 需要 android.permission.BIND_WALLPAPER 权限,它必须被注册到一个 IntentFilter 中,并且这个 IntentFilter 的action是 android.service.wallpaper.WallpaperService 。</p> <p>打开壁纸设定的Intent</p> <pre> <code class="language-java">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); }</code></pre> <p>上代码</p> <p>以下代码可以在 <a href="/misc/goto?guid=4959750917947425728" rel="nofollow,noindex">这里</a> 找到。</p> <p>创建一个新的Project,可以选择不要Activity。但是为了让用户直接跳转到壁纸设置页面,我们创建了一个 MainActivity 。让用户能够对我们提供的壁纸进行设置,我们再创建一个 SettingActivity 。</p> <p>在 /res/xml 文件夹下创建 <em>wallpaper.xml</em> ,当然名字可以自取。包含如下内容。注意 android:settingsActivity 的值,是刚才创建的 SettingActivity 的包名,可能你需要修改。</p> <pre> <code class="language-java"><?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" /></code></pre> <p>这个文件包含了壁纸的描述和图标,同时包含一个设置页面(设置页面是可选的)。</p> <p>这个文件会在 AndroidManifest.xml 中用到。</p> <p>创建一个 NormalWallpaperService 类,暂时不用实现里面的方法。</p> <pre> <code class="language-java">public class NormalWallpaperService extends WallpaperService { @Override public Engine onCreateEngine() { return null; } }</code></pre> <p>同时在 AndroidManifest.xml 中声明它。</p> <pre> <code class="language-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></code></pre> <p>我们还必须在 AndroidManifest.xml 中增加下面的代码:</p> <pre> <code class="language-java"><uses-feature android:name="android.software.live_wallpaper" android:required="true" > </uses-feature></code></pre> <p>到此我们的基本配置已经OK了。下来我们来一步步实现动态壁纸的绘制。</p> <p>我们创建一个 MyPoint 类,用来存储我们绘制过的点。</p> <pre> <code class="language-java">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> <p>在 /res/xml 文件夹下创建 <em>prefs.xml</em> 。用于对动态壁纸的设置。</p> <pre> <code class="language-java"><?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></code></pre> <p>在我们创建的 SettingActivity 中增加如下代码:</p> <pre> <code class="language-java">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> <p>当然不能忘了在 AndroidManifest.xml 中注册。</p> <pre> <code class="language-java"><activity android:name=".SettingActivity" android:exported="true" android:label="@string/app_name"> </activity></code></pre> <p>在我们的壁纸 Service 即 WallpaperService 中,实现 Engine 。完整代码可以看 <a href="/misc/goto?guid=4959750918043914980" rel="nofollow,noindex">这里</a> 。</p> <pre> <code class="language-java">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> <p>最后我们在 MainActivity 中,增加按钮让用户跳转到壁纸设置页面。</p> <pre> <code class="language-java">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)); } }</code></pre> <p>这样,我们的第一个壁纸应用创建好了。每秒钟会随机画一个圆。并且支持自定义设置,可以设置圆的最大数量、是否支持触摸事件。</p> <p>运行之后的效果图:</p> <p><img src="https://simg.open-open.com/show/2446daedf0f4cb28c6661ece827fac5b.gif"></p> <p>GLWallpaperService</p> <p>GL就是OpenGL,它是一个高性能的二维和三维的图形绘制库。这里我不再详细的介绍,有兴趣的同学可以戳 <a href="/misc/goto?guid=4959750918121866631" rel="nofollow,noindex">这里</a> 。</p> <p>GLWallpaperService是早年(Android 2.2时期,为什么不是2.1?因为2.2开始支持OpenGL2.0)一位美国同学开发的,这位同学自发布了这一款开源项目之后在开源界就默默无闻了。当然,你不要觉得代码太老,没人维护。可是它就是那么的好用,而且没有问题。市面上的动态壁纸使用它的数不胜数。</p> <p>为什么GLWallpaperService</p> <p>知道什么是OpenGL,那么原因就很明了了。高性能、高性能、还是高性能。动态壁纸在主屏幕可见的时候就一直在绘制,那么用OpenGL是最适合不过了。</p> <p>让我们开始吧</p> <p>开始之前,需要我们重复上面创建动态壁纸的几个基本步骤。这里直接省略,同学们自己创建。</p> <p>接下来重要的,当然是把代码拿过来。代码也是简单,就一个类,直接放到项目里就行了。 <a href="/misc/goto?guid=4959750918206476478" rel="nofollow,noindex">还是在这里</a> 。可以看到代码的第一行写着 <em>2008年</em> ,你没有看错。</p> <p>现在我们需要实现里面的两个主要的类, Service 类和 GLSurfaceView.Renderer 类。这里的 Service 需要继承 GLWallpaperService ,它的行为和Android的 WallpaperService 类似,都是需要实现 onCreateEngine() 这个方法。但是为了使用OpenGL,我们需要返回一个 GLEngine 对象。 GLEngine 里面有一个 GLThread 对象,渲染操作都会在 GLThread 中执行,从而保证了高效。</p> <p>还是上代码</p> <p>我们还是由一个简单的demo开始,篇幅原因,我就用最简单的demo。</p> <p>创建 MyRenderer 继承自 GLSurfaceView.Renderer 。逻辑很简单,就是用OpenGL画个背景。</p> <pre> <code class="language-java">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() { } }</code></pre> <p>创建 MyGLWallpaperService 继承自 GLWallpaperService 。</p> <pre> <code class="language-java">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; } } }</code></pre> <p>demo创建好了,运行之前需要确保前面的基本配置都做好了。</p> <p>接下来,我们着手实现最前面的效果。</p> <p>先从 <a href="/misc/goto?guid=4959750918299440672" rel="nofollow,noindex">这里</a> 拿到 Cube 类,放到工程中,它用OpenGL接口画出一个立方体,并且每一面都是一张 Bitmap 。具体怎么绘制的,有兴趣自己研究一下,这里不多介绍了。</p> <p>创建一个 AdvanceRenderer 实现 GLSurfaceView.Renderer 。</p> <pre> <code class="language-java">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() { } }</code></pre> <p>代码中充斥着各种OpenGL的调用,看不懂没关系,简单理解成在绘制就行了。</p> <p>接着,创建 AdvanceGLWallpaperService 继承自 GLWallpaperService 。</p> <pre> <code class="language-java">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; } } }</code></pre> <p>目前两个demo的Service基本没有什么区别,区别在于 Renderer 。运行代码,效果如下:</p> <p><img src="https://simg.open-open.com/show/46a0963d40c4a3825d01e5aefdd4a4c4.png"></p> <p>雏形已经出来了,可是它还不能跟着手势滚动。那么下面我们来处理触摸事件。</p> <p>首先,我们需要在 AdvanceGLWallpaperService 中的 AdvanceEngine 中实现 onCreate(SurfaceHolder surfaceHolder) 方法,并且通过 setTouchEventsEnabled(true) 设置它能够接受触摸事件。 同时实现 onTouchEvent(MotionEvent event) 方法来处理触摸事件。</p> <pre> <code class="language-java">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; } }</code></pre> <p>触摸事件我们是交给 Renderer 处理的。 Renderer 中的实现如下:</p> <pre> <code class="language-java">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; }</code></pre> <p>可以看到, Renderer 中仅仅是通过触摸的位置设置了它的一些变量。前面说过动态壁纸会不停的绘制,因此在不断根据这些变量进行绘制,变量一改变,绘制的位置、方向等等就改变了,从而达到了动态的效果。用户看来就是跟着自己的手势动了起来。</p> <p>另外,上一个demo中我们绘制时没有对这些变量进行处理,现在我们加上两句代码。</p> <pre> <code class="language-java">@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); }</code></pre> <p>跟前面的对比发现,还真只加了两句代码。聪明的你能看出是哪两句么?</p> <p>运行效果:</p> <p><img src="https://simg.open-open.com/show/dd7472414eb390ff014e5cd035b6b0ac.gif"></p> <p>What fk</p> <p>同学们可能会问,最上面的效果是不需要触摸就自动动的,现在的效果不一样啊。</p> <p>其实仔细想一想,触摸我们都解决了,自动的难道会难么?这个就当留了个课后作业给大家。</p> <p>提示:有几句代码为给注释掉了。</p> <h2>结论</h2> <p><a href="/misc/goto?guid=4959750918385748307" rel="nofollow,noindex">前一篇文章</a> 讲述Android的架构方面的知识,很多同学说根本看不懂。想当年我语文高考87分,差三分及格,以后我们还是多上代码吧。</p> <p>当然写这篇文章的目的不是为了让大家都去写动态壁纸应用,因为已经有一款非常优秀的了,没错,那就是 <a href="/misc/goto?guid=4959750918471499717" rel="nofollow,noindex">Style</a> , <em> <a href="/misc/goto?guid=4959750918471499717" rel="nofollow,noindex">Style</a> </em> , <strong> <a href="/misc/goto?guid=4959750918471499717" rel="nofollow,noindex">Style</a> </strong> 。</p> <p>这是一个典型的OpenGL应用场景,通过这篇文章大家也能对动态壁纸开发有一定的了解。我更希望的是,大家能动手将代码跑起来,动手的过程就是强化学习的过程。</p> <h2>引用</h2> <p><a href="/misc/goto?guid=4959750918579128402" rel="nofollow,noindex">Style艺术壁纸</a></p> <p><a href="/misc/goto?guid=4959750918121866631" rel="nofollow,noindex">OpenGL</a> (需KX上网)</p> <p><a href="/misc/goto?guid=4959750918673019682" rel="nofollow,noindex">Android动态壁纸支持</a> (需KX上网)</p> <p>感谢各位,感谢开源!</p> <p> </p> <p>来自:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2017/0725/8242.html</p> <p> </p>