Android组件化开发(六)-- 短视频播放组件封装

简介: 前面几篇文章我们封装了几个组件化功能组件:包括:`网络请求组件`,`图片加载请求组件`,`应用保活组件`,`音乐播放组件封装`。> 每个组件都可以直接拿到自己项目中使用,当然还需根据自己项目要求进行优化。

theme: smartblue

highlight: a11y-dark

🔥 Hi,我是小余。

本文已收录到 GitHub · Androider-Planet 中。这里有 Android 进阶成长知识体系,关注公众号 [小余的自习室] ,在成功的路上不迷路!

前言

前面几篇文章我们封装了几个组件化功能组件:
包括:网络请求组件图片加载请求组件应用保活组件音乐播放组件封装

每个组件都可以直接拿到自己项目中使用,当然还需根据自己项目要求进行优化。

组件化开发.png
组件化相关系列文章:

Android组件化开发(一)--Maven私服的搭建

Android组件化开发(二)--网络请求组件封装

Android组件化开发(三)--图片加载组件封装

Android组件化开发(四)--进程保活组件的封装

Android组件化开发(五)--完整版音乐播放组件的封装

今天我们再来封装一个视频播放请求:

我们先来讲解我们封装的思路。

笔者一直觉得:学习别人开发的思路比学习到当前知识内容更重要。
学思路不仅在工作中对自己帮助很大,在生活中用处也很大

首先我们需要了解我们的需求:

笔者这篇文章的需求是基于我们组件化业务中的“朋友”界面中的短视频播放请求。

一图胜千言

device-2022-07-26-154741.png

需求分析

  • 1.切换到朋友界面后,需要显示一个类朋友圈功能的界面,其中核心是内部的一个播放器请求。
  • 2.首先在视频加载过程中显示一个视频加载的动画。

    • 2.1:加载成功后,视频开始播放,并在左上角显示一个可以切到全屏的icon(图像比16:9),此时将音频静音
    • 2.2:加载失败,可以根据重试次数进行重试,如果重试次数达到上限还是失败,则直接显示一个播放错误的icon
  • 3.切换到全屏图像后,从小界面播放器播放的位置开始显示视频,并将音频打开,点击返回键可以回到原朋友圈界面。

按上面的需求分析:
下面我们来做个技术选型:

技术选型

  • ljkPlayer

ljkPlayer 是Bilibili公司维护的一个开源工程,是基于ffmpeg开发的一个播放器软件,目前支持Android和iOS两种平台

优点
1.基于FFmpeg开发,结构比较清晰,对于二次开发难度比较小
2.支持直播开发

缺点
1.基于ffmpeg,可扩展性比较小
2.官方这几年维护力度没那么大了,版本更新较慢

  • ExoPlayer

ExoPlayer是谷歌推出的一个开源播放器,使用原Android提供的解码系统来解析视频和音频
MediaCodec封装地非常完善,形成了一个性能优越,播放稳定性较好的一个开发播放器
由于Google的大力推广,目前非常流行

优点:1.接入包比较小,1.1M左右 2.维护比较勤,版本更新较快

缺点
1.不适合做直播
2.视频硬解码,可扩展性一般
3.适合播放场景比较简单的场景

  • MediaPlayer二次开发:
优点:代码可控性较强
缺点:封装难度比较大。

因为我们需求比较简单,笔者在ExoPlayerMediaPlayer中考虑,最终还是选择使用MediaPlayer做二次开发
因为ExoPlayer内部封装的很多东西都是不必要的,也算变相的做了一次包体积优化咯。
后期如果有直播需求的,可以考虑直接使用ljkPlayer进行二次开发。

封装

根据前面的需求分析和技术选型,下面来做具体的封装工作。

首先你需要对MediaPlayer有一些基础的了解。
如果对MediaPlayer还不是很了解的,请转到官网查看

引用官网的话:

MediaPlayer 类是媒体框架最重要的组成部分之一。此类的对象能够获取、解码以及播放音频和视频,而且只需极少量设置。它支持多种不同的媒体源,例如:

  • 本地资源
  • 内部 URI,例如您可能从内容解析器那获取的 URI
  • 外部网址(流式传输)

MediaPlayer最重要的是:

他内部维护了一套音乐播放状态机每个步骤都需要根据上一个状态来做改变,如果在某个状态执行了错误的步骤就会出异常甚至崩溃,你可能不想看到这种事件发生在你的app中吧。。
所以接下来你会在我的代码中看到很多关于状态的判断。

