前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >小窗播放视频的原理和实现(下)

小窗播放视频的原理和实现(下)

作者头像
QQ音乐技术团队
发布2018-03-01 14:10:30
4.3K2
发布2018-03-01 14:10:30
举报

本文对小窗视频播放进行了详细的研究,针对几种实现方案进行了深入的对比分析,进而给出实现小窗视频播放的最优解。其中通过对系统源码的分析,详细探究了如何完美地实现移动、缩放等效果,很有技术深度。文中几种方案的对比,以及SurfaceViewGLSurfaceViewTextureView相关知识点的讲解,非常实用,值得收藏。 — 责任编辑 junyihan

回顾上篇小窗播放视频的原理和实现(上),SurfaceView在它所在的位置上创建一个新的Window,Window创建一个独立的Surface,显示内容渲染在独立的Surface中,通过在宿主窗口上“挖洞”来显示它。这使得SurfaceView的绘制可以在单独的线程中进行,从而可以绘制复杂的内容。由于SurfaceView的内容没有显示在宿主窗口中, 这样它的显示需要同步宿主窗口的变化。所以它会出现以下情况:它在执行移动和缩放时,会有黑边;在执行旋转时,画面不会跟随旋转;执行透明值动画时,显示有问题。在Android N以上的设备上,SurfaceView执行移动、缩放和旋转时会同步变化,不会看到黑边。TextureView作为普通View在View hierarchy中管理与绘制,在执行移动、缩放、旋转和透明度动画时不会出现异常,更适用于小窗播放视频功能。但TextureView需要硬件加速层,也就是必须使用GPU绘制,使得TextureView比SurfaceView和GLSurfaceView更耗性能、更耗电。

接下来通过实例演示来证明上面的结论。

一、实例演示

以下以MedioPlayer播放视频为例,演示SurfaceViewTextureView在执行移动、缩放、旋转和透明度动画时的效果。实例代码在文章末尾。

1、Android L设备上的动画对比

图1Android L设备上SurfaceView执行动画
图1Android L设备上SurfaceView执行动画
图2 Android L设备上TextureView执行动画
图2 Android L设备上TextureView执行动画

在Android L的设备上,SurfaceView在执行移动、缩放动画时,有黑边;旋转动画时,它的画面不会跟随旋转,有黑边;执行透明动画时,画面先消失,直到动画结束才再次显示画面,说明SurfaceView不支持透明度动画。TextureView执行动画时,效果和普通View一样。

2、Android N设备上的动画对比

图3 Android N设备上SurfaceView执行动画
图3 Android N设备上SurfaceView执行动画
图4 Android N设备上TextureView执行动画
图4 Android N设备上TextureView执行动画

在Android N的设备上,SurfaceView在执行移动和缩放动画时,没有黑边;执行旋转动画时,它的画面没有跟随旋转;执行透明动画时,画面先消失,直到动画结束才再次显示画面,说明SurfaceView不支持透明度动画。因为Android N上SurfaceView的新特性,执行动画时,它的Surface会同步变化,使得它不会出现黑边。TextureView执行动画时,效果和普通View一样。

3、Android N设备上的滑动对比

图5 Android N设备上SurfaceView执行滑动
图5 Android N设备上SurfaceView执行滑动
图6 Android N设备上TextureView执行滑动
图6 Android N设备上TextureView执行滑动

在Android N的设备上,执行滑动和缩放操作时,SurfaceView有黑边,TextureView没有黑边。这里的滑动和缩放操作是通过修改SurfaceView的LayoutParam来实现的,而不是执行动画。

二、交互时无缝播放视频

在大屏和小窗之间切换时,因为重新创建了播放器,导致需要重新加载视频,不能平滑的过渡。通过单例播放器,将视频渲染到大屏和小窗视频控件,这样可以做到无缝播放视频,平滑加载视频,给用户平滑的过渡体验。

了解小窗播放视频原理后,那么有哪些方案可以实现小窗播放视频功能呢?以下对这些方案进行对比分析。

三、小窗播放视频的实现

1、视频播放控件内嵌到应用布局

如下代码所示,将TextureView内嵌到应用布局内,父容器是一个可以跟随手势缩放的控件——DragVideoView,同时还有一个View用来展示视频的描述,这样将视频播放页分为播放器(Player)和描述(Desc)。

代码语言:javascript
复制
<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的视频跟随手指滑动的效果。

代码语言:javascript
复制
//先测量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滑动。

代码语言:javascript
复制
//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 效果一样的小窗播放视频功能。优点是交互好,交互时平滑播放视频;缺点是只能在应用内小窗播放。

2、WindowManager添加视频播放控件

WindowManagerService管理着多种窗口,如Activity中的PhoneWindow、壁纸窗口(Wallpaper Winodw)、弹出的子窗口(Sub Window),状态栏(Status Bar)以及输入法窗口(Input Method Window)等。应用程序添加窗口到WindowManagerService,是通过调用WindowManagerService的addWindow方法添加的。

代码语言:javascript
复制
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类型,使用它们必须拥有特别的权限。

代码语言:javascript
复制
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,从而无法做到流畅的交互。所以使用这种方案的优点是可以在应用内外播放视频;缺点是需要权限,交互差。

3、Android8.0 的画中画

Android8.0 的画中画功能允许用户将播放视频缩小并显示到其他窗口上方。优点是实现简单,缺点是需要兼容8.0以前的设备。

4、Activity的Dialog模式

Dialog模式的Activity可以悬浮在其他Activity之上。Activity创建了PhoneWindow,通过PhoneWindow显示View,如下图所示表现了ActivityWindowView的关系。

(图7 Activity、Window、View的关系)

如下源码所示,进一步分析ActivityWindowView的关系。从Activity的setContentView()开始,setContentView调用了Window的setContentView方法。这里初始化了DecorView,DecorView可以添加titlebar、contentView等。

代码语言:javascript
复制
//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。

代码语言:javascript
复制
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了。

代码语言:javascript
复制
//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添加视频播放控件和视频播放控件内嵌到应用布局。

五、Demo

  1. github地址:https://github.com/FightingLarry/FloatWindowDemo
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-01-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 QQ音乐技术团队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、实例演示
    • 1、Android L设备上的动画对比
      • 2、Android N设备上的动画对比
        • 3、Android N设备上的滑动对比
        • 二、交互时无缝播放视频
        • 三、小窗播放视频的实现
          • 1、视频播放控件内嵌到应用布局
            • 2、WindowManager添加视频播放控件
              • 3、Android8.0 的画中画
                • 4、Activity的Dialog模式
                • 四、结论
                • 五、Demo
                相关产品与服务
                容器服务
                腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档