目录
1. 计划介绍
2. 完成
3. 总结
1. 计划介绍
-
1.1 什么是”前端<video>标签原生化“
上图这个页面里,我敞开了“开发者模式-显现布局鸿沟”选项。能够看到一开始视频是前端
<video>
,点击播映之后,视频变成了Android
视频控件。其核心做法便是在前端<video>
上面添加一个一模一样的Android
视频控件,一起支撑同步翻滚。我把这个称为“前端<video>
标签原生化”。 -
1.2 需求布景
众所周知,
Android
系统形形色色,不同系统对前端<video>
的完成也是比较简陋且不一致,无法一致支撑倍速播映、投屏、画中画等功能;前端<video>
在全屏时,也简略出现古怪的问题,比方:WebView播映视频的方式和坑点记载。总归,功能简陋不一致、简略出现问题、呼应速度不及原生等,都是前端播映视频亟需处理的问题。市面上很多浏览器APP都是采用的上面的计划,比方夸克(下边左图)、QQ浏览器(下边右图)等。所以“前端
<video>
标签原生化”应该是业界比较认同的处理方式。
2. 完成
-
2.1 相关View组成
- FrameLayout - WebView(底层) - WebVideo2NativeVideo(顶层) - FrameLayout - FrameLayout - VideoView - VideoView - ...
上面是页面的结构树。从大的关系来看,页面首要由如下两部分组成:
-
底层的WebView
底层的
WebView
很好理解,便是用于展示前端页面的容器。 -
顶层的WebVideo2NativeVideo
一个继承ScrollView的自定义类,核心逻辑基本在这个类。因为前端页面或许能够翻滚,所以顶层根据
ScrollView
做定制。ScrollView
内嵌了两层FrameLayout
(两层FrameLayout
的原因鄙人一个小节解说),FrameLayout
里边是VideoView
,这些VideoView
在方位上与前端<video>
一一对应。
-
-
2.2 办理原生VideoView
首要,咱们需求在
Android
端为前端<video>
添加对应的VideoView
,VideoView
的方位、视频信息需求与前端<video>
保持一致。首要触及下面两部分:-
2.2.1 添加VideoView
当前端
<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
端去做。// Activity 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
来处理这个问题。这个也便是前面提到“两层FrameLayout”的原因
PS:2 前端返回的
VideoInfo
是以dp
为单位的,这儿需求转一下。 -
2.2.2 更新VideoView
实践开发时,或许遇到屏幕旋转等情况,会导致先前设置的尺度信息不适用,咱们需求在
Activity.onConfigurationChanged
方法中判别方向是否发生变化。// Activity 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
接触事情不熟悉的,主张先看下这个文章:Android接触事情分发机制详解)-
** 2.3.1 剖析需求处理的问题**
- FrameLayout(父容器) - WebView(底层) - WebVideo2NativeVideo(顶层) - FrameLayout - FrameLayout - VideoView
先回顾下页面的结构树,默认情况下,接触事情的传递是这样的(省掉与接触事情传递关系不大的两层
FrameLayout
):因为
WebView
和WebVideo2NativeVideo
同属于FrameLayout
且WebView
坐落底层,所以接触事情是不会传递给它的。但WebView
是需求呼应接触事情的,所以问题1:怎么把接触事情传递给WebView?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:怎么把接触事情传递给WebView? 上一步中,
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: 1 isTouching为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 页面上下padding
如上图所示,因为
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
后,能够更好的拓宽一些视频的能力,如画中画、投屏、视频下载等。