06.播放器UI抽取封装
目录介绍
- 01.视频播放器UI封装需求
- 02.播放器UI架构图
- 03.如何分离播放和UI分离
- 04.VideoPlayer如何实现
- 05.VideoController实现
- 06.播放Player和UI通信
- 07.如何添加自定义播放视图
- 08.关于播放器视图层级
- 09.视频播放器重力感应监听
00.视频播放器通用框架
- 基础封装视频播放器player,可以在ExoPlayer、MediaPlayer,声网RTC视频播放器内核,原生MediaPlayer可以自由切换
- 对于视图状态切换和后期维护拓展,避免功能和业务出现耦合。比如需要支持播放器UI高度定制,而不是该lib库中UI代码
- 针对视频播放,音频播放,播放回放,以及视频直播的功能。使用简单,代码拓展性强,封装性好,主要是和业务彻底解耦,暴露接口监听给开发者处理业务具体逻辑
- 该播放器整体架构:播放器内核(自由切换) + 视频播放器 + 边播边缓存 + 高度定制播放器UI视图层
- 项目地址:github.com/yangchong21…
- 关于视频播放器整体功能介绍文档:juejin.cn/post/688345…
01.视频播放器UI封装需求
- 发展中遇到的问题
- 播放器可支持多种场景下的播放,多个产品会用到同一个播放器,这样就会带来一个问题,一个播放业务播放器状态发生变化,其他播放业务必须同步更新播放状态,各个播放业务之间互相交叉,随着播放业务的增多,开发和维护成本会急剧增加, 导致后续开发不可持续。
- 播放器内核和UI层耦合
- 也就是说视频player和ui操作柔和到了一起,尤其是两者之间的交互。比如播放中需要更新UI进度条,播放异常需要显示异常UI,都比较难处理播放器状态变化更新UI操作
- UI难以自定义或者修改麻烦
- 比如常见的视频播放器,会把视频各种视图写到xml中,这种方式在后期代码会很大,而且改动一个小的布局,则会影响大。这样到后期往往只敢加代码,而不敢删除代码……
- 有时候难以适应新的场景,比如添加一个播放广告,老师开课,或者视频引导业务需求,则需要到播放器中写一堆业务代码。迭代到后期,违背了开闭原则,视频播放器需要做到和业务分离
- 视频播放器结构需要清晰
- 这个是指该视频播放器能否看了文档后快速上手,知道封装的大概流程。方便后期他人修改和维护,因此需要将视频播放器功能分离。比如切换内核+视频播放器(player+controller+view)
- 一定要解耦合
- 播放器player与视频UI解耦:支持添加自定义视频视图,比如支持添加自定义广告,新手引导,或者视频播放异常等视图,这个需要较强的拓展性
- 适合多种业务场景
- 比如适合播放单个视频,多个视频,以及列表视频,或者类似抖音那种一个页面一个视频,还有小窗口播放视频。也就是适合大多数业务场景
02.播放器UI架构图
03.如何分离播放和UI分离
- 方便播放业务发生变化
- 播放状态变化是导致不同播放业务场景之间交叉同步,解除播放业务对播放器的直接操控,采用接口监听进行解耦。比如:player+controller+interface
- 关于视频播放器
- 定义一个视频播放器InterVideoPlayer接口,操作视频播放,暂停,缓冲,进度设置,设置播放模式等多种操作。
- 然后写一个播放器接口的具体实现类,在这个里面拿到内核播放器player,然后做相关的实现操作。
- 关于视频视图View
- 定义一个视图InterVideoController接口,主要负责视图显示/隐藏,播放进度,锁屏,状态栏等操作。
- 然后写一个播放器视图接口的具体实现类,在这里里面inflate视图操作,然后接口方法实现,为了方便后期开发者自定义view,因此需要addView操作,将添加进来的视图用map集合装起来。
- 播放器player和controller交互
- 在player中创建BaseVideoController对象,这个时候需要把controller添加到播放器中,这个时候有两个要点特别重要,需要把播放器状态监听,和播放模式监听传递给控制器
- setPlayState设置视频播放器播放逻辑状态,主要是播放缓冲,加载,播放中,暂停,错误,完成,异常,播放进度等多个状态,方便控制器做UI更新操作
- setPlayerState设置视频播放切换模式状态,主要是普通模式,小窗口模式,正常模式三种其中一种,方便控制器做UI更新
- 播放器player和view交互
- 这块非常关键,举个例子,视频播放失败需要显示控制层的异常视图View;播放视频初始化需要显示loading,然后更新UI播放进度条等。都是播放器和视图层交互
- 可以定义一个类,同时实现InterVideoPlayer接口和InterVideoController接口,这个时候会重新这两个接口所有的方法。此类的目的是为了在InterControlView接口实现类中既能调用VideoPlayer的api又能调用BaseVideoController的api
- 如何添加自定义播放器视图
- 添加了自定义播放器视图,比如添加视频广告,可以选择跳过,选择播放暂停。那这个视图view,肯定是需要操作player或者获取player的状态的。这个时候就需要暴露监听视频播放的状态接口监听
- 首先定义一个InterControlView接口,也就是说所有自定义视频视图view需要实现这个接口,该接口中的核心方法有:绑定视图到播放器,视图显示隐藏变化监听,播放状态监听,播放模式监听,进度监听,锁屏监听等
- 在BaseVideoController中的状态监听中,通过InterControlView接口对象就可以把播放器的状态传递到子类中
04.VideoPlayer如何实现
- 代码如下所示,省略了部分代码,具体看demo
public class VideoPlayer<P extends AbstractVideoPlayer> extends FrameLayout implements InterVideoPlayer, VideoPlayerListener { private Context mContext; /** * 播放器 */ protected P mMediaPlayer; /** * 实例化播放核心 */ protected PlayerFactory<P> mPlayerFactory; /** * 控制器 */ @Nullable protected BaseVideoController mVideoController; /** * 真正承载播放器视图的容器 */ protected FrameLayout mPlayerContainer; public VideoPlayer(@NonNull Context context) { this(context, null); } public VideoPlayer(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public VideoPlayer(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mContext = context; init(attrs); } private void init(AttributeSet attrs) { BaseToast.init(mContext.getApplicationContext()); //读取全局配置 initConfig(); //读取xml中的配置,并综合全局配置 initAttrs(attrs); initView(); } private void initConfig() { VideoPlayerConfig config = VideoViewManager.getConfig(); mEnableAudioFocus = config.mEnableAudioFocus; mProgressManager = config.mProgressManager; mPlayerFactory = config.mPlayerFactory; mCurrentScreenScaleType = config.mScreenScaleType; mRenderViewFactory = config.mRenderViewFactory; //设置是否打印日志 VideoLogUtils.setIsLog(config.mIsEnableLog); } @Override protected Parcelable onSaveInstanceState() { VideoLogUtils.d("onSaveInstanceState: " + mCurrentPosition); //activity切到后台后可能被系统回收,故在此处进行进度保存 saveProgress(); return super.onSaveInstanceState(); } private void initAttrs(AttributeSet attrs) { TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.VideoPlayer); mEnableAudioFocus = a.getBoolean(R.styleable.VideoPlayer_enableAudioFocus, mEnableAudioFocus); mIsLooping = a.getBoolean(R.styleable.VideoPlayer_looping, false); mCurrentScreenScaleType = a.getInt(R.styleable.VideoPlayer_screenScaleType, mCurrentScreenScaleType); mPlayerBackgroundColor = a.getColor(R.styleable.VideoPlayer_playerBackgroundColor, Color.BLACK); a.recycle(); } /** * 初始化播放器视图 */ protected void initView() { mPlayerContainer = new FrameLayout(getContext()); //设置背景颜色,目前设置为纯黑色 mPlayerContainer.setBackgroundColor(mPlayerBackgroundColor); LayoutParams params = new LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); //将布局添加到该视图中 this.addView(mPlayerContainer, params); } /** * 设置控制器,传null表示移除控制器 * @param mediaController controller */ public void setController(@Nullable BaseVideoController mediaController) { mPlayerContainer.removeView(mVideoController); mVideoController = mediaController; if (mediaController != null) { mediaController.setMediaPlayer(this); LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); mPlayerContainer.addView(mVideoController, params); } } /** * 开始播放,注意:调用此方法后必须调用{@link #release()}释放播放器,否则会导致内存泄漏 */ @Override public void start() { if (mVideoController==null){ //在调用start方法前,请先初始化视频控制器,调用setController方法 throw new VideoException(VideoException.CODE_NOT_SET_CONTROLLER, "Controller must not be null , please setController first"); } boolean isStarted = false; if (isInIdleState() || isInStartAbortState()) { isStarted = startPlay(); } else if (isInPlaybackState()) { startInPlaybackState(); isStarted = true; } if (isStarted) { mPlayerContainer.setKeepScreenOn(true); if (mAudioFocusHelper != null){ mAudioFocusHelper.requestFocus(); } } } /** * 第一次播放 * @return 是否成功开始播放 */ protected boolean startPlay() { //如果要显示移动网络提示则不继续播放 if (showNetWarning()) { //中止播放 setPlayState(ConstantKeys.CurrentState.STATE_START_ABORT); return false; } //监听音频焦点改变 if (mEnableAudioFocus) { mAudioFocusHelper = new AudioFocusHelper(this); } //读取播放进度 if (mProgressManager != null) { mCurrentPosition = mProgressManager.getSavedProgress(mUrl); } initPlayer(); addDisplay(); startPrepare(false); return true; } /** * 初始化播放器 */ protected void initPlayer() { //通过工厂模式创建对象 mMediaPlayer = mPlayerFactory.createPlayer(mContext); mMediaPlayer.setPlayerEventListener(this); setInitOptions(); mMediaPlayer.initPlayer(); setOptions(); } /** * 是否显示移动网络提示,可在Controller中配置 */ protected boolean showNetWarning() { //播放本地数据源时不检测网络 if (VideoPlayerHelper.instance().isLocalDataSource(mUrl,mAssetFileDescriptor)){ return false; } return mVideoController != null && mVideoController.showNetWarning(); } /** * 初始化之前的配置项 */ protected void setInitOptions() { } /** * 初始化之后的配置项 */ protected void setOptions() { //设置是否循环播放 mMediaPlayer.setLooping(mIsLooping); } /** * 初始化视频渲染View */ protected void addDisplay() { if (mRenderView != null) { mPlayerContainer.removeView(mRenderView.getView()); mRenderView.release(); } //创建TextureView对象 mRenderView = mRenderViewFactory.createRenderView(mContext); //绑定mMediaPlayer对象 mRenderView.attachToPlayer(mMediaPlayer); LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER); mPlayerContainer.addView(mRenderView.getView(), 0, params); } /** * 开始准备播放(直接播放) */ protected void startPrepare(boolean reset) { if (reset) { mMediaPlayer.reset(); //重新设置option,media player reset之后,option会失效 setOptions(); } if (prepareDataSource()) { mMediaPlayer.prepareAsync(); setPlayState(ConstantKeys.CurrentState.STATE_PREPARING); setPlayerState(isFullScreen() ? ConstantKeys.PlayMode.MODE_FULL_SCREEN : isTinyScreen() ? ConstantKeys.PlayMode.MODE_TINY_WINDOW : ConstantKeys.PlayMode.MODE_NORMAL); } } /** * 设置播放数据 * @return 播放数据是否设置成功 */ protected boolean prepareDataSource() { if (mAssetFileDescriptor != null) { mMediaPlayer.setDataSource(mAssetFileDescriptor); return true; } else if (!TextUtils.isEmpty(mUrl)) { mMediaPlayer.setDataSource(mUrl, mHeaders); return true; } return false; } /** * 视频播放出错回调 */ @Override public void onError() { mPlayerContainer.setKeepScreenOn(false); setPlayState(ConstantKeys.CurrentState.STATE_ERROR); VideoPlayerConfig config = VideoViewManager.getConfig(); if (config!=null && config.mBuriedPointEvent!=null){ //相当于进入了视频页面 if (PlayerUtils.isConnected(mContext)){ config.mBuriedPointEvent.onError(mUrl,false); } else { config.mBuriedPointEvent.onError(mUrl,true); } } } /** * 视频播放完成回调 */ @Override public void onCompletion() { mPlayerContainer.setKeepScreenOn(false); mCurrentPosition = 0; if (mProgressManager != null) { //播放完成,清除进度 mProgressManager.saveProgress(mUrl, 0); } setPlayState(ConstantKeys.CurrentState.STATE_BUFFERING_PLAYING); VideoPlayerConfig config = VideoViewManager.getConfig(); if (config!=null && config.mBuriedPointEvent!=null){ //视频播放完成 config.mBuriedPointEvent.playerCompletion(mUrl); } } @Override public void onInfo(int what, int extra) { switch (what) { case PlayerConstant.MEDIA_INFO_BUFFERING_START: setPlayState(ConstantKeys.CurrentState.STATE_BUFFERING_PAUSED); break; case PlayerConstant.MEDIA_INFO_BUFFERING_END: setPlayState(ConstantKeys.CurrentState.STATE_COMPLETED); break; case PlayerConstant.MEDIA_INFO_VIDEO_RENDERING_START: // 视频开始渲染 setPlayState(ConstantKeys.CurrentState.STATE_PLAYING); if (mPlayerContainer.getWindowVisibility() != VISIBLE) { pause(); } break; case PlayerConstant.MEDIA_INFO_VIDEO_ROTATION_CHANGED: if (mRenderView != null) mRenderView.setVideoRotation(extra); break; } } /** * 视频缓冲完毕,准备开始播放时回调 */ @Override public void onPrepared() { setPlayState(ConstantKeys.CurrentState.STATE_PREPARED); if (mCurrentPosition > 0) { seekTo(mCurrentPosition); } } /** * 获取当前播放器的状态 */ public int getCurrentPlayerState() { return mCurrentPlayerState; } /** * 向Controller设置播放状态,用于控制Controller的ui展示 * 这里使用注解限定符,不要使用1,2这种直观数字,不方便知道意思 * 播放状态,主要是指播放器的各种状态 * -1 播放错误 * 0 播放未开始 * 1 播放准备中 * 2 播放准备就绪 * 3 正在播放 * 4 暂停播放 * 5 正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,缓冲区数据足够后恢复播放) * 6 暂停缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,此时暂停播放器,继续缓冲,缓冲区数据足够后恢复暂停 * 7 播放完成 * 8 开始播放中止 */ protected void setPlayState(@ConstantKeys.CurrentStateType int playState) { mCurrentPlayState = playState; if (mVideoController != null) { mVideoController.setPlayState(playState); } if (mOnStateChangeListeners != null) { for (OnVideoStateListener l : PlayerUtils.getSnapshot(mOnStateChangeListeners)) { if (l != null) { l.onPlayStateChanged(playState); } } } } /** * 向Controller设置播放器状态,包含全屏状态和非全屏状态 * 播放模式 * 普通模式,小窗口模式,正常模式三种其中一种 * MODE_NORMAL 普通模式 * MODE_FULL_SCREEN 全屏模式 * MODE_TINY_WINDOW 小屏模式 */ protected void setPlayerState(@ConstantKeys.PlayModeType int playerState) { mCurrentPlayerState = playerState; if (mVideoController != null) { mVideoController.setPlayerState(playerState); } if (mOnStateChangeListeners != null) { for (OnVideoStateListener l : PlayerUtils.getSnapshot(mOnStateChangeListeners)) { if (l != null) { l.onPlayerStateChanged(playerState); } } } } /** * OnStateChangeListener的空实现。用的时候只需要重写需要的方法 */ public static class SimpleOnStateChangeListener implements OnVideoStateListener { @Override public void onPlayerStateChanged(@ConstantKeys.PlayModeType int playerState) {} @Override public void onPlayStateChanged(int playState) {} } /** * 添加一个播放状态监听器,播放状态发生变化时将会调用。 */ public void addOnStateChangeListener(@NonNull OnVideoStateListener listener) { if (mOnStateChangeListeners == null) { mOnStateChangeListeners = new ArrayList<>(); } mOnStateChangeListeners.add(listener); } /** * 移除某个播放状态监听 */ public void removeOnStateChangeListener(@NonNull OnVideoStateListener listener) { if (mOnStateChangeListeners != null) { mOnStateChangeListeners.remove(listener); } } /** * 设置一个播放状态监听器,播放状态发生变化时将会调用, * 如果你想同时设置多个监听器,推荐 {@link #addOnStateChangeListener(OnVideoStateListener)}。 */ public void setOnStateChangeListener(@NonNull OnVideoStateListener listener) { if (mOnStateChangeListeners == null) { mOnStateChangeListeners = new ArrayList<>(); } else { mOnStateChangeListeners.clear(); } mOnStateChangeListeners.add(listener); } /** * 移除所有播放状态监听 */ public void clearOnStateChangeListeners() { if (mOnStateChangeListeners != null) { mOnStateChangeListeners.clear(); } } /** * 改变返回键逻辑,用于activity */ public boolean onBackPressed() { return mVideoController != null && mVideoController.onBackPressed(); } /**-----------------------------暴露api方法--------------------------------------**/ /**-----------------------------暴露api方法--------------------------------------**/ public void setVideoBuilder(VideoPlayerBuilder videoBuilder){ if (mPlayerContainer==null || videoBuilder==null){ return; } //设置视频播放器的背景色 mPlayerContainer.setBackgroundColor(videoBuilder.mColor); //设置小屏的宽高 if (videoBuilder.mTinyScreenSize!=null && videoBuilder.mTinyScreenSize.length>0){ mTinyScreenSize = videoBuilder.mTinyScreenSize; } //一开始播放就seek到预先设置好的位置 if (videoBuilder.mCurrentPosition>0){ this.mCurrentPosition = videoBuilder.mCurrentPosition; } //是否开启AudioFocus监听, 默认开启 this.mEnableAudioFocus = videoBuilder.mEnableAudioFocus; } }
05.VideoController实现
- 代码如下所示,代码太长,省略部分代码,具体看demo
public abstract class BaseVideoController extends FrameLayout implements InterVideoController, OrientationHelper.OnOrientationChangeListener { //播放器包装类,集合了MediaPlayerControl的api和IVideoController的api protected ControlWrapper mControlWrapper; public BaseVideoController(@NonNull Context context) { //创建 this(context, null); } public BaseVideoController(@NonNull Context context, @Nullable AttributeSet attrs) { //创建 this(context, attrs, 0); } public BaseVideoController(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { super(context, attrs, defStyleAttr); initView(context); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mShowAnim != null){ mShowAnim.cancel(); mShowAnim = null; } if (mHideAnim != null){ mHideAnim.cancel(); mHideAnim = null; } } @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); if (mControlWrapper.isPlaying() && (mEnableOrientation || mControlWrapper.isFullScreen())) { if (hasWindowFocus) { postDelayed(new Runnable() { @Override public void run() { mOrientationHelper.enable(); } }, 800); } else { mOrientationHelper.disable(); } } } protected void initView(Context context) { if (getLayoutId() != 0) { LayoutInflater.from(getContext()).inflate(getLayoutId(), this, true); } mOrientationHelper = new OrientationHelper(context.getApplicationContext()); mEnableOrientation = VideoViewManager.getConfig().mEnableOrientation; mAdaptCutout = VideoViewManager.getConfig().mAdaptCutout; mShowAnim = new AlphaAnimation(0f, 1f); mShowAnim.setDuration(300); mHideAnim = new AlphaAnimation(1f, 0f); mHideAnim.setDuration(300); mActivity = PlayerUtils.scanForActivity(context); } /** * 设置控制器布局文件,子类必须实现 */ protected abstract int getLayoutId(); /** * 重要:此方法用于将{@link VideoPlayer} 和控制器绑定 */ @CallSuper public void setMediaPlayer(InterVideoPlayer mediaPlayer) { mControlWrapper = new ControlWrapper(mediaPlayer, this); //绑定ControlComponent和Controller for (Map.Entry<InterControlView, Boolean> next : mControlComponents.entrySet()) { InterControlView component = next.getKey(); component.attach(mControlWrapper); } //开始监听设备方向 mOrientationHelper.setOnOrientationChangeListener(this); } /** * 添加控制组件,最后面添加的在最下面,合理组织添加顺序,可让ControlComponent位于不同的层级 */ public void addControlComponent(InterControlView... component) { for (InterControlView item : component) { addControlComponent(item, false); } } /** * 添加控制组件,最后面添加的在最下面,合理组织添加顺序,可让ControlComponent位于不同的层级 * * @param isPrivate 是否为独有的组件,如果是就不添加到控制器中 */ public void addControlComponent(InterControlView component, boolean isPrivate) { mControlComponents.put(component, isPrivate); if (mControlWrapper != null) { component.attach(mControlWrapper); } View view = component.getView(); if (view != null && !isPrivate) { addView(view, 0); } } /** * 移除控制组件 */ public void removeControlComponent(InterControlView component) { removeView(component.getView()); mControlComponents.remove(component); } /** * 移除所有的组件 */ public void removeAllControlComponent() { for (Map.Entry<InterControlView, Boolean> next : mControlComponents.entrySet()) { removeView(next.getKey().getView()); } mControlComponents.clear(); } public void removeAllPrivateComponents() { Iterator<Map.Entry<InterControlView, Boolean>> it = mControlComponents.entrySet().iterator(); while (it.hasNext()) { Map.Entry<InterControlView, Boolean> next = it.next(); if (next.getValue()) { it.remove(); } } } /** * {@link VideoPlayer}调用此方法向控制器设置播放状态。 * 这里使用注解限定符,不要使用1,2这种直观数字,不方便知道意思 * 播放状态,主要是指播放器的各种状态 * -1 播放错误 * 0 播放未开始 * 1 播放准备中 * 2 播放准备就绪 * 3 正在播放 * 4 暂停播放 * 5 正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,缓冲区数据足够后恢复播放) * 6 暂停缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,此时暂停播放器,继续缓冲,缓冲区数据足够后恢复暂停 * 7 播放完成 * 8 开始播放中止 */ @CallSuper public void setPlayState(@ConstantKeys.CurrentStateType int playState) { //设置播放器的状态 handlePlayStateChanged(playState); } /** * {@link VideoPlayer}调用此方法向控制器设置播放器状态 * 播放模式 * 普通模式,小窗口模式,正常模式三种其中一种 * MODE_NORMAL 普通模式 * MODE_FULL_SCREEN 全屏模式 * MODE_TINY_WINDOW 小屏模式 */ @CallSuper public void setPlayerState(@ConstantKeys.PlayModeType final int playerState) { //调用此方法向控制器设置播放器状态 handlePlayerStateChanged(playerState); } /** * 播放和暂停 */ protected void togglePlay() { mControlWrapper.togglePlay(); } /** * 横竖屏切换 */ protected void toggleFullScreen() { if (PlayerUtils.isActivityLiving(mActivity)){ mControlWrapper.toggleFullScreen(mActivity); } } /** * 子类中请使用此方法来进入全屏 * * @return 是否成功进入全屏 */ protected boolean startFullScreen() { if (!PlayerUtils.isActivityLiving(mActivity)) { return false; } mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); mControlWrapper.startFullScreen(); return true; } /** * 子类中请使用此方法来退出全屏 * * @return 是否成功退出全屏 */ protected boolean stopFullScreen() { if (!PlayerUtils.isActivityLiving(mActivity)) { return false; } mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); mControlWrapper.stopFullScreen(); return true; } /** * 改变返回键逻辑,用于activity */ public boolean onBackPressed() { return false; } /** * 是否自动旋转, 默认不自动旋转 */ public void setEnableOrientation(boolean enableOrientation) { mEnableOrientation = enableOrientation; } private void handleVisibilityChanged(boolean isVisible, Animation anim) { if (!mIsLocked) { //没锁住时才向ControlComponent下发此事件 for (Map.Entry<InterControlView, Boolean> next : mControlComponents.entrySet()) { InterControlView component = next.getKey(); component.onVisibilityChanged(isVisible, anim); } } onVisibilityChanged(isVisible, anim); } /** * 子类重写此方法监听控制的显示和隐藏 * * @param isVisible 是否可见 * @param anim 显示/隐藏动画 */ protected void onVisibilityChanged(boolean isVisible, Animation anim) { } private void handlePlayStateChanged(int playState) { for (Map.Entry<InterControlView, Boolean> next : mControlComponents.entrySet()) { InterControlView component = next.getKey(); component.onPlayStateChanged(playState); } onPlayStateChanged(playState); } /** * 子类重写此方法并在其中更新控制器在不同播放状态下的ui */ @CallSuper protected void onPlayStateChanged(int playState) { switch (playState) { case ConstantKeys.CurrentState.STATE_IDLE: mOrientationHelper.disable(); mOrientation = 0; mIsLocked = false; mShowing = false; removeAllPrivateComponents(); break; case ConstantKeys.CurrentState.STATE_BUFFERING_PLAYING: mIsLocked = false; mShowing = false; break; case ConstantKeys.CurrentState.STATE_ERROR: mShowing = false; break; } } /** * 播放器状态改变 * @param playerState 播放器状态 */ private void handlePlayerStateChanged(int playerState) { for (Map.Entry<InterControlView, Boolean> next : mControlComponents.entrySet()) { InterControlView component = next.getKey(); component.onPlayerStateChanged(playerState); } onPlayerStateChanged(playerState); } /** * 子类重写此方法并在其中更新控制器在不同播放器状态下的ui * 普通模式,小窗口模式,正常模式三种其中一种 * MODE_NORMAL 普通模式 * MODE_FULL_SCREEN 全屏模式 * MODE_TINY_WINDOW 小屏模式 */ @CallSuper protected void onPlayerStateChanged(@ConstantKeys.PlayMode int playerState) { switch (playerState) { case ConstantKeys.PlayMode.MODE_NORMAL: //视频正常播放是设置监听 if (mEnableOrientation) { //检查系统是否开启自动旋转 mOrientationHelper.enable(); } else { //取消监听 mOrientationHelper.disable(); } if (hasCutout()) { StatesCutoutUtils.adaptCutoutAboveAndroidP(getContext(), false); } break; case ConstantKeys.PlayMode.MODE_FULL_SCREEN: //在全屏时强制监听设备方向 mOrientationHelper.enable(); if (hasCutout()) { StatesCutoutUtils.adaptCutoutAboveAndroidP(getContext(), true); } break; case ConstantKeys.PlayMode.MODE_TINY_WINDOW: //小窗口取消重力感应监听 mOrientationHelper.disable(); break; } } private void handleSetProgress(int duration, int position) { for (Map.Entry<InterControlView, Boolean> next : mControlComponents.entrySet()) { InterControlView component = next.getKey(); component.setProgress(duration, position); } setProgress(duration, position); } /** * 刷新进度回调,子类可在此方法监听进度刷新,然后更新ui * * @param duration 视频总时长 * @param position 视频当前时长 */ protected void setProgress(int duration, int position) { } private void handleLockStateChanged(boolean isLocked) { for (Map.Entry<InterControlView, Boolean> next : mControlComponents.entrySet()) { InterControlView component = next.getKey(); component.onLockStateChanged(isLocked); } onLockStateChanged(isLocked); } }
06.播放Player和UI通信
- 比如,在自定义view视图中,我想调用VideoPlayer的api又能调用BaseVideoController的api,该如何实现呢?
- 当创建了下面的对象,就可以同时拿到player和controller中的api方法呢,这里面省略一部分代码,具体看demo案例
public class ControlWrapper implements InterVideoPlayer, InterVideoController { private InterVideoPlayer mVideoPlayer; private InterVideoController mController; public ControlWrapper(@NonNull InterVideoPlayer videoPlayer, @NonNull InterVideoController controller) { mVideoPlayer = videoPlayer; mController = controller; } @Override public void start() { mVideoPlayer.start(); } @Override public void pause() { mVideoPlayer.pause(); } @Override public long getDuration() { return mVideoPlayer.getDuration(); } @Override public boolean isShowing() { return mController.isShowing(); } @Override public void setLocked(boolean locked) { mController.setLocked(locked); } }
07.如何添加自定义播放视图
- 比如,现在有个业务需求,需要在视频播放器刚开始添加一个广告视图,等待广告倒计时120秒后,直接进入播放视频逻辑。相信这个业务场景很常见,大家都碰到过,使用该播放器就特别简单,代码如下所示:
- 首先创建一个自定义view,需要实现InterControlView接口,重写该接口中所有抽象方法,这里省略了很多代码,具体看demo。
public class AdControlView extends FrameLayout implements InterControlView, View.OnClickListener { private ControlWrapper mControlWrapper; public AdControlView(@NonNull Context context) { super(context); init(context); } private void init(Context context){ LayoutInflater.from(getContext()).inflate(R.layout.layout_ad_control_view, this, true); } /** * 播放状态 * -1 播放错误 * 0 播放未开始 * 1 播放准备中 * 2 播放准备就绪 * 3 正在播放 * 4 暂停播放 * 5 正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,缓冲区数据足够后恢复播放) * 6 暂停缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,此时暂停播放器,继续缓冲,缓冲区数据足够后恢复暂停 * 7 播放完成 * 8 开始播放中止 * @param playState 播放状态,主要是指播放器的各种状态 */ @Override public void onPlayStateChanged(int playState) { switch (playState) { case ConstantKeys.CurrentState.STATE_PLAYING: mControlWrapper.startProgress(); mPlayButton.setSelected(true); break; case ConstantKeys.CurrentState.STATE_PAUSED: mPlayButton.setSelected(false); break; } } /** * 播放模式 * 普通模式,小窗口模式,正常模式三种其中一种 * MODE_NORMAL 普通模式 * MODE_FULL_SCREEN 全屏模式 * MODE_TINY_WINDOW 小屏模式 * @param playerState 播放模式 */ @Override public void onPlayerStateChanged(int playerState) { switch (playerState) { case ConstantKeys.PlayMode.MODE_NORMAL: mBack.setVisibility(GONE); mFullScreen.setSelected(false); break; case ConstantKeys.PlayMode.MODE_FULL_SCREEN: mBack.setVisibility(VISIBLE); mFullScreen.setSelected(true); break; } //暂未实现全面屏适配逻辑,需要你自己补全 } }
- 然后该怎么使用这个自定义view呢?很简单,在之前基础上,通过控制器对象add进来即可,代码如下所示
controller = new BasisVideoController(this); AdControlView adControlView = new AdControlView(this); adControlView.setListener(new AdControlView.AdControlListener() { @Override public void onAdClick() { BaseToast.showRoundRectToast( "广告点击跳转"); } @Override public void onSkipAd() { playVideo(); } }); controller.addControlComponent(adControlView); //设置控制器 mVideoPlayer.setController(controller); mVideoPlayer.setUrl(proxyUrl); mVideoPlayer.start();
08.关于播放器视图层级
- 视频播放器为了拓展性,需要暴露view接口供外部开发者自定义视频播放器视图,通过addView的形式添加到播放器的控制器中。
- 这就涉及view视图的层级性。控制view视图的显示和隐藏是特别重要的,这个时候在自定义view中就需要拿到播放器的状态
- 举一个简单的例子,基础视频播放器
- 添加了基础播放功能的几个播放视图。有播放完成,播放异常,播放加载,顶部标题栏,底部控制条栏,锁屏,以及手势滑动栏。如何控制它们的显示隐藏切换呢?
- 在addView这些视图时,大多数的view都是默认GONE隐藏的。比如当视频初始化时,先缓冲则显示缓冲view而隐藏其他视图,接着播放则显示顶部/底部视图而隐藏其他视图
- 比如有时候需要显示两种不同的自定义视图如何处理
- 举个例子,播放的时候,点击一下视频,会显示顶部title视图和底部控制条视图,那么这样会同时显示两个视图。
- 点击顶部title视图的返回键可以关闭播放器,点击底部控制条视图的播放暂停可以控制播放条件。这个时候底部控制条视图FrameLayout的ChildView在整个视频的底部,顶部title视图FrameLayout的ChildView在整个视频的顶部,这样可以达到上下层都可以相应事件。
- 那么FrameLayout层层重叠,如何让下层不响应事件
- 在最上方显示的层加上: android:clickable="true" 可以避免点击上层触发底层。或者直接给控制设置一个background颜色也可以。
- 比如基础播放器的视图层级是这样的
//添加自动完成播放界面view CustomCompleteView completeView = new CustomCompleteView(mContext); completeView.setVisibility(GONE); this.addControlComponent(completeView); //添加错误界面view CustomErrorView errorView = new CustomErrorView(mContext); errorView.setVisibility(GONE); this.addControlComponent(errorView); //添加与加载视图界面view,准备播放界面 CustomPrepareView prepareView = new CustomPrepareView(mContext); thumb = prepareView.getThumb(); prepareView.setClickStart(); this.addControlComponent(prepareView); //添加标题栏 titleView = new CustomTitleView(mContext); titleView.setTitle(title); titleView.setVisibility(VISIBLE); this.addControlComponent(titleView); if (isLive) { //添加底部播放控制条 CustomLiveControlView liveControlView = new CustomLiveControlView(mContext); this.addControlComponent(liveControlView); } else { //添加底部播放控制条 CustomBottomView vodControlView = new CustomBottomView(mContext); //是否显示底部进度条。默认显示 vodControlView.showBottomProgress(true); this.addControlComponent(vodControlView); } //添加滑动控制视图 CustomGestureView gestureControlView = new CustomGestureView(mContext); this.addControlComponent(gestureControlView);
09.视频播放器重力感应监听
- 区别视频几种不同的播放模式
- 正常播放时,设置检查系统是否开启自动旋转,打开监听
- 全屏模式播放视频的时候,强制监听设备方向
- 在小窗口模式播放视频的时候,取消重力感应监听
- 注意一点。关于是否开启自动旋转的重力感应监听,可以给外部开发者暴露一个方法设置的开关。让用户选择是否开启该功能
- 首先写一个类,然后继承OrientationEventListener类,注意视频播放器重力感应监听不要那么频繁。表示500毫秒才检测一次……
public class OrientationHelper extends OrientationEventListener { private long mLastTime; private OnOrientationChangeListener mOnOrientationChangeListener; public OrientationHelper(Context context) { super(context); } @Override public void onOrientationChanged(int orientation) { long currentTime = System.currentTimeMillis(); if (currentTime - mLastTime < 500) { return; } //500毫秒检测一次 if (mOnOrientationChangeListener != null) { mOnOrientationChangeListener.onOrientationChanged(orientation); } mLastTime = currentTime; } public interface OnOrientationChangeListener { void onOrientationChanged(int orientation); } public void setOnOrientationChangeListener(OnOrientationChangeListener onOrientationChangeListener) { mOnOrientationChangeListener = onOrientationChangeListener; } }
- 关于播放器播放模式状态发生变化时,需要更新是开启重力感应监听,还是关闭重力感应监听。代码如下所示
/** * 子类重写此方法并在其中更新控制器在不同播放器状态下的ui * 普通模式,小窗口模式,正常模式三种其中一种 * MODE_NORMAL 普通模式 * MODE_FULL_SCREEN 全屏模式 * MODE_TINY_WINDOW 小屏模式 */ @CallSuper protected void onPlayerStateChanged(@ConstantKeys.PlayMode int playerState) { switch (playerState) { case ConstantKeys.PlayMode.MODE_NORMAL: //视频正常播放是设置监听 if (mEnableOrientation) { //检查系统是否开启自动旋转 mOrientationHelper.enable(); } else { //取消监听 mOrientationHelper.disable(); } break; case ConstantKeys.PlayMode.MODE_FULL_SCREEN: //在全屏时强制监听设备方向 mOrientationHelper.enable(); break; case ConstantKeys.PlayMode.MODE_TINY_WINDOW: //小窗口取消重力感应监听 mOrientationHelper.disable(); break; } }