MediaPlayer播放状态切换图

音乐播放状态机.awebp

可以看到内部调用过程还是挺复杂的。好在这些都是MediaPlayer自己内部维护,大部分情况不需要人为去处理。

MediaPlayer不仅可以播放音频还可以播放适配,区别是播放视频需要一个Surface才可以显示

笔者根据需求设计了
类关系图

视频播放.png

  • SmartVideoView:用于小屏播放时的视频界面
  • VideoFullScreenDialog:用于大屏播放时的界面
  • VideoAdSlot:是对大小屏的控制类,大小屏切换就是根据这个控制类进行
  • VideoAdContext:是业务层和VideoAdSlot之间通讯的桥梁,起承上启下的作用,这样可以让播放的处理操作处于一个黑盒中,隔离业务层逻辑

来看具体代码:

  • 1.SmartVideoView

由于类的篇幅太长,下面只列出部分关键代码:
完整代码已经上传Github 上。

public class SmartVideoView extends RelativeLayout implements View.OnClickListener,
        MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener,
        TextureView.SurfaceTextureListener {
    ...
    private static final int STATE_ERROR = -1;
    private static final int STATE_IDLE = 0;
    private static final int STATE_PLAYING = 1;
    private static final int STATE_PAUSING = 2;
    private static final int STATE_COMP = 3;
    
    
    private static final int LOAD_TOTAL_COUNT = 3;

    ...
    private TextureView mVideoView;
    private RelativeLayout mPlayerView;
    ...
  
    /**
     * 屏幕宽高
     */
    private int mScreenWidth;
    private int mVideoHeight;
    ...
    private Surface videoSurface;

    /**给mMediaPlayer设置Surface
     * @param surface
     * @param width
     * @param height
     */
    @Override
    public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height) {
        videoSurface = new Surface(surface);
        checkMediaPlayer();
        mMediaPlayer.setSurface(videoSurface);
        load();
    }
    public SmartVideoView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        initData();
        initView();
        //注册屏幕切换广播
        registerBroadcastReceiver();
    }

    /**
     * 初始化布局
     * 1.设置点击事件
     * 2.SurfaceTexture回调事件
     * 3.初始化小屏状态
     */
    private void initView() {
        mPlayerView = (RelativeLayout) LayoutInflater.from(context).inflate(R.layout.video_player_layout,this);
        mVideoView = findViewById(R.id.xadsdk_player_video_textureView);
        mVideoView.setOnClickListener(this);
        mVideoView.setSurfaceTextureListener(this);
        initSmallLayout();
    }

    /**小模式状态
     * 1.设置各种View的点击事件
     */
    private void initSmallLayout() {
        setPlayViewlayoutParam();
        mMiniPlayBtn = mPlayerView.findViewById(R.id.xadsdk_small_play_btn);
        mFullBtn = mPlayerView.findViewById(R.id.xadsdk_to_full_view);
        mLoadingBar = mPlayerView.findViewById(R.id.loading_bar);
        mMiniPlayBtn.setOnClickListener(this);
        mFullBtn.setOnClickListener(this);
    }

    /**
     * 初始化宽高:16:9
     */
    private void initData() {
        DisplayMetrics dm = new DisplayMetrics();
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        wm.getDefaultDisplay().getMetrics(dm);
        mScreenWidth = dm.widthPixels;
        mVideoHeight = (int) (mScreenWidth * (9/16.0f));
    }
   ...
    /**数据元加载成功,准备调用start开启播放
     * @param mp
     */
    @Override
    public void onPrepared(MediaPlayer mp) {
        //1.显示playing状态‘
        showPlayView();
        //2.设置当前状态为Playing
        setPlayerState(STATE_PLAYING);
        //3设置静音模式
        mute(true);
        //4.开始调用start启动MediaPlayer
        mMediaPlayer.start();
        mCurrentCount = 0;
    }


    /**播放事件完成后回调:
     * 1.显示完成状态的view
     * 2.设置当前状态太为完成撞他
     * @param mp
     */
    @Override
    public void onCompletion(MediaPlayer mp) {
        if (listener != null) {
            listener.onAdVideoLoadComplete();
        }
        //显示播放完成状态
        playBack();
        //设置当前状态为完成状态
        setPlayerState(STATE_COMP);

        setIsComplete(true);
        setIsRealPause(true);
    }

    /**出现异常
     * @param mp
     * @param what
     * @param extra
     * @return
     */
    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        //1设置当前状态为ERROR
        setPlayerState(STATE_ERROR);
        //2回调error事件给上游
        if (this.listener != null) {
            listener.onAdVideoLoadFailed();
        }
        //3调用stop方法
        stop();
        return false;
    }

    /**各种View的点击事件处理
     * @param v
     */
    @Override
    public void onClick(View v) {
        ...
    }

    /**
     * 加载视频
     */
    public void load(){
        //1.判断状态是否是初始化状态
        if(playerState != STATE_IDLE){
            return;
        }
        //2.显示对应的Vew状态
        showLoadingView();
        //3.判断MediaPlayer是否已经初始化过
        checkMediaPlayer();
        //4.异步加载请求,回调onPrepare方法
        try {
            mMediaPlayer.setDataSource(mUrl);
            mMediaPlayer.prepareAsync();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 暂停按钮
     */
    public void pause(){
        //1检测状态是否是播放状态
       ...
        if(isPlaying()){
            //3调用MediaPlayer的pause方法
            mMediaPlayer.pause();
        }
        //2显示暂停状态
        ...
        //4设置当前状态为pause状态
        ...
    }

    /**
     * 恢复按钮
     */
    public void resume(){
        //1检测当前状态是否是暂停状态
        if(playerState != STATE_PAUSING && this.playerState != STATE_COMP){
            return;
        }
        if(!isPlaying()){
            //3调用MediaPlayer的resume方法
            mMediaPlayer.start();
        }
        //2显示恢复状态
        showPlayView();
        //4设置当前状态为播放状态
        entryResumeState();
    }

    //跳到指定点播放视频
    public void seekAndResume(int position) {
        ...
    }

    //跳到指定点暂停视频
    public void seekAndPause(int position) {
        if (this.playerState != STATE_PLAYING) {
            return;
        }
        showPauseView(false);
        setPlayerState(STATE_PAUSING);
        if (isPlaying()) {
            mMediaPlayer.seekTo(position);
            mMediaPlayer.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() {
                @Override
                public void onSeekComplete(MediaPlayer mp) {
                    mMediaPlayer.pause();
                }
            });
        }
    }

    //全屏不显示暂停状态,后续可以整合,不必单独出一个方法
    public void pauseForFullScreen() {
        ...
    }



    /**
     * 停止播放,需要seek到0的位置,一般出异常才会调用这个
     */
    private void stop(){
        //1.将MediaPlayer释放掉
        ..
        //2.设置当前状态为初始化状态
        ..
        //3.判断是否满足重试条件,根据重试次数来
        ..
    }

    /**
     * 释放MediaPlayer状态
     */
    public void destroy(){
        //1.将MediaPlayer释放掉
        ..
        //2.设置当前状态为初始化状态
        ..
        //3 显示pause状态
        ..
    }

    /**
     * 显示加载View
     */
    private void showLoadingView() {
        ...
    }

    /**
     * 显示播放View
     */
    private void showPlayView() {
        ...
    }

    /**显示状态状态
     * @param show 是否显示暂停和播放按钮
     */
    private void showPauseView(boolean show) {
        ...
    }

    //播放完成后回到初始状态
    private void playBack(){
        setPlayerState(STATE_PAUSING);
        if (mMediaPlayer != null) {
            mMediaPlayer.setOnSeekCompleteListener(null);
            mMediaPlayer.seekTo(0);
            mMediaPlayer.pause();
        }
        this.showPauseView(false);
    }

    ..

    /**
     * 设置播放器的宽高
     */
    private void setPlayViewlayoutParam() {
        ...
    }


    
    ...
    private void registerBroadcastReceiver() {
        if (mScreenReceiver == null) {
            mScreenReceiver = new ScreenEventReceiver();
            IntentFilter filter = new IntentFilter();
            filter.addAction(Intent.ACTION_SCREEN_OFF);
            filter.addAction(Intent.ACTION_USER_PRESENT);
            getContext().registerReceiver(mScreenReceiver, filter);
        }
    }
    ...

    /**
     * 监听锁屏事件的广播接收器
     */
    private class ScreenEventReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            //主动锁屏时 pause, 主动解锁屏幕时,resume
            switch (intent.getAction()) {
                case Intent.ACTION_USER_PRESENT:
                    if (playerState == STATE_PAUSING) {
                        if (mIsRealPause) {
                            //手动点的暂停,回来后还暂停
                            pause();
                        } else {
                            resume();
                        }
                    }
                    break;
                case Intent.ACTION_SCREEN_OFF:
                    if (playerState == STATE_PLAYING) {
                        pause();
                    }
                    break;
            }
        }
    }
}

