#工作笔记 Android歌词视频开发

前言

入职半年承接的第一个重要需求就是做一个可以任意切换背景,生成自带歌词和音乐的视频,用户导出后保存至相册,下面记录开发过程中遇到的几个有意义的问题和创新。

  • 创新1:实时根据解析的得到的该行歌词时间长度使歌词有渐入渐出的效果(不使用Animation 仅用5行Java代码解决)
  • 问题1:遇到Activity onPause()回调后,onStop()回调慢10s,原因及解决办法
  • 问题2:surfaceView不能使用View.GONE和View.VISIBLE 报空指针异常,且初始化的位置需要放在activity的onCreate()中的原因
  • 问题3:RecyclerView ViewHolder复用问题:底部模板自带进度条,横向滑动后返回 进度条会消失,连续双击进度条会闪动

难点

  • 两个播放器(一个背景音乐+一个视频)解耦
  • 视频播放涉及到渲染、视频导出涉及到编解码
  • 歌词实时展示渐入渐出效果
  • 歌词解析及歌曲缓存、考虑网络中断情况
  • 背景模板下载与歌词缓存下载的时序与同步问题
  • 连续点击背景模板、进度条展示与视频播放逻辑逻辑

UI效果及主要功能介绍

在这里插入图片描述
在这里插入图片描述
1、滑动底部模板,切换视频背景视频,其中白线为矩形进度条表示模板下载进度、若连续点击多个背景,则进行同步下载、进度条同时展示。但加载完毕后只保留播放模板的进度条(100%绕一圈)。如上两张图所示。

在这里插入图片描述
2、歌词界面、滑动播放和展示的歌词、根据用户滑动停止位置、自动播放15s视频。如上图所示。

3、制作完成后点击导出视频编码保存至本地,保存完成后唤起分享弹框点击跳转至相应第三方app,需要使用户手动从相册中选择视频进行发布和分享

创新1:仅用5行代码实现实时歌词渐进渐出效果

1、简介:
由于歌词解析之后返回一个list、其中list.get(i)中包含的信息有(start:本句歌词开始播放的时间int类型、end:本句歌词结束播放的时间int类型、以及该行歌词string类型)、且每一句歌词的时间总长不定所以此处我没有采用Animation来进行alpha动画的效果、稍微有点麻烦。而是直接根据解析得到的信息来实时计算alpha值。

2、原理:
原理图如下所示、已知参数startTime、endTime、factor(自行设定、此处我设置的是0.5)、totoalTime。渐进则是从A->B,alpha值由0->1、渐出则是C->D,alpha值由1->0、横坐标为歌词播放时间、纵坐标为透明度alpha值。那么处于简便的原则,此处我设置0->1的渐变为一个一次函数y=kx+b,且渐进渐出的时间占比为时间总长的一半,并且渐进渐出时间相等。
1)由于A坐标已知(startTime, 0),B坐标已知(start+total*factor/2, 1),两点得一直线就可以求出k和b值从而得出AB点的函数y=k+b;
2)由于该函数为一个等腰梯形,则AB与CD的斜率k值互为相反数,CD渐出函数为y=-kx+b,所以只需要带入C、D任意一点求出b值即可。

需要注意的是:影响参数有factor和播放器的刷新频率,factor的大小影响了渐进渐出变化的快慢(k值的大小)、刷新频率则影响了播放器性能以及效果、不可过低也不可过高,过低会使性能降低、过高会使渐变不生效。

在这里插入图片描述在这里插入图片描述
代码如下:

/**
     * @param factor    规定alpha在每句歌词中所占的渐变时间比
     * @param startTime 该行歌词开始的时间
     * @param endTime   该行歌词结束的时间
     * @param curTime   当前时间
     * @implNote 计算过程是以curTime为横坐标、alpha为纵坐标、该函数为一个等腰梯形的分段函数
     * @return: 透明度alpha值:与刷新频率有关,setVideoUpdateProgressTime设置为10ms
     */
    public static float getAlpha(float factor, long curTime, long startTime, long endTime) {
        long totalTime = endTime - startTime;
        if (curTime <= startTime) {//刚进入本句歌词
            return 0f;
        } else if (endTime - startTime <= 20) {//本句歌词时间小于刷新时间则无需进行渐变
            return 1f;
        } else {
            if (curTime <= totalTime * factor / 2 + startTime) {//渐入0-》1
                return 2 * (curTime - startTime) / (factor * totalTime);
            } else if (curTime >= endTime - (totalTime * factor / 2) && curTime <= endTime) {//渐出1-》0
                return -2 * (curTime - endTime) / (factor * totalTime);
            } else {
                return 1f;
            }
        }
    }

