某天,女朋友在刷某音,突然看到有视频作为锁屏,十分崇拜,撒娇的眼生看着我...看来这个需求躲不过了,身为安卓仔,这有什么的,开干!
动态壁纸-设置页面.png 视频设置成功-锁屏.png 视频设置成功-桌面.png搜了下资料,实现大概分为以下几步:
- 步骤一:新建类,继承WallpaperService,实现抽象方法。
- 步骤二:res->xml文件夹,新建xml文件,wallpaper节点,描述动态壁纸,就是一个相当于配置文件。
- 步骤三:AndroidManifest.xml清单文件,配置service和绑定步骤二的xml文件。
思路
- 使用视频动态壁纸的思路是这样的,WallpaperService需要返回一个Engine类对象,Engine类会回调返回桌面的SurfaceHolder,我们则可以用MediaPlayer来绑定这个SurfaceHolder来播放视频。这样我们的桌面和壁纸就可以播放视频啦~
下面是码代码时间~
- 第一步:新建类,继承WallpaperService,实现抽象方法。具体看注释吧~
这里只给出复写方法先,后面再仔细写~
public class VideoLiveWallpaper extends WallpaperService {
//返回一个Engine对象,这里
@Override
public Engine onCreateEngine() {
return new VideoEngine();
}
class VideoEngine extends Engine {
@Override
public void onCreate(SurfaceHolder surfaceHolder) {
//Engine对象被创建时回调,这里可以做广播注册等操作
}
@Override
public void onDestroy() {
//Engine对象被销毁时回调,这里可以注销广播注册等操作
super.onDestroy();
}
@Override
public void onVisibilityChanged(boolean visible) {
//显示、隐藏时切换,在桌面时为显示,跳转到别的App页面时为隐藏
//这里做视频的暂停和恢复播放~
}
@Override
public void onSurfaceCreated(SurfaceHolder holder) {
//SurfaceView被创建时回调,我们的视频MediaPlayer对象播放的视频输出在这个surface上。
}
@Override
public void onSurfaceDestroyed(SurfaceHolder holder) {
//Surface销毁时回调,这里我们应该销毁MediaPlayer,回收MediaPlayer。
}
}
}
- 第二步,res->xml文件夹,新建xml文件,wallpaper节点,描述动态壁纸,就是一个相当于配置文件。thumbnail标识在动态壁纸设置页面上显示的图标。
<?xml version="1.0" encoding="utf-8"?>
<wallpaper xmlns:android="http://schemas.android.com/apk/res/android"
android:thumbnail="@mipmap/fa_live_wallpaper_icon"/>
- 第三步,AndroidManifest.xml清单文件,配置service和绑定步骤二的xml文件。meta-data节点配置我们第二步写的xml文件。
<!-- 配置实时壁纸Service -->
<service
android:name="me.wally.arch.livewallpaper.VideoLiveWallpaper"
android:label="@string/fs_video_live_wallpaper"
android:permission="android.permission.BIND_WALLPAPER"
android:process=":wallpaper">
<!-- 为实时壁纸配置intent-filter -->
<intent-filter>
<action android:name="android.service.wallpaper.WallpaperService" />
</intent-filter>
<!-- 为实时壁纸配置meta-data -->
<meta-data
android:name="android.service.wallpaper"
android:resource="@xml/fs_video_wallpaper" />
</service>
上面分别分析了WallpaperService服务的几个抽象方法,下面我们一步步来将MediaPlayer的视频输出到桌面的SufaceView中~
- 新建Engine类子类,VideoEngine,作为我们的Engine类。在Service的onCreateEngine()方法中返回出去。
@Override
public Engine onCreateEngine() {
return new VideoEngine();
}
- 复写onSurfaceCreated()方法,在Suface创建时,创建MediaPlayer
@Override
public void onSurfaceCreated(SurfaceHolder holder) {
super.onSurfaceCreated(holder);
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setSurface(holder.getSurface());
try {
mMediaPlayer.setDataSource(getApplicationContext(), Uri.parse(videoFilePath));
mMediaPlayer.setLooping(true);
mMediaPlayer.setVolume(0, 0);
mMediaPlayer.prepare();
mMediaPlayer.start();
} catch (IOException e) {
e.printStackTrace();
}
}
- 在onSurfaceDestroyed()方法中释放MediaPlayer。
@Override
public void onSurfaceDestroyed(SurfaceHolder holder) {
super.onSurfaceDestroyed(holder);
mMediaPlayer.release();
mMediaPlayer = null;
}
- 在onVisibilityChanged()方法中,桌面显示时播放视频,不可见时暂停播放。
@Override
public void onVisibilityChanged(boolean visible) {
if (visible) {
mMediaPlayer.start();
} else {
mMediaPlayer.pause();
}
}
到这里其实我们已经将视频功能做完啦是不是挺简单的
- 但是,,,我们要的效果是桌面和锁屏都是视频,原生ROM测试是可以滴,国产ROM都改为他们自家的主题设置了😂(我冇眼睇!)
- 接下来就是写Activity,点按钮设置我们的动态壁纸~
-
新建Activity,这一堆操作就不写了,只贴重点。
-
思路是这样的,点击按钮,调用系统图库选择视频,拿到视频地址后,调用系统设置动态壁纸的Api,将视频地址传过去,再广播通知我们的Service的MediaPlayer切换视频~
- 首先是我们调用系统图库选择文件,这里我做了抽取,将选择工作交给了一个叫FileChooseCompat的类,将选择资源的工作交给它。然后写了一个ChooseFileCallback回调接口,回调选择结果到Activity。
public class FileChooseCompat {
/**
* 资源类型分隔符
*/
private static final String RESOURCES_TYPE_SEPARATOR = ";";
private FragmentActivity mActivity;
/**
* 资源类型
*/
public enum ResourceType {
/**
* 无类型限制
*/
GENERAL("0", "*/*", "无类型限制"),
/**
* 选择图片
*/
IMAGE("1", "image/*", "选择图片"),
/**
* 选择音频
*/
AUDIO("2", "audio/*", "选择音频"),
/**
* 选择视频,(mp4 3gp 是android支持的视频格式)
*/
VIDEO("3", "video/*", "选择视频");
private String mType;
private String mIntentType;
private String mDesc;
ResourceType(String type, String intentType, String desc) {
mType = type;
mIntentType = intentType;
mDesc = desc;
}
public String getType() {
return mType;
}
public String getIntentType() {
return mIntentType;
}
public String getDesc() {
return mDesc;
}
@Override
public String toString() {
return this.mType;
}
}
public interface ChooseFileCallback {
void onChoose(Uri uri, String filePath);
void onChooseCancel();
}
public static class SimpleChooseFileCallback implements ChooseFileCallback {
@Override
public void onChoose(Uri uri, String filePath) {
}
@Override
public void onChooseCancel() {
}
}
private FileChooseCompat() {
}
private FileChooseCompat(FragmentActivity activity) {
this.mActivity = activity;
}
public static FileChooseCompat create(FragmentActivity activity) {
return new FileChooseCompat(activity);
}
/**
* 选择图片
*/
public void startChooseImageFile(ChooseFileCallback chooseFileCallback) {
startChooseFile(chooseFileCallback, ResourceType.IMAGE);
}
/**
* 选择音频
*/
public void startChooseAudioFile(ChooseFileCallback chooseFileCallback) {
startChooseFile(chooseFileCallback, ResourceType.AUDIO);
}
/**
* 选择视频
*/
public void startChooseVideoFile(ChooseFileCallback chooseFileCallback) {
startChooseFile(chooseFileCallback, ResourceType.VIDEO);
}
/**
* 选择图片和视频
*/
public void startChooseImageAndVideo(ChooseFileCallback chooseFileCallback) {
startChooseFile(chooseFileCallback, ResourceType.IMAGE, ResourceType.VIDEO);
}
private void startChooseFile(ChooseFileCallback chooseFileCallback, final ResourceType... type) {
FileChooseDelegateFragment delegateFragment = DelegateFragmentFinder
.getInstance()
.find(mActivity, FileChooseDelegateFragment.class);
//拼接多个资源类型
StringBuilder builder = new StringBuilder();
for (FileChooseCompat.ResourceType resourcesType : type) {
builder.append(resourcesType.getIntentType());
builder.append(RESOURCES_TYPE_SEPARATOR);
}
String typeResult = builder.toString();
typeResult = typeResult.substring(0, typeResult.length() - 1);
delegateFragment.startChooseFile(chooseFileCallback, typeResult);
}
}
- 调用系统图库Api,需要复写onActivityResult()拿到选择文件的地址,我这里将onActivityResult()的回调给代理Fragment。
public class FileChooseDelegateFragment extends BaseDelegateFragment {
private static final int REQUEST_CHOOSE_FILE = 100;
/**
* 选择回调
*/
private FileChooseCompat.ChooseFileCallback mChooseFileCallback;
private UriToPathFactory mUriToPathFactory = new UriToPathFactory();
/**
* 选择文件
*
* @param chooseFileCallback 选择的回调
* @param type 资源类型
*/
public void startChooseFile(FileChooseCompat.ChooseFileCallback chooseFileCallback, final String type) {
this.mChooseFileCallback = chooseFileCallback;
runTaskOnStart(new LifecycleTask() {
@Override
public void execute(BaseDelegateFragment delegateFragment) {
//选择文件,调用系统的文件管理
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType(type);
intent.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(intent, REQUEST_CHOOSE_FILE);
}
});
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {//选择文件返回
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == Activity.RESULT_CANCELED) {
switch (requestCode) {
case REQUEST_CHOOSE_FILE:
if (mChooseFileCallback != null) {
mChooseFileCallback.onChooseCancel();
}
break;
default:
break;
}
} else if (resultCode == Activity.RESULT_OK) {
switch (requestCode) {
case REQUEST_CHOOSE_FILE:
Uri uri = data.getData();
String chooseFilePath = mUriToPathFactory.startUriToPath(getContext(), uri);
if (mChooseFileCallback != null) {
mChooseFileCallback.onChoose(uri, chooseFilePath);
}
break;
default:
break;
}
}
}
}
- Activity按钮点击事件开始调用~
Button chooseVideoBtn = findViewById(R.id.choose_video_btn);
chooseVideoBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
FileChooseCompat.create(ChooseVideoWallpaperActivity.this).startChooseVideoFile(new FileChooseCompat.SimpleChooseFileCallback() {
@Override
public void onChoose(Uri uri, String filePath) {
//这里将文件的filePath交给Service处理。
VideoLiveWallpaper.setWallPaper(ChooseVideoWallpaperActivity.this, filePath);
}
});
}
});
- 上面的VideoLiveWallpaper.setWallPaper(),将视频地址交给Service处理。
这里有个小问题,估计谷歌在设计这个动态壁纸时,没有提供是否设置成功的回调,就是说跳转到系统的设置界面后,我们就没法得知用户是设置了我们的壁纸,还是点了返回键返回...所以这里,我的处理是,设置前将文件地址保存到sp,再跳转到系统的设置界面,再广播更新视频地址。为什么在这里就要更新视频地址?因为系统的设置页面,在选择你的动态地址后,会跳转到一个预览界面,播放你的动态壁纸,而上面的动态壁纸也是使用我们前面返回Engine类,并且每次进入该界面,Engine类不一定会重建,有时候会使用之前的实例,而且Engine类创建时,也没有提供一个Intent设置一个Bundle参数带这个地址过去😂完全不受自己控制...所以这里就需要进行广播更新了(😂总感觉这块没设计好...)。
- 由于要使用到广播,广播的Action,Bundle的Key自然要写一下~发送广播的方法再封装一下到内部。
public static final String VIDEO_PARAMS_CONTROL_ACTION = "me.wally.arch.livewallpaper.VideoLiveWallpaper";
public static final String KEY_ACTION = "action";
public static final int ACTION_UPDATE_VIDEO_FILE_PATH = 112;
/**
* 跳转到动态壁纸设置页面
*/
public static void setWallPaper(Context context, String videoFilePath) {
//因为没有提供是否设置成功的回调,只能当选择了就是设置成功
saveVideoPath(videoFilePath);
startNewWallpaper(context);
updateWallpaperVideo(context);
}
private static void saveVideoPath(String videoFilePath) {
//将视频地址保存,后面广播更新时再读取
PropertyHelper.setProperty(KEY_WALLPAPER_VIDEO_DATA_SOURCE_FILE_PATH, videoFilePath);
}
/**
* 跳转到系统的动态壁纸设置界面
*/
private static void startNewWallpaper(Context context) {
Intent intent = new Intent(WallpaperManager.ACTION_CHANGE_LIVE_WALLPAPER);
intent.putExtra(WallpaperManager.EXTRA_LIVE_WALLPAPER_COMPONENT, new ComponentName(context, VideoLiveWallpaper.class));
context.startActivity(intent);
}
private static void updateWallpaperVideo(Context context) {
//发送广播更新
Intent intent = new Intent(VideoLiveWallpaper.VIDEO_PARAMS_CONTROL_ACTION);
intent.putExtra(VideoLiveWallpaper.KEY_ACTION, VideoLiveWallpaper.ACTION_UPDATE_VIDEO_FILE_PATH);
context.sendBroadcast(intent);
}
- 在VideoEngine的onCreate()方法中,注册广播,在onDestroy()方法中,注销广播。这样我们就完成了选择视频->视频播放~
@Override
public void onCreate(SurfaceHolder surfaceHolder) {
super.onCreate(surfaceHolder);
IntentFilter intentFilter = new IntentFilter(VIDEO_PARAMS_CONTROL_ACTION);
registerReceiver(mVideoParamsControlReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int action = intent.getIntExtra(KEY_ACTION, -1);
switch (action) {
case ACTION_UPDATE_VIDEO_FILE_PATH:
//更新视频播放源
String videoFilePath = getVideoPath();
try {
mMediaPlayer.setDataSource(getApplicationContext(), Uri.parse(videoFilePath));
} catch (IOException e) {
e.printStackTrace();
}
break;
default:
break;
}
}
}, intentFilter);
}
@Override
public void onDestroy() {
unregisterReceiver(mVideoParamsControlReceiver);
super.onDestroy();
}
视频设置成功-桌面.png
视频设置成功-锁屏.png
你以为这样就完了吗?不,还有点小缺陷,写完以上代码,你会发现,选择视频后,咋么视频的声音都出来了...回个桌面还有声音,吓人呢😂。其实这是正常滴,视频播放就肯定会播声音啦,那我们我还需要做一步静音操作~
- 设置静音的方法也很简单,给我们MediaPlayer调用setVolume()方法,有2个参数,一个是左声道,一个是右声道,要静音则都设置0,不静音则1.0f。
//设置不静音
mMediaPlayer.setVolume(1.0f, 1.0f);
//设置静音
mMediaPlayer.setVolume(0, 0);
- 设置同样使用广播通知。提供setVolume()方法给外部使用,毕竟不能一棒子打死嘛,要是需求改为不静音呢?记得产品的话,千万不要信,记住!代码不要写死~噢,这次需求方是女朋友👩。
public static final int ACTION_VOICE_SILENCE = 110;
public static final int ACTION_VOICE_NORMAL = 111;
/**
* 设置是否静音
*
* @param isSilence 是否静音
*/
public static void setVolume(Context context, boolean isSilence) {
Intent intent = new Intent(VideoLiveWallpaper.VIDEO_PARAMS_CONTROL_ACTION);
if (isSilence) {
intent.putExtra(VideoLiveWallpaper.KEY_ACTION, VideoLiveWallpaper.ACTION_VOICE_SILENCE);
} else {
intent.putExtra(VideoLiveWallpaper.KEY_ACTION, VideoLiveWallpaper.ACTION_VOICE_NORMAL);
}
context.sendBroadcast(intent);
}
- 修改VideoEngine类的onCreate()方法,广播接收加上设置静音、非静音的操作。
@Override
public void onCreate(SurfaceHolder surfaceHolder) {
super.onCreate(surfaceHolder);
IntentFilter intentFilter = new IntentFilter(VIDEO_PARAMS_CONTROL_ACTION);
registerReceiver(mVideoParamsControlReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int action = intent.getIntExtra(KEY_ACTION, -1);
switch (action) {
case ACTION_VOICE_NORMAL:
mMediaPlayer.setVolume(1.0f, 1.0f);
break;
case ACTION_VOICE_SILENCE:
mMediaPlayer.setVolume(0, 0);
break;
case ACTION_UPDATE_VIDEO_FILE_PATH:
...
break;
default:
break;
}
}
}, intentFilter);
}
- 最后Activity,设置一个切换静音、非静音CheckBox。
CheckBox voiceCheckBox = findViewById(R.id.voice_check_box);
boolean isVolume = getVideoLiveWallpaperSilence();
voiceCheckBox.setChecked(isVolume);
voiceCheckBox.setOnCheckedChangeListener(
new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
//静音
setVideoLiveWallpaperSilence(true);
} else {
setVideoLiveWallpaperSilence(false);
}
}
});
- 这里其实VideoEngine的onCreate()还要做个恢复操作,为毛呢?因为国产ROM杀进程都杀得精光,一个内存清理就把我们的视频壁纸给杀没了,毕竟我们的视频动态壁纸都是Service~记得吗,我们设置了一个Service,所以桌面的播放操作都在Service,Service死了,那桌面的SufaceView也就跟我们的MediaPlayer解绑了😂...(是否静音也记录到SP,方便恢复上次的设置...这里对国产ROM杀精光的做法无法吐槽...)
@Override
public void onCreate(SurfaceHolder surfaceHolder) {
super.onCreate(surfaceHolder);
boolean isVolume = PropertyHelper.getProperty(Const.Key.KEY_WALLPAPER_VOLUME_IS_SILENCE, true);
VideoLiveWallpaper.setVolume(this.getApplicationContext(), isVolume);
}
总结
- 通过官方Api,我们可以设置一个动态壁纸,但是国产ROM做了阉割...只能设置桌面,锁屏基本是他们的主题设置,无奈...
- 感觉官方Api一开始没设置好,跳转到设置页面后,也没有回调给我们,是否用户设置了我们的动态壁纸。。强制保存地址,通过广播刷新,也是缺点,万一用户选择完视频又不设置呢😂
- 国产ROM清理内存,Service就被杀了...动态壁纸就没了,只能引导用户去给我们的App设置白名单,清理时忽略我们的App了。
- 最后我的视频动态壁纸做好了可以满心欢喜给女朋友炫耀了
- 最后发现...某音右上角其实可以设置当前播放的视频为桌面动态壁纸(他们也是只能设置桌面,不能锁屏也有)...😂我。。。
网友评论