下面我来说下该类的设计思路?为什么这么设计?

  • 1.继承自RelativeLAyout,并实现了MediaPlayer需要的几个监听方法:onPrepare,onCompleted,onError,这些和音频播放都一样,其中最重要的是onSurfaceTextureAvailable

监听,这个监听回调是在我们的SurfaceTexture初始化好后,回调给应用层的一个操作。

public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height) {
        videoSurface = new Surface(surface);
        checkMediaPlayer();
        mMediaPlayer.setSurface(videoSurface);
        load();
    }

应用接收到onSurfaceTextureAvailable回调后可以调用
MediaPlayerprepareAsync异步加载操作,并在onPrepare回调中,调用MediaPlayerstart操作启动播放,


/**
     * 加载视频
     */
    public void load(){
        //1.判断状态是否是初始化状态
        if(playerState != STATE_IDLE){
            return;
        }
        //2.显示对应的Vew状态
        showLoadingView();
        //3.判断MediaPlayer是否已经初始化过
        checkMediaPlayer();
        //4.异步加载请求,回调onPrepare方法
        try {
            mMediaPlayer.setDataSource(mUrl);
            mMediaPlayer.prepareAsync();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
 /**数据元加载成功,准备调用start开启播放
     * @param mp
     */
    @Override
    public void onPrepared(MediaPlayer mp) {
        //1.显示playing状态‘
        showPlayView();
        //2.设置当前状态为Playing
        setPlayerState(STATE_PLAYING);
        //3设置静音模式
        mute(true);
        //4.开始调用start启动MediaPlayer
        mMediaPlayer.start();
        mCurrentCount = 0;
    }

最后我们的视频数据渲染到对应的SurfaceTexture上面。

  • 2.这个类的内部设置了很多播放器状态:

包括空闲状态idle,播放状态Playing,暂停状态Pausing,停止状态stoped和错误error状态等

private static final int STATE_ERROR = -1;
private static final int STATE_IDLE = 0;
private static final int STATE_PLAYING = 1;
private static final int STATE_PAUSING = 2;
private static final int STATE_COMP = 3;
和音频类似,每个阶段的操作都是根据 MediaPlayer这个 状态机来处理的,每个状态下只能执行特定的任务,需要对每个状态进行判断,
这也是自己封装MediaPlayer做二次开发的难点所在。
  • 3.这里我们将界面宽高比设置为16:9
/**
     * 初始化宽高:16:9
     */
    private void initData() {
        DisplayMetrics dm = new DisplayMetrics();
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        wm.getDefaultDisplay().getMetrics(dm);
        mScreenWidth = dm.widthPixels;
        mVideoHeight = (int) (mScreenWidth * (9/16.0f));
    }
  • 4.注册了一个锁屏的广播:可以在视频处于锁屏状态下执行对应的操作
/**
     * 监听锁屏事件的广播接收器
     */
    private class ScreenEventReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            //主动锁屏时 pause, 主动解锁屏幕时,resume
            switch (intent.getAction()) {
                case Intent.ACTION_USER_PRESENT:
                    if (playerState == STATE_PAUSING) {
                        if (mIsRealPause) {
                            //手动点的暂停,回来后还暂停
                            pause();
                        } else {
                            resume();
                        }
                    }
                    break;
                case Intent.ACTION_SCREEN_OFF:
                    if (playerState == STATE_PLAYING) {
                        pause();
                    }
                    break;
            }
        }
    }

