【Android】Wallpaper学习
从wallpaper的设置来了解相关内容:
一,静态壁纸
静态壁纸设置的原理是在WallpaperManagerService里监听/data/system/users/0/wallpaper_orig相关文件的变化来触发设置,通过相应的组件程序去进行绘制,
相应的组件如:
mWallpaperComponent=ComponentInfo{com.android.systemui/com.android.systemui.wallpapers.ImageWallpaper}
Wallpaper connection com.android.server.wallpaper.WallpaperManagerService$WallpaperConnection@739a4fa:
以下是一个简单的 Android 程序示例,展示如何设置设备的壁纸。程序会从资源文件中加载一张图片,并将其设置为设备的主屏幕壁纸。
1. 项目结构
(1) 添加权限
在 AndroidManifest.xml 中添加设置壁纸的权限:
Xml
深色版本
<uses-permission android:name="android.permission.SET_WALLPAPER" />
(2) 准备壁纸图片
将你想要设置为壁纸的图片放入 res/drawable 目录中,例如命名为 wallpaper_image.jpg。
2. 代码实现
(1) 主 Activity (MainActivity.java)
创建一个按钮,点击后设置壁纸。
Java
深色版本
package com.example.setwallpaper;
import android.app.WallpaperManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import java.io.IOException;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 获取按钮控件
Button setWallpaperButton = findViewById(R.id.set_wallpaper_button);
// 设置按钮点击事件
setWallpaperButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setWallpaper();
}
});
}
private void setWallpaper() {
// 获取 WallpaperManager 实例
WallpaperManager wallpaperManager = WallpaperManager.getInstance(getApplicationContext());
try {
// 加载图片资源
Bitmap wallpaperBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.wallpaper_image);
// 设置壁纸
wallpaperManager.setBitmap(wallpaperBitmap);
// 提示用户成功设置壁纸
Toast.makeText(MainActivity.this, "壁纸设置成功!", Toast.LENGTH_SHORT).show();
} catch (IOException e) {
// 捕获异常并提示用户失败
Toast.makeText(MainActivity.this, "壁纸设置失败!", Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
}
(2) 布局文件 (activity_main.xml)
定义一个按钮,用于触发设置壁纸的操作。
Xml
深色版本
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">
<Button
android:id="@+id/set_wallpaper_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设置壁纸" />
</LinearLayout>
3. 运行效果
启动应用后,界面会显示一个按钮。
点击按钮后,应用会从资源文件中加载图片,并将其设置为主屏幕的壁纸。
如果设置成功,会弹出一个 Toast 提示 "壁纸设置成功!";如果失败,则会提示 "壁纸设置失败!"。
4. 扩展功能
(1) 支持锁屏壁纸
如果你想同时设置主屏幕和锁屏壁纸,可以使用以下代码:
Java
深色版本
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// 设置主屏幕壁纸
wallpaperManager.setBitmap(wallpaperBitmap, null, true, WallpaperManager.FLAG_SYSTEM);
// 设置锁屏壁纸
wallpaperManager.setBitmap(wallpaperBitmap, null, true, WallpaperManager.FLAG_LOCK);
} else {
// 对于低版本系统,只能设置主屏幕壁纸
wallpaperManager.setBitmap(wallpaperBitmap);
}
(2) 从相册选择图片
你可以通过 Intent 让用户从相册中选择一张图片作为壁纸:
Java
深色版本
private static final int PICK_IMAGE_REQUEST = 1;
private void openGallery() {
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
startActivityForResult(intent, PICK_IMAGE_REQUEST);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null) {
try {
// 获取选中的图片 URI
Uri imageUri = data.getData();
// 将图片转换为 Bitmap
Bitmap wallpaperBitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), imageUri);
// 设置壁纸
WallpaperManager wallpaperManager = WallpaperManager.getInstance(getApplicationContext());
wallpaperManager.setBitmap(wallpaperBitmap);
// 提示用户成功设置壁纸
Toast.makeText(this, "壁纸设置成功!", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "壁纸设置失败!", Toast.LENGTH_SHORT).show();
}
}
}
5. 注意事项
权限问题:
设置壁纸不需要动态申请权限,但如果你从相册选择图片,则需要处理运行时权限(如 READ_EXTERNAL_STORAGE)。
setBitmap的调用,可以查看WallpaperManager.java
public int setBitmap(Bitmap fullImage, Rect visibleCropHint,boolean allowBackup, @SetWallpaperFlags int which, int userId)throws IOException {validateRect(visibleCropHint);if (sGlobals.mService == null) {Log.w(TAG, "WallpaperService not running");throw new RuntimeException(new DeadSystemException());}final Bundle result = new Bundle();final WallpaperSetCompletion completion = new WallpaperSetCompletion();try {ParcelFileDescriptor fd = sGlobals.mService.setWallpaper(null,mContext.getOpPackageName(), visibleCropHint, allowBackup,result, which, completion, userId);if (fd != null) {FileOutputStream fos = null;try {fos = new ParcelFileDescriptor.AutoCloseOutputStream(fd);fullImage.compress(Bitmap.CompressFormat.PNG, 90, fos);fos.close();completion.waitForCompletion();} finally {IoUtils.closeQuietly(fos);}}} catch (RemoteException e) {throw e.rethrowFromSystemServer();}return result.getInt(EXTRA_NEW_WALLPAPER_ID, 0);}
调用到setWallpaper方法
@Override
3009 public ParcelFileDescriptor setWallpaper(String name, String callingPackage,
3010 Rect cropHint, boolean allowBackup, Bundle extras, int which,
3011 IWallpaperManagerCallback completion, int userId) {
3012 userId = ActivityManager.handleIncomingUser(getCallingPid(), getCallingUid(), userId,
3013 false /* all */, true /* full */, "changing wallpaper", null /* pkg */);
3014 checkPermission(android.Manifest.permission.SET_WALLPAPER);
3015
3016 if ((which & (FLAG_LOCK|FLAG_SYSTEM)) == 0) {
3017 final String msg = "Must specify a valid wallpaper category to set";
3018 Slog.e(TAG, msg);
3019 throw new IllegalArgumentException(msg);
3020 }
3021
3022 if (!isWallpaperSupported(callingPackage) || !isSetWallpaperAllowed(callingPackage)) {
3023 return null;
3024 }
3025
3026 // "null" means the no-op crop, preserving the full input image
3027 if (cropHint == null) {
3028 cropHint = new Rect(0, 0, 0, 0);
3029 } else {
3030 if (cropHint.width() < 0 || cropHint.height() < 0
3031 || cropHint.left < 0
3032 || cropHint.top < 0) {
3033 throw new IllegalArgumentException("Invalid crop rect supplied: " + cropHint);
3034 }
3035 }
3036
3037 synchronized (mLock) {
3038 if (DEBUG) Slog.v(TAG, "setWallpaper which=0x" + Integer.toHexString(which));
3039 WallpaperData wallpaper;
3040 final WallpaperData originalSystemWallpaper = mWallpaperMap.get(userId);
3041 final boolean systemIsStatic =
3042 originalSystemWallpaper != null && mImageWallpaper.equals(
3043 originalSystemWallpaper.wallpaperComponent);
3044 final boolean systemIsBoth = mLockWallpaperMap.get(userId) == null;
3045
3046 /* If we're setting system but not lock, and lock is currently sharing the system
3047 * wallpaper, we need to migrate that image over to being lock-only before
3048 * the caller here writes new bitmap data.
3049 */
3050 if (which == FLAG_SYSTEM && systemIsStatic && systemIsBoth) {
3051 Slog.i(TAG, "Migrating current wallpaper to be lock-only before"
3052 + " updating system wallpaper");
3053 if (!migrateStaticSystemToLockWallpaperLocked(userId)
3054 && !isLockscreenLiveWallpaperEnabled()) {
3055 which |= FLAG_LOCK;
3056 }
3057 }
3058
3059 wallpaper = getWallpaperSafeLocked(userId, which);
3060 if (mPendingMigrationViaStatic != null) {
3061 Slog.w(TAG, "Starting new static wp migration before previous migration finished");
3062 }
3063 mPendingMigrationViaStatic = new WallpaperDestinationChangeHandler(wallpaper);
3064 final long ident = Binder.clearCallingIdentity();
3065 try {
3066 ParcelFileDescriptor pfd = updateWallpaperBitmapLocked(name, wallpaper, extras);
3067 if (pfd != null) {
3068 wallpaper.imageWallpaperPending = true;
3069 wallpaper.mSystemWasBoth = systemIsBoth;
3070 wallpaper.mWhich = which;
3071 wallpaper.setComplete = completion;
3072 wallpaper.fromForegroundApp = isFromForegroundApp(callingPackage);
3073 wallpaper.cropHint.set(cropHint);
3074 wallpaper.allowBackup = allowBackup;
3075 wallpaper.mWallpaperDimAmount = getWallpaperDimAmount();
3076 }
3077 return pfd;
3078 } finally {
3079 Binder.restoreCallingIdentity(ident);
3080 }
3081 }
3082 }
又调用到
updateWallpaperBitmapLocked
3121 ParcelFileDescriptor updateWallpaperBitmapLocked(String name, WallpaperData wallpaper,
3122 Bundle extras) {
3123 if (name == null) name = "";
3124 try {
3125 File dir = getWallpaperDir(wallpaper.userId);
3126 if (!dir.exists()) {
3127 dir.mkdir();
3128 FileUtils.setPermissions(
3129 dir.getPath(),
3130 FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
3131 -1, -1);
3132 }
3133 ParcelFileDescriptor fd = ParcelFileDescriptor.open(wallpaper.wallpaperFile,
3134 MODE_CREATE|MODE_READ_WRITE|MODE_TRUNCATE);
3135 if (!SELinux.restorecon(wallpaper.wallpaperFile)) {
3136 Slog.w(TAG, "restorecon failed for wallpaper file: " +
3137 wallpaper.wallpaperFile.getPath());
3138 return null;
3139 }
3140 wallpaper.name = name;
3141 wallpaper.wallpaperId = makeWallpaperIdLocked();
3142 if (extras != null) {
3143 extras.putInt(WallpaperManager.EXTRA_NEW_WALLPAPER_ID, wallpaper.wallpaperId);
3144 }
3145 // Nullify field to require new computation
3146 wallpaper.primaryColors = null;
3147 Slog.v(TAG, "updateWallpaperBitmapLocked() : id=" + wallpaper.wallpaperId
3148 + " name=" + name + " file=" + wallpaper.wallpaperFile.getName());
3149 return fd;
3150 } catch (FileNotFoundException e) {
3151 Slog.w(TAG, "Error setting wallpaper", e);
3152 }
3153 return null;
3154 }
二,动态壁纸
在 Android 中开发动态壁纸需要使用 WallpaperService 和 Engine 类。动态壁纸本质上是一个服务,它运行在后台并持续渲染内容到用户的主屏幕或锁屏上。
以下是开发一个简单动态壁纸的步骤和示例代码:
1. 创建动态壁纸项目
(1) 添加权限
在 AndroidManifest.xml 中声明动态壁纸服务,并添加必要的权限。
Xml
深色版本
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.dynamicwallpaper">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<!-- 动态壁纸服务 -->
<service
android:name=".MyWallpaperService"
android:label="@string/wallpaper_label"
android:exported="true"
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/wallpaper_info" />
</service>
</application>
</manifest>
(2) 创建动态壁纸元数据文件
在 res/xml 目录下创建一个 XML 文件(例如 wallpaper_info.xml),用于描述动态壁纸的元信息。
Xml
深色版本
<wallpaper
xmlns:android="http://schemas.android.com/apk/res/android"
android:thumbnail="@drawable/thumbnail_image"
android:description="@string/wallpaper_description"
android:settingsActivity="com.example.dynamicwallpaper.SettingsActivity" />
android:thumbnail:动态壁纸的缩略图。
android:description:动态壁纸的描述。
android:settingsActivity(可选):设置界面的 Activity。
2. 实现动态壁纸服务
(1) 继承 WallpaperService
创建一个类继承 WallpaperService,并实现其抽象方法。
package com.example.dynamicwallpaper;
import android.service.wallpaper.WallpaperService;
import android.view.SurfaceHolder;
public class MyWallpaperService extends WallpaperService {
@Override
public Engine onCreateEngine() {
return new MyWallpaperEngine();
}
private class MyWallpaperEngine extends Engine {
private boolean isVisible = false;
private final Handler handler = new Handler();
@Override
public void onCreate(SurfaceHolder surfaceHolder) {
super.onCreate(surfaceHolder);
// 初始化操作
}
@Override
public void onVisibilityChanged(boolean visible) {
this.isVisible = visible;
if (visible) {
drawFrame();
}
}
@Override
public void onDestroy() {
super.onDestroy();
// 清理资源
}
private void drawFrame() {
if (!isVisible) {
return;
}
SurfaceHolder holder = getSurfaceHolder();
Canvas canvas = null;
try {
canvas = holder.lockCanvas();
if (canvas != null) {
// 在这里绘制你的动态内容
drawOnCanvas(canvas);
}
} finally {
if (canvas != null) {
holder.unlockCanvasAndPost(canvas);
}
}
// 循环绘制下一帧
handler.removeCallbacks(drawRunner);
if (isVisible) {
handler.postDelayed(drawRunner, 16); // 每 16ms 更新一次(约 60fps)
}
}
private final Runnable drawRunner = new Runnable() {
@Override
public void run() {
drawFrame();
}
};
private void drawOnCanvas(Canvas canvas) {
// 示例:绘制一个简单的渐变色背景
Paint paint = new Paint();
LinearGradient gradient = new LinearGradient(
0, 0, canvas.getWidth(), canvas.getHeight(),
Color.RED, Color.BLUE, Shader.TileMode.CLAMP);
paint.setShader(gradient);
canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), paint);
}
}
}
3. 运行和测试
安装应用:
将 APK 安装到设备上。
设置动态壁纸:
打开设备的 设置 > 壁纸 > 选择壁纸。
这里可以看到注册了wallpaperService的程序
找到你的动态壁纸并设置。
查看效果:
返回主屏幕,你应该能看到动态壁纸在运行。
设置动态壁纸的log
04-20 20:39:25.432 2392 3859 W WallpaperManagerService: getWallpaperIdForUser wallpaper = WallpaperData@f4ca951, id: 10, which: 6, file mod: 1745150434978, no connection, type = 0, userId = 0, uri= null, wallpaperComponent = null, nextWallpaperComponent = null
04-20 20:39:34.437 2392 2705 D SemWallpaperManagerService: getModeEnsuredWhich: detected which = 7
04-20 20:39:34.437 2392 2705 V WallpaperManagerService: setWallpaperComponent name=ComponentInfo{com.example.testmac/com.example.testmac.MyWallpaperService} , which = 7 , coverWallpaperInfo = null
04-20 20:39:34.438 2392 2705 D WALLPAPER_SVC:WallpaperManagerService: Wallpaper file is changed or deleted. Delete previous cropped bitmap
04-20 20:39:34.449 2392 2705 D WALLPAPER_SVC:WallpaperManagerService: bindComponentNow 7
04-20 20:39:34.449 2392 2705 V WallpaperManagerService: bindWallpaperComponentLocked: componentName=ComponentInfo{com.example.testmac/com.example.testmac.MyWallpaperService} force=false
04-20 20:39:34.449 2392 2705 D WallpaperManagerService: WPMS.bindWallpaperComponentLocked-ComponentInfo{com.example.testmac/com.example.testmac.MyWallpaperService}
04-20 20:39:34.454 2392 2705 D WALLPAPER_SVC:WallpaperManagerService: bindService success connect : com.android.server.wallpaper.WallpaperManagerService$WallpaperConnection@c843b0d
04-20 20:39:34.454 2392 2392 D WallpaperManagerService: WPMS.onServiceConnected-ComponentInfo{com.example.testmac/com.example.testmac.MyWallpaperService}
04-20 20:39:34.456 2392 2705 V WallpaperManagerService: Removing window token: android.os.Binder@56ab743 , mDisplayId = 0
04-20 20:39:34.459 2392 2705 D WallpaperManagerService: wallpaper userId = 0 , current user Id = 0
04-20 20:39:34.460 2392 2705 V WallpaperManagerService: WPMS.bindWallpaperComponentLocked-ComponentInfo{com.example.testmac/com.example.testmac.MyWallpaperService} took to complete: 11ms
04-20 20:39:34.466 2392 2705 V WallpaperManagerService: notifyWallpaperColorsChangedOnDisplay 1
调用到bindWallpaperComponentLocked
广播
参考资料:
Android动态壁纸实现与WallPaPerService教程-CSDN博客
【安卓笔记】wallpaper设置系统主页墙纸的一种方法_android wallpaperservice-CSDN博客
Android T wallpaper相关流程_android wallpaperservice-CSDN博客