问题2:退出acitivity之后,onPause()立刻回调,onStop()回调慢10s?

1、问题现象:在退出该activity之后返回全屏播放器需要立刻回到全屏播放器的播放状态、但现象是该activity的歌曲播放10s之后才能恢复上一个页面的播放状态。给每一个生命周期写日志查看调用时间发现、onPause()立刻回调、而onStop每次都隔了10s才回调。

2、问题原因:初步推测是上一个页面有动画或者本acitvity有动画不断在主线程进行postInvalidate()导致线程阻塞、onStop无法回调。查找发现确实是上一个页面的动画一直向主线程发送消息。但是为什么只阻塞了onStop没有阻塞onPause?并且每次都是10s?带着疑问去搜博客发现原因总结起来就是以下两点:
1)从一个acitivity A回到activity B的生命周期:onPause(B)->onRestart(A)->onStart(A)->onResume(A)->onStop(B)->onDestroy(B)。所以该activity的onPause会立刻回调,而由于在onResume(A)时、acitivity A中的动画一直在阻塞主线程、从而导致之后的生命周期无法调用

2)线程阻塞的机制:
在这里插入图片描述
总结一下就一句话:LifeCircleManager有强制执行线程的操作、上一个acitivity的动画一直在调用发送消息导致queue阻塞,若10s内handlerIdle未接收到消息则强制执行onStop()、原理如上图所示。

3、解决办法:在activity A onPause()时将动画暂停,在onRestart()时再开始播放,并且尽量不使用view.postInvalidate()而是改成view.invalidate()。

问题2:surfaceView使用View.GONE和View.VISIBLE 会报错,且初始化需要放在onCreate()中的原因

1、问题背景:
1)surfaceView不是常规的View、是GlsurfaceView下的一个子类、不能使用View.GONE。若使用则会报空指针异常。但是为什么没有crash 原因还没搞清楚、后续弄清楚之后补足。
2)普通view的渲染:android5之后将UI thread分了一部分出来变成了Render Thread来进行渲染相关的处理。而渲染过程需要硬件加速和软件加速、硬件加速的部分需要主线程通过ViewRootImpl类来通过SurfaceFlinger通知render thread需要渲染的窗口ANativeWindow、而这个方法就在activity onCreate()时的setContentView()里的setView()中的enableHardwareAcceleration()方法来new 一个RenderThread。

GlView的渲染:activity在onCreate()时会调用setContentView()方法、其中ViewRootImpl()类中doTrasversal()-》ViewTreeObserver类中diapatchOnPreDraw()方法-》SurfaceView类的updateSurface()方法最后调用GLsurfaceView中的surfaceCreated()方法 会发现GLthread并没有初始化从而报空,所以需要在setContentView()中先setRender()来new 一个GLthread 。
在这里插入图片描述
在这里插入图片描述

2、解决办法:将sufaceView的背景在xml文件中设置颜色,然后在视频播放前使背景置空(必须置空!否则无法播放)。视频下载完成后再使封面图消失、播放视频。

问题3:RecyclerView中Adapter的ViewHolder复用问题:底部模板自带进度条,横向滑动后返回 进度条会消失,连续双击进度条会闪动

1、问题背景1:选中A模板播放 进度条为绕一圈的白色,滑动之后返回进度条消失。由于底部模板是由RecyclerView的adapter来进行一个封装和展示、所以其中涉及到holder问题。其原因就是因为没有深刻理解holder复用。

2、问题原因1:一般而言,要是RecyclerView.Adapter的getItemViewType方法返回相同值时,RecyclerView就会复用已经滑出屏幕变为不可见的ViewHolder,假如被复用的ViewHolder持有的View没有被重新赋值或者恢复原始状态,就会出现显示被复用之前的View状态。由于本需求中的背景模板较复杂。连续点击模板、未下载过的模板进度条也需要展示进度、加载完成后以最后一次点击为准、播放相应背景模板的视频。所以此处封装了一个moduleBean对象、根据模板下载状态state(0 未下载、1 正在下载中、2 已下载完成)来进行相应的点击事件处理。

