美文网首页Android之旅
动手做视频动态壁纸~

动手做视频动态壁纸~

作者: h2coder | 来源:发表于2018-12-23 00:38 被阅读167次

    某天,女朋友在刷某音,突然看到有视频作为锁屏,十分崇拜,撒娇的眼生看着我...看来这个需求躲不过了,身为安卓仔,这有什么的,开干!

    动态壁纸-设置页面.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();
                }
            }
    

    到这里其实我们已经将视频功能做完啦是不是挺简单的

    1. 但是,,,我们要的效果是桌面和锁屏都是视频,原生ROM测试是可以滴,国产ROM都改为他们自家的主题设置了😂(我冇眼睇!)
    2. 接下来就是写Activity,点按钮设置我们的动态壁纸~
    • 新建Activity,这一堆操作就不写了,只贴重点。

    • 思路是这样的,点击按钮,调用系统图库选择视频,拿到视频地址后,调用系统设置动态壁纸的Api,将视频地址传过去,再广播通知我们的Service的MediaPlayer切换视频~

    1. 首先是我们调用系统图库选择文件,这里我做了抽取,将选择工作交给了一个叫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);
        }
    }
    
    1. 调用系统图库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;
                }
            }
        }
    }
    
    1. 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);
                        }
                    });
                }
            });
    
    1. 上面的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);
        }
    
    1. 在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了。
    • 最后我的视频动态壁纸做好了可以满心欢喜给女朋友炫耀了
    • 最后发现...某音右上角其实可以设置当前播放的视频为桌面动态壁纸(他们也是只能设置桌面,不能锁屏也有)...😂我。。。

    相关文章

      网友评论

        本文标题:动手做视频动态壁纸~

        本文链接:https://www.haomeiwen.com/subject/zptgkqtx.html