前言

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

  • 创新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-》1return 2 * (curTime - startTime) / (factor * totalTime);} else if (curTime >= endTime - (totalTime * factor / 2) && curTime <= endTime) {//渐出1-》0return -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() {@Overridepublic 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() {@Overridepublic 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")@Overridepublic 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更新当前选择信息}}@Overridepublic void error() {module.setState(0);if (selectPostion == position) { //重置selectPostion ,如果下载期间用户未选择其它的那么则将其重置为正在播放的位置selectPostion = curPostion;}holder.pb.setProgress(0);holder.pb.setVisibility(GONE);}@Overridepublic void start() {//此处应在开始时设置Progress为0holder.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>() {@Overridepublic void onStart() {progressCallback.start();}@Overridepublic void onError(ApiException e) {//缓存失败bean.state = 0; //下载失败则未下载progressCallback.error();MiguToast.showWarningNotice(delegate.getActivity(), "下载失败,请稍后重试!");}@Overridepublic 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;}@Overridepublic 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左右才回调

#工作笔记 Android歌词视频开发相关推荐

  1. Android 音视频开发(二):使用 AudioRecord 采集音频PCM并保存到文件(学习笔记)

    关于 AudioRecord Android SDK 提供了两套音频采集的API,分别是:MediaRecorder 和 AudioRecord,前者是一个更加上层一点的API,它可以直接把手机麦克风 ...

  2. 企业级Android音视频开发笔记分享,快来get正确的学习姿势

    随着传统的图文媒体向以音视频为主的新媒体转变,音视频开发逐渐成为Android领域内的小热门.但音视频开发涉及的层面较广,相关的技术繁多且复杂,想要深入确有一定难度.且目前网络上关于Android 音 ...

  3. Android 音视频开发学习思路

    Android 音视频开发这块目前的确没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的.只能通过一点点的学习和积累把这块的知识串联积累起来. 初级入门篇: Android 音视频开发(一) ...

  4. Android 音视频开发之基础篇 使用 imageview绘制一张图片

    Android 音视频开发 任务一 ImageView 绘制图片 文章目录 Android 音视频开发 任务一 ImageView 绘制图片 前言 一.配置activity_main.xml 二.添加 ...

  5. Android短视频开发中的sdk接入方案

    目前短视频平台非常火,云豹科技作为优质的app源码提供商,在短视频开发领域有丰富的经验和完善的技术.下面以云豹短视频为例,概述Android短视频开发中的sdk接入方案,这里我们选择腾讯云的sdk进行 ...

  6. 23最新《Android音视频开发进阶指南》,音视频开发者速领

    作为Android开发程序员,我们时刻站在互联网的前端,而音视频作为现在乃至未来几年一个强劲的风口,吸引了许多程序员的关注. 那么音视频开发的行业现状究竟如何呢?我们又该怎样入门呢?请看下文: 音视频 ...

  7. 那些年,Android音视频开发那些事儿

    音视频开发的主要应用有哪些? 音频播放器,录音机,语音电话,音视频监控应用,音视频直播应用,音频编辑/处理软件,蓝牙耳机/音箱,等等 1.视频监控类 (JNI+应用层开发) 从硬件到嵌入式再到软件,涉 ...

  8. Android音视频开发基础(六):学习MediaCodec API,完成视频H.264的解码

    前言 在Android音视频开发中,网上知识点过于零碎,自学起来难度非常大,不过音视频大牛Jhuster提出了<Android 音视频从入门到提高 - 任务列表>.本文是Android音视 ...

  9. 【Android音视频开发】音频编码原理

    文章变更表 文章版本号 变更内容 变更日期 备注 0.0.1 创建 2022/9/29 初版 0.0.2 补充编码原理和音频格式等内容 2022/9/30 1. 前言 在[Android音视频开发] ...

最新文章

  1. Cocos2d-X中实现菜单特效
  2. Application runtime path /opt/lampp/htdocs/yii/test/protected/runtime is not valid. 错误
  3. python 笔记 之 sqlalchemy操作数据库-创建表
  4. JoshChen判断是否微信内置浏览器访问【转载】
  5. ios无痕埋点_iOS可视化埋点方案
  6. 冠榕智能灯光控制协议分析(controller init)
  7. Keil(MDK-ARM-STM32)系列教程(三)工程目标选项配置(Ⅰ)
  8. hive 修改分桶数 分桶表_疯狂Hive之DDL操作二(三)
  9. Momentum(动量/冲量)的理解及应用
  10. Fiddler自动保存抓包内容到文件
  11. linux源码解读系列
  12. 五款服务器配置管理工具
  13. lilo是什么意思_Lilo_英文名Lilo是什么意思
  14. 知识图谱:R2RDF转换之D2RQ
  15. D. Lizard Era: Beginning
  16. 苹果手机怎么编辑word文档_word文档的基本编辑操作
  17. 靠腰,badboy录制脚本老是发生脚本错误
  18. 阿里云商标注册申请进度查询方法
  19. 数据仓库系列文章一:浅谈数仓设计
  20. 获取的字段值是空值或者为null,而你自己的需求就是想要获取的字段为一个 * 默认的值

热门文章

  1. dos2unix和unix2dos
  2. STM32 W5500 OTA功能 - bootloader及app的设计和实现
  3. ssm框架搭建流程及原理分析
  4. 华为 USG6000防火墙配置镜像模式双机热备
  5. 2021最新版CDA数据分析认证模拟题库
  6. DIY TCP/IP IP模块和ICMP模块的实现1
  7. 步进电机驱动器设计c语言软件,最新基于单片机系统的步进电机驱动STC单片机步进电机驱动器的设计及C语言程序.doc...
  8. java List集合去重保持原顺序
  9. 1024程序员节:谈谈自我感受
  10. UDP 不阻塞的原因