3、解决办法1:
1)直接在RecyclerView.Adapter的onBindViewHolder方法里设置holder.setIsRecyclable(false);
但该方法存在一个问题就是adapter中的图片item过多时会造成OOM、所以尽量不使用该方法
2)在onBindViewHolder方法里面对ViewHolder持有的所有view都按需重新赋值或者恢复初始状态。首先、根据LinearLayoutManager来判断position是否在可见范围内、若在可见范围内则根据所处的位置来获取对应view的viewHolder。

	if (curPostionIsVisiable(position)) { //不可见则不更新,这个view可能已经被复用了
		LinearLayoutManager manager = (LinearLayoutManager) rcv.getLayoutManager();
		if (manager == null) return;
		int firstItemPosition = manager.findFirstVisibleItemPosition();
		if (position - firstItemPosition >= 0) {
			//防止progressBar复用出错
			View view = rcv.getChildAt(position - firstItemPosition);
			if (null != rcv.getChildViewHolder(view)) {
				ModuleAdapter.ViewHolder viewHolder = (ModuleAdapter.ViewHolder)rcv.getChildViewHolder(view);
				viewHolder.pb.setProgress(progress);
			}
		}
	}

	public boolean curPostionIsVisiable(int position) {
        //当前item是否可见
        LinearLayoutManager manager = (LinearLayoutManager) rcv.getLayoutManager();
        if (manager == null) return false;
        int first = manager.findFirstVisibleItemPosition();
        int last = manager.findLastVisibleItemPosition();
        return position >= first && position <= last;
    }

4、问题背景2:由于每个模板的状态不同、在快速点击之后、进度条会出现闪烁状态、且重复点击同一个正在下载的模板、进度条也会闪烁。

5、问题原因2:也是由于ViewHolder没有复用正确且对于module的状态的判断滞后、导致判断状态的语句还未走到、点击事件就再次触发了下载、导致进度重写。

6、解决办法2:提前设置module的状态直接上代码
adapter中代码:

 //设置模板点击事件
        holder.image.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!presenter.isDownload(position) && !NetworkUtils.isNetworkAvailable(mContext)) {//无网络时点击正在下载或者没有下载(除了已下载的状态)的模板,提示网络错误
                    MiguToast.showWarningNotice(mContext, R.string.lrc_video_module_load_net_error);
                    return;
                }
                if (curPostionIsVisiable(position)) { //不可见则不更新,这个view可能已经被复用了
                    holder.moduleSize.setVisibility(GONE);
                    holder.load.setVisibility(GONE);
                }
                if (presenter.isDownloading(position)) { //点击当前正在下载的item,则不做其它操作,只将最后点击的位置更新,为保证下载完成后播放的是用户最后点击的。
                    selectPostion = position;
                } else if (presenter.isDownload(position)) { //已下载完成
                    selectPostion = position;
                    curPostion = position;
                    delegate.playVideo(curPostion);
                    notifyDataSetChanged();//通知adpter更新当前选择信息
                } else {//需要去下载
                    selectPostion = position;
                    holder.pb.setVisibility(View.VISIBLE);
                    String videoUrl = module.getUrl();
                    String videoName = module.getName();
                    module.setState(2);//在此处提前设置module状态!!避免快速点击时module的状态还是0从而再次加载
                    presenter.loadVideo(position, videoUrl, videoName, new LrcVideoPresenter.ProgressCallback() {
                        @Override
                        public void progressLoaded(int progress) {
                            if (curPostionIsVisiable(position)) { //不可见则不更新,这个view可能已经被复用了
                                LinearLayoutManager manager = (LinearLayoutManager) rcv.getLayoutManager();
                                if (manager == null) return;
                                int firstItemPosition = manager.findFirstVisibleItemPosition();
                                if (position - firstItemPosition >= 0) {
                                    //防止progressBar复用出错
                                    View view = rcv.getChildAt(position - firstItemPosition);
                                    if (null != rcv.getChildViewHolder(view)) {
                                        ModuleAdapter.ViewHolder viewHolder = (ModuleAdapter.ViewHolder) rcv.getChildViewHolder(view);
                                        viewHolder.pb.setProgress(progress);
                                    }
                                }
                            }
                        }

                        @SuppressLint("NotifyDataSetChanged")
                        @Override
                        public void finish(String url) {
                            if (curPostionIsVisiable(position)) { //不可见则不更新,这个view可能已经被复用了
                                LinearLayoutManager manager = (LinearLayoutManager) rcv.getLayoutManager();
                                if (manager== null) return;
                                int firstItemPosition = manager.findFirstVisibleItemPosition();
                                View view = rcv.getChildAt(position - firstItemPosition );
                                if (null != rcv.getChildViewHolder(view)) {
                                    ModuleAdapter.ViewHolder viewHolder = (ModuleAdapter.ViewHolder) rcv.getChildViewHolder(view);
                                    if (selectPostion == position) { //如果下载期间用户未选择其它的
                                        //变成选择状态
                                        viewHolder.pb.setVisibility(View.VISIBLE);
                                        viewHolder.pb.setProgress(100);
                                    }else{
                                        viewHolder.pb.setVisibility(GONE);
                                    }
                                }
                            }

                            if (selectPostion == position) { //如果下载期间用户未选择其它的,那么准备播放这个下载的,else则不需要做任何处理
                                curPostion = position;
                                delegate.playVideo(curPostion);
                                notifyDataSetChanged();//通知adpter更新当前选择信息
                            }
                        }

                        @Override
                        public void error() {
                            module.setState(0);
                            if (selectPostion == position) { //重置selectPostion ,如果下载期间用户未选择其它的那么则将其重置为正在播放的位置
                                selectPostion = curPostion;
                            }
                            holder.pb.setProgress(0);
                            holder.pb.setVisibility(GONE);
                        }

                        @Override
                        public void start() {
                            //此处应在开始时设置Progress为0
                            holder.pb.setProgress(0);
                        }
                    });
                }
                XLog.i("click item: position->" + position + "curPostion->" + curPostion + "selectPostion->" + selectPostion);
            }
        });

