本文对小窗视频播放进行了详细的研究,针对几种实现方案进行了深入的对比分析,进而给出实现小窗视频播放的最优解。其中通过对系统源码的分析,详细探究了如何完美地实现移动、缩放等效果,很有技术深度。文中几种方案的对比,以及
SurfaceView
、GLSurfaceView
和TextureView
相关知识点的讲解,非常实用,值得收藏。 — 责任编辑 junyihan
回顾上篇小窗播放视频的原理和实现(上),SurfaceView在它所在的位置上创建一个新的Window,Window创建一个独立的Surface,显示内容渲染在独立的Surface中,通过在宿主窗口上“挖洞”来显示它。这使得SurfaceView的绘制可以在单独的线程中进行,从而可以绘制复杂的内容。由于SurfaceView的内容没有显示在宿主窗口中, 这样它的显示需要同步宿主窗口的变化。所以它会出现以下情况:它在执行移动和缩放时,会有黑边;在执行旋转时,画面不会跟随旋转;执行透明值动画时,显示有问题。在Android N以上的设备上,SurfaceView执行移动、缩放和旋转时会同步变化,不会看到黑边。TextureView作为普通View在View hierarchy中管理与绘制,在执行移动、缩放、旋转和透明度动画时不会出现异常,更适用于小窗播放视频功能。但TextureView需要硬件加速层,也就是必须使用GPU绘制,使得TextureView比SurfaceView和GLSurfaceView更耗性能、更耗电。
接下来通过实例演示来证明上面的结论。
以下以MedioPlayer播放视频为例,演示SurfaceView
和TextureView
在执行移动、缩放、旋转和透明度动画时的效果。实例代码在文章末尾。
在Android L的设备上,SurfaceView在执行移动、缩放动画时,有黑边;旋转动画时,它的画面不会跟随旋转,有黑边;执行透明动画时,画面先消失,直到动画结束才再次显示画面,说明SurfaceView不支持透明度动画。TextureView执行动画时,效果和普通View一样。
在Android N的设备上,SurfaceView在执行移动和缩放动画时,没有黑边;执行旋转动画时,它的画面没有跟随旋转;执行透明动画时,画面先消失,直到动画结束才再次显示画面,说明SurfaceView不支持透明度动画。因为Android N上SurfaceView的新特性,执行动画时,它的Surface会同步变化,使得它不会出现黑边。TextureView执行动画时,效果和普通View一样。
在Android N的设备上,执行滑动和缩放操作时,SurfaceView有黑边,TextureView没有黑边。这里的滑动和缩放操作是通过修改SurfaceView的LayoutParam来实现的,而不是执行动画。
在大屏和小窗之间切换时,因为重新创建了播放器,导致需要重新加载视频,不能平滑的过渡。通过单例播放器,将视频渲染到大屏和小窗视频控件,这样可以做到无缝播放视频,平滑加载视频,给用户平滑的过渡体验。
了解小窗播放视频原理后,那么有哪些方案可以实现小窗播放视频功能呢?以下对这些方案进行对比分析。
如下代码所示,将TextureView内嵌到应用布局内,父容器是一个可以跟随手势缩放的控件——DragVideoView,同时还有一个View用来展示视频的描述,这样将视频播放页分为播放器(Player
)和描述(Desc
)。
<com.iamlarry.floatwindowdemo.drag.DragVideoView
android:id="@+id/drag_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextureView
android:id="@+id/player"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ff000000" />
<View
android:id="@+id/desc"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorAccent" />
</com.iamlarry.floatwindowdemo.drag.DragVideoView>
如下DragVideoView的代码所示,在onMeasure中,测量Player和Desc的宽高。在onLayout时,Desc的大小和位置会受到Player的大小和mTop的影响;Player也会受到mTop的影响。mTop就是手势滑动时距离屏幕最上方的距离,这样就做到了如图5、图6的视频跟随手指滑动的效果。
//先测量Player和Desc的宽高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measurePlayer(widthMeasureSpec, heightMeasureSpec);
measureDesc(widthMeasureSpec, heightMeasureSpec);
}
//将Desc放置到Player的下面
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mDragDirect != HORIZONTAL) {
mLeft = this.getWidth() - this.getPaddingRight() - this.getPaddingLeft() - mPlayer.getMeasuredWidth();
mDesc.layout(mLeft, mTop + mPlayer.getMeasuredHeight(), mLeft + mDesc.getMeasuredWidth(), mTop + mDesc.getMeasuredHeight());
}
mPlayer.layout(mLeft, mTop, mLeft + mPlayer.getMeasuredWidth(), mTop + mPlayer.getMeasuredHeight());
}
如下代码所示,DragVideoView的手势交给DragHelper管理。在slideVerticalTo方法中计算mPlayer的起始位置,控制Player滑动,从而带动Desc滑动。
//DragVideoView.java
//手势交给DragHelper管理
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return mDragHelper.shouldInterceptTouchEvent(event);
}
//滑动到垂直方向上某位置
private boolean slideVerticalTo(float slideOffset) {
int topBound = mMinTop;
int y = (int) (topBound + slideOffset * mVerticalRange);
if (mDragHelper.smoothSlideViewTo(mPlayer, mIsMinimum ?
(int) (mPlayerMaxWidth * (1 - PLAYER_RATIO)) : getPaddingLeft(), y)) {
ViewCompat.postInvalidateOnAnimation(this);
return true;
}
return false;
}
通过以上代码的实现,可以做到和 YouTube 效果一样的小窗播放视频功能。优点是交互好,交互时平滑播放视频;缺点是只能在应用内小窗播放。
WindowManagerService管理着多种窗口,如Activity中的PhoneWindow、壁纸窗口(Wallpaper Winodw)、弹出的子窗口(Sub Window),状态栏(Status Bar)以及输入法窗口(Input Method Window)等。应用程序添加窗口到WindowManagerService,是通过调用WindowManagerService的addWindow方法添加的。
public class WindowManagerService extends IWindowManager.Stub …… {
public int addWindow(Session session, IWindow client,……) {
if (attrs.type == TYPE_INPUT_METHOD) {
} else if (attrs.type == TYPE_INPUT_METHOD_DIALOG) {
} else {
if (attrs.type == TYPE_WALLPAPER) {
adjustWallpaperWindowsLocked();
} else if ((attrs.flags&FLAG_SHOW_WALLPAPER) != 0) {
adjustWallpaperWindowsLocked();
}
}
}
}
如上源码所示,attrs.type是Window类型,用来决定Window的显示高度,可以理解为窗口位置的Z轴,Z轴越大,显示在越上层。通常有三种Window类型:
1)Application windows
取值范围从FIRST_APPLICATION_WINDOW
(0x00000001)到 LAST_APPLICATION_WINDOW
(0x00000063),这种Window是普通的顶层Window,应用的层级都在这个范围。
2)Sub windows
取值范围从FIRST_SUB_WINDOW
(0x000003e8)到 LAST_SUB_WINDOW
(0x000007cf) ,这种window一般都和其他顶层window关联在一起,所有的应用都在FIRST_SUB_WINDOW
之下。
3)System windows
取值范围为从 FIRST_SYSTEM_WINDOW
(0x000007d0) 到 LAST_SYSTEM_WINDOW
(0x00000bb7),这种window是特殊的window类型,使用它们必须拥有特别的权限。
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT,WindowManager.LayoutParams.WRAP_CONTENT,0, 0,PixelFormat.TRANSPARENT);
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
layoutParams.gravity = Gravity.CENTER;
WindowManager windowManager = getWindowManager();
windowManager.addView(floatingButton, layoutParams);
如上代码所示,是使用WindowManager添加视频播放控件的代码,设置type为TYPE_PHONE,可以浮在所有应用之上。通过WindowManager的addView添加的View,会创建新的Window。交互时的滑动手势不能从Acitvity转移到WindowManager,从而无法做到流畅的交互。所以使用这种方案的优点是可以在应用内外播放视频;缺点是需要权限,交互差。
Android8.0 的画中画功能允许用户将播放视频缩小并显示到其他窗口上方。优点是实现简单,缺点是需要兼容8.0以前的设备。
Dialog模式的Activity可以悬浮在其他Activity之上。Activity创建了PhoneWindow,通过PhoneWindow显示View,如下图所示表现了Activity
、Window
、View
的关系。
(图7 Activity、Window、View的关系)
如下源码所示,进一步分析Activity
、Window
、View
的关系。从Activity的setContentView()开始,setContentView调用了Window的setContentView方法。这里初始化了DecorView,DecorView可以添加titlebar、contentView等。
//Activity#setContentView
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
//PhoneWindow#setContentView
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
//这里初始化了DecorView
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
通过上面的分析,了解到Activity创建了Window,Window显示了View,可以通过修改Window的属性来移动Dialog模式的Activity。如下代码所示,Activity通过getWindow()得到Window,通过getAttributes获取WindowManager.LayoutParams,最后通过setAttributes来更新WindowManager.LayoutParams。
Window window = getWindow();
WindowManager.LayoutParams layoutParams = window.getAttributes();
layoutParams.y = (int) y;
layoutParams.x = (int) (y * lastX / lastY);
layoutParams.width = QQMusicUIConfig.getWidth() - layoutParams.x;
layoutParams.height = QQMusicUIConfig.getHeight() - layoutParams.y;
window.setAttributes(layoutParams);
以下是Window.setAttributes()的代码,dispatchWindowAttributesChanged会调用到Activity的onWindowAttributesChanged方法。最后调用了WindowManager的updateViewLayout方法,这个方法就是用来更新Window属性的。这样可以移动和缩放Dialog模式的Activity了。
//Window.setAttributes()
public void setAttributes(WindowManager.LayoutParams a) {
mWindowAttributes.copyFrom(a);
dispatchWindowAttributesChanged(mWindowAttributes);
}
//Activity.java
public void onWindowAttributesChanged(WindowManager.LayoutParams params) {
if (mParent == null) {
View decor = mDecor;
if (decor != null && decor.getParent() != null) {
getWindowManager().updateViewLayout(decor, params);
}
}
}
通过以上代码就可以实现Activity的浮动窗口功能了。但是在拖拽时,视频播放时会有黑边。优点是实现简单,缺点是滑动时会视频播放有黑边。
通过实例演示了解到,SurfaceView在执行移动和缩放时,会有黑边;在执行旋转时,画面不会跟随旋转;执行透明值动画时,显示有问题。在Android N以上的设备上,SurfaceView执行移动、缩放和旋转时会同步变化,不会看到黑边。TextureView执行动画时,在执行移动、缩放、旋转和透明度动画时不会出现异常,更适用于小窗播放视频功能。
在大屏和小窗之间切换时,使用单例播放器实现无缝播放视频,平滑加载视频,给用户平滑的过渡体验。
以上四种方案都可以实现小窗播放视频功能,各方案或多或少都有缺点。最适合做小窗播放视频功能的是WindowManager添加视频播放控件和视频播放控件内嵌到应用布局。