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

本文对小窗视频播放进行了详细的研究,针对几种实现方案进行了深入的对比分析,进而给出实现小窗视频播放的最优解。其中通过对系统源码的分析,详细探究了如何完美地实现移动、缩放等效果,很有技术深度。文中几种方案的对比,以及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执行动画
图2 Android L设备上TextureView执行动画

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

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

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

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

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

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

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

二、交互时无缝播放视频

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

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

三、小窗播放视频的实现

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

如下代码所示,将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 效果一样的小窗播放视频功能。优点是交互好,交互时平滑播放视频;缺点是只能在应用内小窗播放。

2、WindowManager添加视频播放控件

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

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等。

//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添加视频播放控件和视频播放控件内嵌到应用布局。

五、Demo

  1. github地址:https://github.com/FightingLarry/FloatWindowDemo

原文发布于微信公众号 - QQ音乐技术团队(gh_287053a877e6)

原文发表时间:2018-01-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏知识分享

3-系统方案A(Activity界面跳转,携带数据,显示曲线界面)

https://www.cnblogs.com/yangfengwu/p/9970387.html

962
来自专栏Android干货

Android项目实战(四十一):游戏和视频类型应用 状态栏沉浸式效果

3356
来自专栏向治洪

SliferMenu详解

SlidingMenu简介: SlidingMenu的是一种比较新的设置界面或配置界面效果,在主界面左滑或者右滑出现设置界面,能方便的进行各种操作.目前有大...

1985
来自专栏androidBlog

使用CoordinatorLayout打造各种炫酷的效果

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/gdutxiaoxu/article/details...

6681
来自专栏何俊林

如何优化你的布局层级结构之RelativeLayout和LinearLayout及FrameLayout性能分析(一)

工作一段时间后,经常会被领导说,你这个进入速度太慢了,竞品的进入速度很快,你搞下优化吧?每当这时,你会怎么办?功能实现都有啊,进入时要加载那么多view,这也没...

3829
来自专栏程序员叨叨叨

听说你想用ViewPager实现这样的效果?

此图盗于https://github.com/smallnew/FuCardPager

983
来自专栏Android干货园

Android--仿淘宝商品详情(继续拖动查看详情)及标题栏渐变

版权声明:本文为博主原创文章,转载请标明出处。 https://blog.csdn.net/lyhhj/article/details/52...

1691
来自专栏Android源码框架分析

仿淘宝、京东拖拽商品详情(可嵌套ViewPager、ListView、WebView、FragmentTabhost)实现效果图实现

1893
来自专栏Android机器圈

Android图片处理--全景查看效果

PS:Android对于图片处理这块资源还是挺多的,之前用OpenGL制作图片的全景效果,耗时耗力,而且只能点击进去后看到,但是效果是非常的号,今天所写的是编写...

2263
来自专栏向治洪

android 仿音悦台页面交互效果

概述 新版的音悦台 APP 播放页面交互非常有意思,可以把播放器往下拖动,然后在底部悬浮一个小框,还可以左右拖动,然后回弹的时候也会有相应的效果,这种交互效果在...

2507

扫码关注云+社区

领取腾讯云代金券