基本上就是按上面逻辑来开发设计的,还有一些是在不同状态显示不同UI这里就不介绍了

下面来介绍下VideoFullScreenDialog

这个类是用来显示一个全屏播放页面的,其实也是按 16:9的比例显示,并非正真的全屏

代码:

public class VideoFullScreenDialog extends Dialog implements SmartVideoView.ADVideoPlayerListener {
    private SmartVideoView mVideoView;
     public VideoFullScreenDialog(Context context, SmartVideoView mraidView, String instance,
                                 int position) {
        super(context, R.style.dialog_full_screen);
        mPosition = position;
        mVideoView = mraidView;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS,
                WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.dialog_video_layout);
        initVideoView();
    }
    
    private void initVideoView() {
        mParentView = (RelativeLayout) findViewById(R.id.content_layout);
        mRootView = findViewById(R.id.root_view);
        mRootView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onClickVideo();
            }
        });
        mRootView.setVisibility(View.INVISIBLE);

        mVideoView.setListener(this);
        mVideoView.mute(false);
        mParentView.addView(mVideoView);
        mParentView.getViewTreeObserver()
                .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                    @Override
                    public boolean onPreDraw() {
                        mParentView.getViewTreeObserver().removeOnPreDrawListener(this);
                        prepareScene();
                        runEnterAnimation();
                        return true;
                    }
                });
    }
    
    //准备动画所需数据
    private void prepareScene() {
        mEndBundle = Utils.getViewProperty(mVideoView);
        /**
         * 将desationview移到originalview位置处
         */
        deltaY = (mStartBundle.getInt(Utils.PROPNAME_SCREENLOCATION_TOP) - mEndBundle.getInt(
                Utils.PROPNAME_SCREENLOCATION_TOP));
        mVideoView.setTranslationY(deltaY);
    }

    //准备入场动画
    private void runEnterAnimation() {
        mVideoView.animate()
                .setDuration(200)
                .setInterpolator(new LinearInterpolator())
                .translationY(0)
                .withStartAction(new Runnable() {
                    @Override
                    public void run() {
                        mRootView.setVisibility(View.VISIBLE);
                    }
                })
                .start();
    }

    //准备出场动画
    private void runExitAnimator() {
        mVideoView.animate()
                .setDuration(200)
                .setInterpolator(new LinearInterpolator())
                .translationY(deltaY)
                .withEndAction(new Runnable() {
                    @Override
                    public void run() {
                        dismiss();
                        if (mListener != null) {
                            mListener.getCurrentPlayPosition(mVideoView.getCurrentPosition());
                        }
                    }
                })
                .start();
    }
}

