安卓--前端&;lt;视频和标签原生化
作者:访客发布时间:2023-12-13分类:程序开发学习浏览:97
目录
1.方案介绍
2.实现
3.总结
1.方案介绍
1.1Video<;什么是“前端>;标签原生化”
上图这个页面里,我开启了“开发者模式-显示布局边界”选项.可以看到一开始视频是前端
<video>
,点击播放之后,视频变成了Android
视频控件.其核心做法就是在前端<video>
上面添加一个一模一样的Android
视频控件,同时支持同步滚动.我把这个称为“前端<video>
标签原生化“。1.2需求背景
众所周知,
Android
系统五花八门,不同系统对前端<video>
的实现也是比较简陋且不统一,无法统一支持倍速播放、投屏、画中画等功能;前端<video>
在全屏时,也容易出现奇怪的问题,比如:Webview播放视频的方式和坑点记录。总之,功能简陋不统一、容易出现问题、响应速度不及原生等,都是前端播放视频亟需解决的问题.市面上很多浏览器应用程序都是采用的上面的方案,比如夸克(下边左图)、QQ浏览器(下边右图)等。所以“前端
<video>
标签原生化“应该是业界比较认同的处理方式.
2.实现
2.1相关查看组成
- FrameLayout - WebView(底层) - WebVideo2NativeVideo(顶层) - FrameLayout - FrameLayout - VideoView - VideoView - ...
上面是页面的结构树.从大的关系来看,页面主要由如下两部分组成:
底层的网络视图
底层的
WebView
很好理解,就是用于展示前端页面的容器.顶层的网络视频2原生视频
一个继承ScrollView的自定义类,核心逻辑基本在这个类。因为前端页面可能可以滚动,所以顶层基于
ScrollView
做定制.ScrollView
内嵌了两层FrameLayout
(两层)FrameLayout
的原因在下一个小节解释),FrameLayout
里面是VideoView
,这些VideoView
在位置上与前端<video>
一一对应.
2.2管理原生视频观看
首先,我们需要在
Android
端为前端<video>
添加对应的VideoView
,VideoView
的位置、视频信息需要与前端<video>
保持一致.主要涉及下面两部分:2.2.1添加视频观看
当前端
<video>
标签被点击时,获取<video>
标签的相关信息(下面称:VideoInfo
)、然后在Android
端添加对应的VideoView
那就是。实现上述步骤的方式可能有很多种,比如:利用
WebView.evaluateJavascript
全部由Android
端实现、利用WebView.addJavascript
由Android
端和前端配合实现......下面介绍
Android
端和前端配合的方案.首先在前端给每个<video>
添加play
监听,在视频开始播放时,把VideoInfo
传递给Android
端:const postVideoPlay = () => { function getVideoInfo() { let video = videoRef.value; var rect = video.getBoundingClientRect(); return { 'scrollHeight': document.body.scrollHeight, 'width': rect.width, 'height': rect.height, 'left': rect.left, 'top': rect.top + window.scrollY, 'url': video.src, 'poster': video.poster }; } // 调用约定好的ydk方法 window.ydk.onVideoPlay(getVideoInfo()) } videoRef.value.addEventListener('play', postVideoPlay);
至此前端
<video>
完成使命,但由于前端页可能被其他端使用,所以把<video>
的暂停操作放在Android
端去做.val pauseWebVideoJS = """ try { var url = "${videoInfo.url}"; let selector = 'video[src="' + url + '"]'; let video = document.querySelector(selector) video.pause(); } catch (e) { console.log(e); } """.trimIndent() getWebView().evaluateJavascript(pauseWebVideoJS, null)
把前端
<video>
暂停后,我们需要利用VideoInfo
在Android
端添加一个VideoView
那就是。// WebVideo2NativeVideo fun addVideoView(...) { if (mIsFirstAddVideoView) { mIsFirstAddVideoView = false // PS:1 val scrollViewChild = FrameLayout(context) addView(scrollViewChild) scrollViewChild.addView(mVideoViewContainer) } setContainerLayoutParams(webView, videoInfo) ... mVideoViewContainer.addView(videoView) } private fun setContainerLayoutParams(webView: WebView, videoInfo: VideoInfo) { // 设置整个大容器的高度 mVideoViewContainer.updateLayoutParams { height = SizeUtils.dp2px(videoInfo.scrollHeight.toFloat()) } ... }
上面有两个需要注意的地方:
PS:1前面提到
WebVideo2NativeVideo
是自定义的ScrollView
,ScrollView
比较特殊,measure
时会强制将子View
的Height
设置成UNSPECIFIED
,导致我们无法设置子View
的具体高度,即我们无法设置承载VideoView
的FrameLayout
的高度,会影响VideoView
的定位和滚动.所以这里我们额外增加一层FrameLayout
来解决这个问题.这个也就是前面提到“两层框架布局”的原因
PS:2前端返回的
VideoInfo
是以dp
为单位的,这里需要转一下.2.2.2更新视频观看
实际开发时,可能遇到屏幕旋转等情况,会导致先前设置的尺寸信息不适用,我们需要在
Activity.onConfigurationChanged
方法中判断方向是否发生变化.override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) val newScreenOrientation = newConfig.orientation if (newScreenOrientation != mScreenOrientation) { mScreenOrientation = newScreenOrientation val refreshVideoInfoTask = Runnable { val getVideoInfoJS = """ function getVideoInfo() { // PS:1 let videos = document.getElementsByTagName('video'); const resultList = []; Array.from(videos).forEach(video => { ... }); return resultList; } getVideoInfo(); """.trimIndent() getWebView().evaluateJavascript(getVideoInfoJS) { value -> if (value != null) { ... mWebVideo2NativeVideo.updateLayoutParams(getWebView(), videoInfoList) } } } if (mWebVideo2NativeVideo.haveAddedVideoView) { // PS:2 mWebVideo2NativeVideo.postDelayed(refreshVideoInfoTask, 100) } } }
上面有两个需要注意的地方:
PS:1屏幕旋转时,可能已经添加了多个
VideoView
,所以需要一次性获取所有VideoInfo
那就是。PS:2实践证明,
onConfigurationChanged
回调时,获取到的VideoInfo
还是旋转前的,所以这里通过postDelayed
处理.拿到所有
VideoInfo
后,我们通过VideoInfo.url
匹配更新已有的VideoView
那就是。// WebVideo2NativeVideo fun updateLayoutParams(webView: WebView, newVideoInfoList: List<VideoInfo>) { if (newVideoInfoList.isNotEmpty()) { setContainerLayoutParams(webView, newVideoInfoList[0]) mVideoViewContainer.children.forEach { child -> val videoView = child as EduVideoView val newVideoInfo = newVideoInfoList.find { it.url == videoView.mediaInfo.videoUrl } if (newVideoInfo != null) { setVideoViewLayoutParams(videoView, newVideoInfo) } } } }
2.3处理触摸事件
经过上一步,我们已经可以在前端
<video>
上面盖一层VideoView
、如上图所示.但是当用户滑动时,页面会露馅.所以这一步,我们要让VideoView
与前端<video>
同步滚动,那样才不会露馅.(对)Android
触摸事件不熟悉的,建议先看下这个文章:安卓触摸事件分发机制详解)2.3.1分析需要处理的问题
- FrameLayout(父容器) - WebView(底层) - WebVideo2NativeVideo(顶层) - FrameLayout - FrameLayout - VideoView
先回顾下页面的结构树,默认情况下,触摸事件的传递是这样的(省略与触摸事件传递关系不大的两层
FrameLayout
):由于
WebView
和WebVideo2NativeVideo
同属于FrameLayout
且WebView
位于底层,所以触摸事件是不会传递给它的.但WebView
是需要响应触摸事件的,所以问题1:如何把触摸事件传递给网络浏览?WebView
的上层还有VideoView
,VideoView
同样需要响应触摸事件且优先级更高.所以当触摸事件落在VideoView
上面时,我们需要选择性的消费一些事件,所以问题2:VideoView
如何选择性的消费事件?假设我们把前面两个问题解决了,触摸事件可以给到
WebView
,这个时候WebView
能响应点击事件,滑动事件等.也就是说前端<video>
可以滚动了,那为了让VideoView
也可以滚动,我们需要让WebVideo2NativeVideo
这个容器也可以滚动,所以问题3:如何让WebVideo2NativeVideo
和WebView
同步滚动?2.3.2解决问题
下面按照事件响应的优先级,逐步分析解决上面的问题.
问题2:
VideoView
如何选择性的消费事件?当触摸事件落在VideoView
时,我们才需要考虑这个问题.解决这个问题,需要从功能需求出发.目前的需求,VideoView
需要响应左右滑动、点击等,但不响应上下滑动,所以统一把上下滑动事件过滤.做法也比较简单粗暴,就是在
VideoView
(VideoView
本身是个FrameLayout
容器)的onInterceptTouchEvent
方法进行判断拦截.// VideoView private var mDownX = 0F private var mDownY = 0F override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { when (ev?.action) { MotionEvent.ACTION_DOWN -> { mDownX = ev.rawX mDownY = ev.rawY } MotionEvent.ACTION_MOVE -> { val moveX = ev.rawX val moveY = ev.rawY val diffX: Float = Math.abs(moveX - mDownX) val diffY: Float = Math.abs(moveY - mDownY) if (diffY > diffX) { return true } } } return false }
问题1:如何把触摸事件传递给网络浏览?上一步中,
VideoView
消费了点击、左右滑动等事件.那么剩下的事件就是:没落在VideoView
上的事件、落在VideoView
上的上下滑动事件.这些事件最终会来到WebVideo2NativeVideo
,我们需要把这些事件传给WebView
那就是。于是我们需要对
WebVideo2NativeVideo
的onTouchEvent
做特殊处理.// WebVideo2NativeVideo private var mWebView: WebView? = null private var isTouching = false override fun onTouchEvent(ev: MotionEvent?): Boolean { if (ev?.action == MotionEvent.ACTION_DOWN) { isTouching = true } else if (ev?.action == MotionEvent.ACTION_MOVE) { if (!isTouching) { // PS: 1 val mockDown = MotionEvent.obtain(ev).also { it.action = MotionEvent.ACTION_DOWN } mWebView?.dispatchTouchEvent(mockDown) } isTouching = true } else if (ev?.action == MotionEvent.ACTION_UP || ev?.action == MotionEvent.ACTION_CANCEL) { isTouching = false } return mWebView?.dispatchTouchEvent(ev) ?: false }
做法同样比较简单,直接把触摸事件传给
WebView
,但有个地方需要注意下:PS:1IsTouting为False,代表“没被
VideoView
消费的上下滑动事件“。这些滑动事件直接传递给WebView
是没用的,因为一个完整的事件流还需要ACTION_DOWN
事件,所以我们要模拟一个ACTION_DOWN
事件,那样WebView
才会响应.问题3:如何让
WebVideo2NativeVideo
和WebView
同步滚动?经过上一步,WebView
已经可以滚动了,为了WebVideo2NativeVideo
可以同步滚动,我们利用WebView
的setOnScrollChangeListener
简单处理.// WebVideo2NativeVideo fun syncScroll(webView: WebView) { this.scrollY = webView.scrollY webView.setOnScrollChangeListener { _, _, scrollY, _, _ -> this.scrollY = scrollY } mWebView = webView }
2.4%处理页面细节
经过上面的处理,
VideoView
与前端<video>
就可以同步滚动了,但有些细节还需要处理下.2.4.1页面上下填充
如上图所示,由于
VideoView
是Android
端添加的,不受前端ToolBar
遮挡,所以在向上滑动过程中,Android
端的VideoView
没有很好的隐藏,导致露馅.所以我们需要给
WebVideo2NativeVideo
添加topPadding
,同时不分发落在topPadding
内的触摸事件(原因见下面),topPadding
的大小可以来自前端.如果底部也有类似前端ToolBar
的前端BottomBar
,那么需要类似的处理WebVideo2NativeVideo
的bottomPadding
那就是。如果分发
topPadding
内的触摸事件,如果刚好VideoView
在topPadding
区域内,会导致触摸事件被VideoView
消费,传递不到前端,从而导致前端的控件无法响应(如:返回键)。// WebVideo2NativeVideo fun setExtraPadding(topPadding: Int = paddingTop, bottomPadding: Int = paddingBottom) { setPadding(0, topPadding, 0, bottomPadding) } override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { if (ev != null) { if (ev.y <= paddingTop || ev.y >= (height - paddingBottom)) { return false } } return super.dispatchTouchEvent(ev) }
2.4.2前端弹窗处理
如上图所示,前端的弹窗会被
VideoView
遮挡,所以需要前端在弹窗的时候,告诉Android
端,Android
端做隐藏.
3.总结
经过一番操作,就可以达到上面的效果.把前端<video>
转化为Android
的VideoView
后,可以更好的拓展一些视频的能力,如画中画、投屏、视频下载等.
相关推荐
- 如何在视频嵌入周围添加IFRAME边框
- 如何从YouTube视频自动创建wordpress帖子
- 如何在WordPress的评论栏中添加用户角色标签
- 如何在wordPress中添加共享按钮作为Youtube视频的覆盖
- 9个有用的Youtube小贴士,用视频给你的wordpress网站增添情趣
- 如何给您的WordPress管理仪表板贴上白色标签
- 如何在WordPress中添加特色视频缩略图
- WordPress的9个最佳YouTube视频集插件
- 如何使用WordPress在线销售视频
- 在 Android 上使用 MediaExtractor 和 MediaMuxer 提取视频_提取音频_转封装_添加音频等操作
- 程序开发学习排行
-
- 1鸿蒙HarmonyOS:Web组件网页白屏检测
- 2HTTPS协议是安全传输,为啥还要再加密?
- 3HarmonyOS鸿蒙应用开发——数据持久化Preferences
- 4记解决MaterialButton背景颜色与设置值不同
- 5鸿蒙HarmonyOS实战-ArkUI组件(RelativeContainer)
- 6鸿蒙HarmonyOS实战-ArkUI组件(Stack)
- 7[Android][NDK][Cmake]一文搞懂Android项目中的Cmake
- 8Android广播如何解决Sending non-protected broadcast问题
- 9鸿蒙HarmonyOS实战-ArkUI组件(mediaquery)
- 最近发表
-
- WooCommerce最好的WordPress常用插件下载博客插件模块的相关产品
- 羊驼机器人最好的WordPress常用插件下载博客插件模块
- IP信息记录器最好的WordPress常用插件下载博客插件模块
- Linkly for WooCommerce最好的WordPress常用插件下载博客插件模块
- 元素聚合器Forms最好的WordPress常用插件下载博客插件模块
- Promaker Chat 最好的WordPress通用插件下载 博客插件模块
- 自动更新发布日期最好的WordPress常用插件下载博客插件模块
- WordPress官方最好的获取回复WordPress常用插件下载博客插件模块
- Img to rss最好的wordpress常用插件下载博客插件模块
- WPMozo为Elementor最好的WordPress常用插件下载博客插件模块添加精简版