下载部分代码:

//加载视频背景资源,异步
    public void loadVideo(int pos, String videoUrl, String name,
                          final ProgressCallback progressCallback) {
        VideoInfoBean bean = beanMap.get(pos);
        if (bean.state == 1) {
            progressCallback.finish(bean.getLoadUrl());
            progressCallback.progressLoaded(100);
            return;
        }

        //若内存不存在则开启线程下载背景视频
        File downLoadFolder = new File(SdPath);//临时文件
        File tmpFile = new File(SdPath, name + "-tmp.mp4");
        File file = new File(SdPath, name);

        NetLoader.downLoad(videoUrl)
                .savePath(downLoadFolder.getPath())
                .saveName(tmpFile.getName()).execute(new DownloadProgressCallBack<String>() {
            @Override
            public void onStart() {
                progressCallback.start();
            }

            @Override
            public void onError(ApiException e) {
                //缓存失败
                bean.state = 0; //下载失败则未下载
                progressCallback.error();
                MiguToast.showWarningNotice(delegate.getActivity(), "下载失败,请稍后重试!");
            }

            @Override
            public void update(long bytesRead, long contentLength, boolean done) {
                //下载进度
                int progress = (int) (((double) bytesRead / (double) contentLength) * 100);
                if (progress >= 99) {
                    progress = 100;
                }
                //增加背景模板缓存进度条
                progressCallback.progressLoaded(progress);
                bean.state = 2;
                bean.progress = progress;
            }

            @Override
            public void onComplete(String path) {
                if (path == null) {
                    MiguToast.showWarningNotice(delegate.getActivity(), "下载失败,请稍后重试!");
                    return;
                }
                if (tmpFile.exists()) {
                    if (tmpFile.renameTo(file)) {
                        //缓存完成
                        bean.state = 1;
                        bean.progress = 100;
                        bean.setLoadUrl(file.getAbsolutePath());
                        progressCallback.finish(file.getAbsolutePath());
                    }
                }
            }
        });
    }

后续优化

1、修改布局:将LinearLayout改成ViewPager、方便后续新增歌词图片部分的切换。底部歌词模块和背景模块都改成ViewPager
2、修复自定义View存在的问题:矩形进度条转一圈后偶现一瞬间闪动
3、优化sufaceView

参考文章

深入分析Activity onStop()生命周期延时10s回调的原因

Activity销毁onStop或onDestroy延时10s左右才回调

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值