可以看到这个类就比较简单了。

关键点

  • 1.我们的构造方法中传入了一个SmartVideoView为什么要传入一个SmartVideoView呢?新建一个不可以么?

这样做的目的其实是为了我们的VideoView复用做准备,大家想想,我们切换到大屏的时候,小屏的VideoView其实是在后台的,为啥不直接拿来复用呢,还省去了很多初始化的操作。

  • 2.这里对VideoView做了一个入场和出场动画。防止动画切换不协调
所以别看这个类很简单,背面的思想还是很需要一番考量的。

好了继续下一个 VideoAdSlot

这个类设计来由:
前面我们对大小屏切换说过我们的SmartVideoView是可以拿来复用给的,那复用的逻辑就是这个类来控制的。

public class VideoAdSlot implements SmartVideoView.ADVideoPlayerListener {
    private Context mContext;
    /**
     * UI
     */
    private SmartVideoView mVideoView;
    private ViewGroup mParentView;
    /**
     * Data
     */
    private String mXAdInstance;
    private SDKSlotListener mSlotListener;

    public VideoAdSlot(String adInstance, SDKSlotListener slotLitener) {

        mXAdInstance = adInstance;
        mSlotListener = slotLitener;
        mParentView = slotLitener.getAdParent();
        mContext = mParentView.getContext();
        initVideoView();
    }

    public void destroy() {
        mVideoView.destroy();
        mVideoView = null;
        mContext = null;
        mXAdInstance = null;
    }


    private void initVideoView() {
        mVideoView = new SmartVideoView(mContext);
        if (mXAdInstance != null) {
            mVideoView.setDataSource(mXAdInstance);
            mVideoView.setListener(this);
        }
        RelativeLayout paddingView = new RelativeLayout(mContext);
        paddingView.setBackgroundColor(mContext.getResources().getColor(android.R.color.black));
        paddingView.setLayoutParams(mVideoView.getLayoutParams());
        mParentView.addView(paddingView);
        mParentView.addView(mVideoView);
    }


