不知道大家有没有遇到过在利用VideoView播放视频的时候会遇到右边缺一块、或者底部缺一块,不能铺满的情况,反正我是遇到了的,所以在此做个总结:

1、原生VideoView的效果,这里没有让底部的导航栏也变透明、并且没有隐藏状态栏

html5中video标签如何设置全屏 video标签无法全屏_android

视频:1080*1920 模拟器也是1080*1920

先说说结论:这里如果隐藏状态栏和去掉底部导航就是全屏,也不会缺失右边这一块的

html5中video标签如何设置全屏 video标签无法全屏_android_02

下面我们来重写VideoView的onMeasure方法后

class MyVideoView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : VideoView(context, attrs, defStyleAttr) {

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        Log.d("MyVideoView", "MyVideoView1 size = $measuredWidth:$measuredHeight")
        val measuredWidth = getDefaultSize(0,widthMeasureSpec)
        val measuredHeight = getDefaultSize(0,heightMeasureSpec)
        setMeasuredDimension(measuredWidth,measuredHeight)
        Log.d("MyVideoView", "MyVideoView size = $measuredWidth:$measuredHeight")
    }
}

xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".TextureViewMainActivity"
    android:id="@+id/frameLayout">
    <com.jp.mediaplayer.video.MyVideoView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/videoView"
        android:background="@color/white"
        />

    <Button
        android:id="@+id/btn_play"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|center_horizontal"
        android:padding="8dp"
        android:text="开始" />
</FrameLayout>

VideoViewActivity.class

class VideoViewActivity : AppCompatActivity() {
    lateinit var videoView: MyVideoView
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_video_view)
        /*
         //
         val controller = ViewCompat.getWindowInsetsController(videoView)
         controller?.hide(WindowInsetsCompat.Type.systemBars())
         controller?.hide(WindowInsetsCompat.Type.navigationBars())*/
        videoView = MyVideoView(this)
        frameLayout.addView(videoView)
        btn_play.setOnClickListener {
            if (btn_play.text == "开始") {
                btn_play.text = "停止"
                videoView.setVideoURI(Uri.parse("android.resource://" + packageName + "/" + R.raw.test))
                videoView.setOnCompletionListener {
                    btn_play.text = "开始"
                }
                videoView.setOnPreparedListener {
                    videoView.start()
                }
            } else {
                btn_play.text = "开始"
                videoView.stopPlayback()
            }
        }
    }

//
    /*override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
        if (hasFocus && Build.VERSION.SDK_INT >= 19) {
            val decorView = window.decorView
            //真正的全屏模式
            decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                    or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                    or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                    or View.SYSTEM_UI_FLAG_FULLSCREEN
                    or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
        }
    }*/
    override fun onDestroy() {
        super.onDestroy()
        videoView?.stopPlayback()
    }
}

利用重写了onMeasure的VideoView后,实现了存在状态栏、导航栏也能宽高都能铺满的情况:

html5中video标签如何设置全屏 video标签无法全屏_ide_03

2、在对比原生VideoView的onMeasure方法之前,先了解一些事情。

1、这里涉及到MeasureSpec类,这个类代码不多,但很精妙。我也有很多地方没弄懂。不过在这里,只需了解它的三种mode就可以了。

/**
     * 1、UNSPECIFIED
     * 根据源码的注释,其大概意思是parent不对child做出限制.它想要什么size就给什么size.看了一些教程,都说用得很少,或者是系统内部才用得上.所以先不管了
     * 2、EXACTLY
     * 对应于match_parent和给出具体的数值
     * 3、AT_MOST
     * 对应于wrap_content
     */
public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    public static final int EXACTLY     = 1 << MODE_SHIFT;

    public static final int AT_MOST     = 2 << MODE_SHIFT;     ......
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }     ......
}

而在xml中,所有控件的width和height都是mach_parent,所以以下分析都是基于MeasureSpec.EXACTLY这个mode。

2、getDefaultSize方法

public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

由于都是MeasureSpec.EXACTLY,所以最终返回的结果是MeasureSpec.getSize(measureSpec),与size,也就是第一个参数无关。

3、setMeasuredDimension

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

