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>

在我们的壁纸ServiceWallpaperService中,实现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));
    }
}

这样,我们的第一个壁纸应用创建好了。每秒钟会随机画一个圆。并且支持自定义设置,可以设置圆的最大数量、是否支持触摸事件。

运行之后的效果图:

Demo2

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。运行代码,效果如下:

demo3

雏形已经出来了,可是它还不能跟着手势滚动。那么下面我们来处理触摸事件。

首先,我们需要在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);
    }

跟前面的对比发现,还真只加了两句代码。聪明的你能看出是哪两句么?

运行效果: demo3

What fk

同学们可能会问,最上面的效果是不需要触摸就自动动的,现在的效果不一样啊。

其实仔细想一想,触摸我们都解决了,自动的难道会难么?这个就当留了个课后作业给大家。

提示:有几句代码为给注释掉了。

结论

前一篇文章讲述Android的架构方面的知识,很多同学说根本看不懂。想当年我语文高考87分,差三分及格,以后我们还是多上代码吧。

当然写这篇文章的目的不是为了让大家都去写动态壁纸应用,因为已经有一款非常优秀的了,没错,那就是 Style, Style, Style

这是一个典型的OpenGL应用场景,通过这篇文章大家也能对动态壁纸开发有一定的了解。我更希望的是,大家能动手将代码跑起来,动手的过程就是强化学习的过程。

引用

Style艺术壁纸

OpenGL(需翻墙)

Android动态壁纸支持(需翻墙)

感谢各位,感谢开源!