    /**
     * 实现play层接口
     */
    @Override
    public void onClickFullScreenBtn() {
        //获取videoview在当前界面的属性
        Bundle bundle = Utils.getViewProperty(mParentView);
        mParentView.removeView(mVideoView);
        VideoFullScreenDialog dialog =
                new VideoFullScreenDialog(mContext, mVideoView, mXAdInstance, mVideoView.getCurrentPosition());
        dialog.setListener(new VideoFullScreenDialog.FullToSmallListener() {
            @Override
            public void getCurrentPlayPosition(int position) {
                backToSmallMode(position);
            }

            @Override
            public void playComplete() {
                bigPlayComplete();
            }
        });
        dialog.setViewBundle(bundle); //为Dialog设置播放器数据Bundle对象
        dialog.setSlotListener(mSlotListener);
        dialog.show();
    }

    private void backToSmallMode(int position) {
        if (mVideoView.getParent() == null) {
            mParentView.addView(mVideoView);
        }
        mVideoView.setTranslationY(0); //防止动画导致偏离父容器
        mVideoView.isShowFullBtn(true);
        mVideoView.mute(true);
        mVideoView.setListener(this);
        mVideoView.seekAndResume(position);
    }

    private void bigPlayComplete() {
        if (mVideoView.getParent() == null) {
            mParentView.addView(mVideoView);
        }
        mVideoView.setTranslationY(0); //防止动画导致偏离父容器
        mVideoView.isShowFullBtn(true);
        mVideoView.seekAndPause(0);
    }
    
    @Override
    public void onAdVideoLoadSuccess() {
        if (mSlotListener != null) {
            mSlotListener.onVideoLoadSuccess();
        }
    }

    @Override
    public void onAdVideoLoadFailed() {
        if (mSlotListener != null) {
            mSlotListener.onVideoFailed();
        }
    }

    @Override
    public void onAdVideoLoadComplete() {
        if (mSlotListener != null) {
            mSlotListener.onVideoComplete();
        }
        mVideoView.setIsRealPause(true);
    }
    ...
}

关键点:

  • 1.ViewGroup mParentView;

    这个是外部传入进来的作为SmartVedioView容器,每次启动一个SmartVedioView会提供一个容器来包裹,
    由外部传入,可以让我们的容器可扩展性更强。

  • 2.内部控制大小屏切换,切换过程中,需要将VedioView移除原mParentView,并传入到大屏中去。

我们用一张图来总结下大小屏切换的View复用机制

SmartVedioView复用.png

最后来看下VideoAdContext

public class VideoAdContext implements VideoAdSlot.SDKSlotListener {

    //the ad container
    private ViewGroup mParentView;

    private VideoAdSlot mAdSlot;
    private String mInstance;
    //the listener to the app layer
    private VideoContextInterface mListener;

    public VideoAdContext(ViewGroup parentView, String instance) {
        this.mParentView = parentView;
        this.mInstance = instance;
        load();
    }

    /**
     * init the ad,不调用则不会创建videoview
     */
    public void load() {
        if (mInstance != null) {
            mAdSlot = new VideoAdSlot(mInstance, this);
        } else {
            mAdSlot = new VideoAdSlot(null, this); //创建空的slot,不响应任何事件
            if (mListener != null) {
                mListener.onVideoFailed();
            }
        }
    }
    ...
    
}

可以看到这个类在构造方法中创建了一个VideoAdSlot,其他都不用处理,一切逻辑控制都交由VideoAdSlot自身处理。

几个类都介绍完毕了:
我来说说上面代码的优缺点吧?你没听错,我在怼我自己。。

优点

  • 1.对ViedeoView的复用
  • 2.对大小屏切换操作使用一个类来控制,而非直接跳转,让大小屏做自己的事情就好了,增加可扩展性和隔离性。
  • 3.使用第三方VideoAdSlot类来承上启下,做到接口隔离。

缺点

SmartViedeoView中的代码还是太集中了,View中做了很多MediaPlayer做的事情,可以将MediaPlayer的逻辑剥离开来,对外部SmartViedeoView提供访问接口
,让SmartViedeoView去调用这部分逻辑,也许代码看起来会更好,这个后面有时间会重新设计下。

总结

本文主要是基于MediaPlayer二次封装了一个大小屏切换的播放器功能。
背后逻辑不复杂,但是内部有些设计思路需要有一定功底才能想到,

学习过程中:希望你能 学人之鱼,也能学人之渔

到今天我们组件化实战项目已经开发了差不多一半多了,后面还会对分享组件以及应用更新组件等进行封装,以及对base库的封装,arout库的使用,让组件之间进行解耦
持续输出中。。