中间的判断语句,涉及到ViewGroup的LayoutMode,它有两个值,一个是默认值clipBounds,效果就是保留子view之间的空白,因为有些控件看上去要比实际的小,但它仍然是占了给定的大小,只是系统让它的一部分边缘变成留白,这样的话,不至于子view真的是连接在一起;另一个是opticalBounds,它就是用来消除clipBounds的效果。不过,这种情况只在Holo Theme下才能有效。可以参考官方文档https://developer.android.com/about/versions/android-4.3.html#UI。但现在应该是用AppCompat Theme的比较多,至于怎么在AppCompat Theme下也实现这种效果,还有待解决。
一般情况下,都不会进入判断语句块里。
而这里要关注的其实是最后一句代码,setMeasuredDimensionRaw。

4、setMeasuredDimensionRaw

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

这个方法就是将最终的测量结果赋值给对应的view的全局变量,意味着measure部分结束。

3、对比原生VideoView的onMeasure方法

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//        Log.i("@@@@", "onMeasure(" + MeasureSpec.toString(widthMeasureSpec) + ", "
//                + MeasureSpec.toString(heightMeasureSpec) + ")");

        int width = getDefaultSize(mVideoWidth, widthMeasureSpec);
        int height = getDefaultSize(mVideoHeight, heightMeasureSpec);     if (mVideoWidth > 0 && mVideoHeight > 0) {

            int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
            int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
            int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);       if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) {
                // the size is fixed
                width = widthSpecSize;
                height = heightSpecSize;

                // for compatibility, we adjust size based on aspect ratio
                if ( mVideoWidth * height  < width * mVideoHeight ) {
                    //Log.i("@@@", "image too wide, correcting");
                    width = height * mVideoWidth / mVideoHeight;
                } else if ( mVideoWidth * height  > width * mVideoHeight ) {
                    //Log.i("@@@", "image too tall, correcting");
                    height = width * mVideoHeight / mVideoWidth;
                }
            } else if (widthSpecMode == MeasureSpec.EXACTLY) {
                  ......
            } else if (heightSpecMode == MeasureSpec.EXACTLY) {
                         ......
            } else {
         ......
            }
        } else {
            // no size yet, just adopt the given spec sizes
        }
        setMeasuredDimension(width, height);
    }

为了方便对比,再贴出onMeasure方法。我在这个方法中,打印过width和height的值,它们的值就是屏幕显示部分的分辨率。意思是说,按这里的情况来讲,当状态栏和底部的导航栏都是隐藏,也就是全屏时,width是1080,height是1920,正好是Pixel 2 API 28的分辨率。

当底部的导航栏和状态栏显示时,height就是1731。

@Override
   override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        Log.d("MyVideoView", "MyVideoView1 size = $measuredWidth:$measuredHeight")
        val measuredWidth = getDefaultSize(0,widthMeasureSpec)
        val measuredHeight = getDefaultSize(0,heightMeasureSpec)
        setMeasuredDimension(measuredWidth,measuredHeight)
        Log.d("MyVideoView", "MyVideoView size = $measuredWidth:$measuredHeight")
    }

现在对比原生的onMeasure方法来分析。

首先是通过getDefaultSize来得到width和height。上面说过,在我这个例子中,getDefaultSize的返回值只与第二个参数有关,即widthMeasureSpec和heightMeasureSpec,而这两个参数都是从相同的ViewGroup传进来的,所以无论是原生还是重写,其从getDefaultSize中得到的值都是一样的。然后进入第一层判断语句块,在这里通过MeasureSpec.getMode()和getSize(),再次取得控件的mode和size。其实这在getDefaultSize里也有实现,所以外层的width和widthSpecSize的值是相同的,height也是这种情况。

根据之前的说明,可以知道进入的是第一个判断语句块,而其它情况也被我省略了。

再到下面的判断语句,比较乘积之后,就修改width或height,对比重写的方法可以判断,导致效果不同的地方就是这里。代码的逻辑很清晰简单。这里直接取具体值来分析。这里的视频资源的帧宽度是1080,帧高度是1920。用来测试的Pixel 2是1080×1920。不隐藏状态栏和导航栏,实际的显示区域是1080*1731

mVideoWidth * height = 1080 × 1731 = 1,869,480,mVideoHeight * width= 1920 × 1080 = 2,073,600,所以修改的是width,等于973.69,

所以如上面的截图,差别就比较明显了。

html5中video标签如何设置全屏 video标签无法全屏_UI_04


这么看来,这部分代码就是把VideoView的宽或高给修改了,因为我是指定match_parent的,也就应该是屏幕显示部分的大小。而重写的方法就是跳过了这部分,让VideoView的宽高仍然是match_parent。