项目地址:https://github.com/ByteYuhb/anna_music_app

组件化开发.png

相关文章
|
14天前
|
搜索推荐 前端开发 API
探索安卓开发中的自定义视图:打造个性化用户界面
在安卓应用开发的广阔天地中,自定义视图是一块神奇的画布,让开发者能够突破标准控件的限制,绘制出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战技巧,逐步揭示如何在安卓平台上创建和运用自定义视图来提升用户体验。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开新的视野,让你的应用在众多同质化产品中脱颖而出。
40 19
|
15天前
|
JSON Java API
探索安卓开发:打造你的首个天气应用
在这篇技术指南中,我们将一起潜入安卓开发的海洋,学习如何从零开始构建一个简单的天气应用。通过这个实践项目,你将掌握安卓开发的核心概念、界面设计、网络编程以及数据解析等技能。无论你是初学者还是有一定基础的开发者,这篇文章都将为你提供一个清晰的路线图和实用的代码示例,帮助你在安卓开发的道路上迈出坚实的一步。让我们一起开始这段旅程,打造属于你自己的第一个安卓应用吧!
41 14
|
14天前
|
开发框架 Android开发 iOS开发
安卓与iOS开发中的跨平台策略:一次编码,多平台部署
在移动应用开发的广阔天地中,安卓和iOS两大阵营各占一方。随着技术的发展,跨平台开发框架应运而生,它们承诺着“一次编码,到处运行”的便捷。本文将深入探讨跨平台开发的现状、挑战以及未来趋势,同时通过代码示例揭示跨平台工具的实际运用。
|
15天前
|
搜索推荐 前端开发 测试技术
打造个性化安卓应用:从设计到开发的全面指南
在这个数字时代,拥有一个定制的移动应用不仅是一种趋势,更是个人或企业品牌的重要延伸。本文将引导你通过一系列简单易懂的步骤,从构思你的应用理念开始,直至实现一个功能齐全的安卓应用。无论你是编程新手还是希望拓展技能的开发者,这篇文章都将为你提供必要的工具和知识,帮助你将创意转化为现实。
|
15天前
|
Java Android开发 开发者
探索安卓开发:构建你的第一个“Hello World”应用
在安卓开发的浩瀚海洋中,每个新手都渴望扬帆起航。本文将作为你的指南针,引领你通过创建一个简单的“Hello World”应用,迈出安卓开发的第一步。我们将一起搭建开发环境、了解基本概念,并编写第一行代码。就像印度圣雄甘地所说:“你必须成为你希望在世界上看到的改变。”让我们一起开始这段旅程,成为我们想要见到的开发者吧!
24 0
|
27天前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台策略
在移动应用开发的战场上,安卓和iOS两大阵营各据一方。随着技术的演进,跨平台开发框架成为开发者的新宠,旨在实现一次编码、多平台部署的梦想。本文将探讨跨平台开发的优势与挑战,并分享实用的开发技巧,帮助开发者在安卓和iOS的世界中游刃有余。
|
1月前
|
缓存 前端开发 Android开发
安卓开发中的自定义视图:从零到英雄
【10月更文挑战第42天】 在安卓的世界里,自定义视图是一块画布,让开发者能够绘制出独一无二的界面体验。本文将带你走进自定义视图的大门,通过深入浅出的方式,让你从零基础到能够独立设计并实现复杂的自定义组件。我们将探索自定义视图的核心概念、实现步骤,以及如何优化你的视图以提高性能和兼容性。准备好了吗?让我们开始这段创造性的旅程吧!
26 1
|
1月前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
|
27天前
|
IDE Java 开发工具
移动应用与系统:探索Android开发之旅
在这篇文章中,我们将深入探讨Android开发的各个方面,从基础知识到高级技术。我们将通过代码示例和案例分析,帮助读者更好地理解和掌握Android开发。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和技巧。让我们一起开启Android开发的旅程吧!
|
18天前
|
Java Linux 数据库
探索安卓开发:打造你的第一款应用
在数字时代的浪潮中,每个人都有机会成为创意的实现者。本文将带你走进安卓开发的奇妙世界,通过浅显易懂的语言和实际代码示例,引导你从零开始构建自己的第一款安卓应用。无论你是编程新手还是希望拓展技术的开发者,这篇文章都将为你打开一扇门,让你的创意和技术一起飞扬。