Android VideoView方向随着缓冲video而改变

我试图从本质上复制Android市场上最新的YouTube应用程序的function。 观看video时,有两种独立的布局,一种是纵向显示附加信息,另一种是横向显示video。

YouTube应用程序肖像布局
YouTupe应用程序在肖像模式

YouTube应用横向布局
横向模式下的YouTube应用

(抱歉的照片的随机性,但他们是我能find的实际布局的第一个图片)

这很容易做到 – 只需在layout-land中指定一个可选的布局,一切都会很好。 YouTube应用程序确实做得很好(以及我想要复制的内容)是在方向更改上,video继续播放,不必从头开始重新缓冲。

我已经知道覆盖onConfigurationChange()并设置新的LayoutParameters将允许我调整video的大小而不强制重新制止 – 但是当多次旋转屏幕时video会随机缩放到不同的宽度/高度。 我试过在VideoView上做各种各样的invalidate()调用,试着在父亲的RelativeLayout容器上调用RequestLayout(),尽可能地尝试尽可能多的东西,但是我似乎无法使它正常工作。 任何build议将不胜感激!

这是我的代码:

@Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { questionText.setVisibility(View.GONE); respond.setVisibility(View.GONE); questionVideo.setLayoutParams(new RelativeLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); } else { questionText.setVisibility(View.VISIBLE); respond.setVisibility(View.VISIBLE); Resources r = getResources(); int height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 150.0f, r.getDisplayMetrics()); questionVideo.setLayoutParams(new RelativeLayout.LayoutParams(LayoutParams.FILL_PARENT, height)); } } 

编辑:我已经发现了logcat一些有趣的输出,当我的video旋转似乎是罪魁祸首 – 虽然我不知道如何解决它:

正确resize时的Logcat输出(占用整个窗口)

注意h = 726

 12-13 15:37:35.468 1262 1270 I ActivityManager: Config changed: { scale=1.0 imsi=310/4 loc=en_US touch=3 keys=1/1/2 nav=1/1 orien=2 layout=34 uiMode=17 seq=210} 12-13 15:37:35.561 1262 1268 I TIOverlay: Position/X0/Y76/W480/H225 12-13 15:37:35.561 1262 1268 I TIOverlay: Adjusted Position/X1/Y0/W403/H225 12-13 15:37:35.561 1262 1268 I TIOverlay: Rotation/90 12-13 15:37:35.561 1262 1268 I Overlay : v4l2_overlay_set_position:: w=480 h=224 12-13 15:37:35.561 1262 1268 I Overlay : v4l2_overlay_set_position:: w=402 h=726 12-13 15:37:35.561 1262 1268 I Overlay : dumping driver state: 12-13 15:37:35.561 1262 1268 I Overlay : output pixfmt: 12-13 15:37:35.561 1262 1268 I Overlay : w: 432 12-13 15:37:35.561 1262 1268 I Overlay : h: 240 12-13 15:37:35.561 1262 1268 I Overlay : color: 7 12-13 15:37:35.561 1262 1268 I Overlay : UYVY 12-13 15:37:35.561 1262 1268 I Overlay : v4l2_overlay window: 12-13 15:37:35.561 1262 1268 I Overlay : window l: 1 12-13 15:37:35.561 1262 1268 I Overlay : window t: 0 12-13 15:37:35.561 1262 1268 I Overlay : window w: 402 12-13 15:37:35.561 1262 1268 I Overlay : window h: 726 

Logcat输出时,resize不正确(占用全屏幕的一小部分)

注意h = 480

 12-13 15:43:00.085 1262 1270 I ActivityManager: Config changed: { scale=1.0 imsi=310/4 loc=en_US touch=3 keys=1/1/2 nav=1/1 orien=2 layout=34 uiMode=17 seq=216} 12-13 15:43:00.171 1262 1268 I TIOverlay: Position/X0/Y76/W480/H225 12-13 15:43:00.171 1262 1268 I TIOverlay: Adjusted Position/X138/Y0/W266/H225 12-13 15:43:00.171 1262 1268 I TIOverlay: Rotation/90 12-13 15:43:00.179 1262 1268 I Overlay : v4l2_overlay_set_position:: w=480 h=224 12-13 15:43:00.179 1262 1268 I Overlay : v4l2_overlay_set_position:: w=266 h=480 12-13 15:43:00.179 1262 1268 I Overlay : dumping driver state: 12-13 15:43:00.179 1262 1268 I Overlay : output pixfmt: 12-13 15:43:00.179 1262 1268 I Overlay : w: 432 12-13 15:43:00.179 1262 1268 I Overlay : h: 240 12-13 15:43:00.179 1262 1268 I Overlay : color: 7 12-13 15:43:00.179 1262 1268 I Overlay : UYVY 12-13 15:43:00.179 1262 1268 I Overlay : v4l2_overlay window: 12-13 15:43:00.179 1262 1268 I Overlay : window l: 138 12-13 15:43:00.179 1262 1268 I Overlay : window t: 0 12-13 15:43:00.179 1262 1268 I Overlay : window w: 266 12-13 15:43:00.179 1262 1268 I Overlay : window h: 480 

也许有人知道“覆盖”是什么,为什么它没有得到正确的身高值?

我能够将问题缩小到VideoView类中的onMeasure函数。 通过创build一个子类并重写onMeasure函数,我能够获得所需的function。

 public class VideoViewCustom extends VideoView { private int mForceHeight = 0; private int mForceWidth = 0; public VideoViewCustom(Context context) { super(context); } public VideoViewCustom(Context context, AttributeSet attrs) { this(context, attrs, 0); } public VideoViewCustom(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public void setDimensions(int w, int h) { this.mForceHeight = h; this.mForceWidth = w; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { Log.i("@@@@", "onMeasure"); setMeasuredDimension(mForceWidth, mForceHeight); } } 

然后在我的活动中,我只是做了以下几点:

 @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); questionVideo.setDimensions(displayHeight, displayWidth); questionVideo.getHolder().setFixedSize(displayHeight, displayWidth); } else { getWindow().setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); questionVideo.setDimensions(displayWidth, smallHeight); questionVideo.getHolder().setFixedSize(displayWidth, smallHeight); } } 

该行:

 questionVideo.getHolder().setFixedSize(displayWidth, smallHeight); 

是做这项工作的关键。 如果你没有这个家伙的setDimensions调用,video仍然不会resize。

您唯一需要做的其他事情是确保您在onCreate()方法内调用setDimensions(),否则video将不会开始缓冲,因为video不会被设置为在任何大小的表面上绘制。

 // onCreate() questionVideo.setDimensions(initialWidth, initialHeight); 

最后一个关键部分 – 如果您发现自己想知道为什么VideoView没有调整旋转大小,则需要确保resize的尺寸与可见区域完全相同或小于可见区域。 当我在屏幕上仍然有通知栏/标题栏,而且根本没有调整VideoView的大小时,我遇到了一个非常大的问题,即将VideoView的宽度/高度设置为整个显示大小。 简单地删除通知栏和标题栏解决了这个问题。

希望这有助于未来的人!

编辑:( 2016年6月)

这个答案是非常旧的(我认为Android 2.2 / 2.3),可能不如下面的其他答案相关! 先看看他们,除非你正在开发传统的安卓系统:)

首先,非常感谢您的广泛答复。

我有同样的问题,video会在大多数情况下更小,或更大,或在video内旋转后扭曲。

我尝试了你的解决scheme,但我也试过随机的东西,偶然我注意到,如果我的VideoView是在其父母中心,神奇地自行工作(没有自定义的VideoView需要或任何东西)。

更具体地说,在这个布局中,我大多数时候都会重现这个问题:

 <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <VideoView android:id="@+id/videoView" android:layout_width="match_parent" android:layout_height="match_parent" /> </RelativeLayout> 

有了这一个布局,我从来没有这个问题(加上,video是居中,这是怎么回事):

 <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <VideoView android:id="@+id/videoView" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_centerInParent="true" /> </RelativeLayout> 

它也适用于wrap_content而不是match_parent (video仍占用所有的空间),这对我来说没有多大意义。

无论如何,我没有任何解释 – 这看起来像VideoView的bug。

这是一个非常简单的方法来完成你想要的最小代码:

AndroidManifest.xml中:

 android:configChanges="orientation|keyboard|keyboardHidden|screenSize|screenLayout|uiMode" 

注意:根据需要编辑您的API,这涵盖了10+,但较低的API需要删除此行的“screenSize | screenLayout | uiMode”部分

在“OnCreate”方法中,通常在“super.onCreate”下面添加:

 getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); 

然后在某个地方,通常在底部,添加:

 @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); } 

这会将video调整为全屏,只要方向改变而不中断播放,只需要重写configuration方法。

VideoView使用所谓的叠加层(overlay),即video呈现的区域。 该叠加层位于持有VideoView的窗口后面。 VideoView在其窗口中打孔,以便覆盖层可见。 然后,它保持与布局同步(例如,如果您移动或调整VideoView的大小,覆盖层也必须移动和resize)。

在布局阶段有一个错误,使覆盖使用VideoView设置的以前的大小。

为了修复它,子类VideoView并重载onLayout:

 @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); getHolder().setSizeFromLayout(); } 

并且叠加层将从VideoView的布局尺寸中获得正确的大小。

复制YouTube应用

我设法构build了一个不需要 android:configChanges="orientation"或者自定义VideoView的示例项目。 由此产生的体验与YouTube应用在video播放过程中处理旋转的方式相同。 换句话说,video不需要暂停,重新缓冲或重新加载,并且在设备方向改变时不跳过或丢弃任何audio帧。

最佳方法

此方法使用一个TextureView及其伴随的SurfaceTexture作为MediaPlayer当前video帧的接收器。 由于SurfaceTexture使用GL纹理对象(只需从GL上下文中引用整数),相信通过configuration更改来保留对SurfaceTexture的引用也是可以的。 TextureView本身在configuration更改(以及后备Activity)期间被销毁并重新创build,新创build的TextureView在附加之前仅使用SurfaceTexture引用进行更新。

我已经创build了一个完整的工作示例,展示如何以及何时初始化MediaPlayer和一个可能的MediaController,并且我将突出显示与以下问题相关的有趣部分:

 public class VideoFragment { TextureView mDisplay; SurfaceTexture mTexture; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View rootView = inflater.inflate(R.layout.fragment_main, container, false); mDisplay = (TextureView) rootView.findViewById(R.id.texture_view); if (mTexture != null) { mDisplay.setSurfaceTexture(mTexture); } mDisplay.setSurfaceTextureListener(mTextureListener); return rootView; } TextureView.SurfaceTextureListener mTextureListener = new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { mTexture = surface; // Initialize your media now or flag that the SurfaceTexture is available.. } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { mTexture = surface; } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { mTexture = surface; return false; // this says you are still using the SurfaceTexture.. } @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { mTexture = surface; } }; @Override public void onDestroyView() { mDisplay = null; super.onDestroyView(); } // ... } 

由于解决scheme使用保留的Fragment而不是手动处理configuration更改的Activity,因此您可以充分利用configuration特定的资源膨胀系统。 不幸的是,如果你的最小sdk低于API 16,你将基本上需要backport TextureView (我还没有完成)。

最后,如果你有兴趣,请检查我最初的问题,详细说明:我的原始方法,Android媒体堆栈,为什么不起作用,以及替代解决方法。

用这个 :

 @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { getActivity().getWindow().addFlags( WindowManager.LayoutParams.FLAG_FULLSCREEN); getActivity().getWindow().clearFlags( WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { getActivity().getWindow().addFlags( WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); getActivity().getWindow().clearFlags( WindowManager.LayoutParams.FLAG_FULLSCREEN); } } 

也不要忘记添加下面的行到您的清单中的活动:

 android:configChanges="orientation|keyboard|keyboardHidden|screenSize" 

我从这里的一些答案中拿出了一些例子,并试图按照自己的方式去做。 看起来像一个简单的解决scheme。

 videoLayout = (RelativeLayout) videoView.findViewById(R.id.videoFrame); 

onConfigurationChange内一个片段。其中video以全屏模式显示在横向模式。注意我隐藏了操作栏。

 @Override public void onConfigurationChanged(Configuration newConfig) { int height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200.0f,getResources().getDisplayMetrics()); ActionBar actionBar = weatherActivity.getSupportActionBar(); LayoutParams params = videoLayout.getLayoutParams(); if(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { if(actionBar.isShowing()) actionBar.hide(); params.width = ViewGroup.LayoutParams.MATCH_PARENT; params.height = ViewGroup.LayoutParams.MATCH_PARENT; videoLayout.requestLayout(); } else if(newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { if(!actionBar.isShowing()) actionBar.show(); params.width = ViewGroup.LayoutParams.MATCH_PARENT; params.height = height; videoLayout.requestLayout(); } super.onConfigurationChanged(newConfig); } 

这里是我的布局文件

 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <RelativeLayout android:id="@+id/videoFrame" android:layout_height="200dp" android:layout_width="match_parent"> <VideoView android:id="@+id/video" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_centerHorizontal="true"/> </RelativeLayout> 

我有另一个相关的布局,这不是在这个布局。 但是这不会有什么区别。

看到我的示例代码,它为我工作

 public class CustomVideoView extends android.widget.VideoView { private int width; private int height; private Context context; private VideoSizeChangeListener listener; private boolean isFullscreen; public CustomVideoView(Context context) { super(context); init(context); } public CustomVideoView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } /** * get video screen width and height for calculate size * * @param context Context */ private void init(Context context) { this.context = context; setScreenSize(); } /** * calculate real screen size */ private void setScreenSize() { Display display = ((Activity) context).getWindowManager().getDefaultDisplay(); if (Build.VERSION.SDK_INT >= 17) { //new pleasant way to get real metrics DisplayMetrics realMetrics = new DisplayMetrics(); display.getRealMetrics(realMetrics); width = realMetrics.widthPixels; height = realMetrics.heightPixels; } else if (Build.VERSION.SDK_INT >= 14) { //reflection for this weird in-between time try { Method mGetRawH = Display.class.getMethod("getRawHeight"); Method mGetRawW = Display.class.getMethod("getRawWidth"); width = (Integer) mGetRawW.invoke(display); height = (Integer) mGetRawH.invoke(display); } catch (Exception e) { //this may not be 100% accurate, but it's all we've got width = display.getWidth(); height = display.getHeight(); } } else { //This should be close, as lower API devices should not have window navigation bars width = display.getWidth(); height = display.getHeight(); } // when landscape w > h, swap it if (width > height) { int temp = width; width = height; height = temp; } } /** * set video size change listener * */ public void setVideoSizeChangeListener(VideoSizeChangeListener listener) { this.listener = listener; } public interface VideoSizeChangeListener { /** * when landscape */ void onFullScreen(); /** * when portrait */ void onNormalSize(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { // full screen when landscape setSize(height, width); if (listener != null) listener.onFullScreen(); isFullscreen = true; } else { // height = width * 9/16 setSize(width, width * 9 / 16); if (listener != null) listener.onNormalSize(); isFullscreen = false; } } /** * @return true: fullscreen */ public boolean isFullscreen() { return isFullscreen; } /** * set video sie * * @param w Width * @param h Height */ private void setSize(int w, int h) { setMeasuredDimension(w, h); getHolder().setFixedSize(w, h); } } 

改变方向时不要重新创build视图

 // AndroidManifest.xml android:configChanges="screenSize|orientation|keyboardHidden" 

portrain

景观 景观

我的源代码在这里

虽然Mark37的(非常有用)的答案确实工作,它需要手动设置尺寸(使用setDimensions)。 在预先知道所需尺寸的应用程序中,这可能是正确的,但如果您希望视图根据video的参数自动确定尺寸(例如,以确保保持原始纵横比),则需要采用不同的方法。

幸运的是,setDimensions部分实际上并不是必须的。 VideoView的onMeasure已经包含了所有必要的逻辑,所以不用依赖别人来调用setDimensions,而是调用super.onMeasure,然后使用视图的getMeasuredWidth / getMeasuredHeight来获得固定的大小。

所以,VideoViewCustom类变得简单:

 public class VideoViewCustom extends VideoView { public VideoViewCustom(Context context) { super(context); } public VideoViewCustom(Context context, AttributeSet attrs) { this(context, attrs, 0); } public VideoViewCustom(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); getHolder().setFixedSize(getMeasuredWidth(), getMeasuredHeight()); } } 

这个版本不需要调用者提供额外的代码,并充分利用了VideoView现有的onMeasure实现。

这实际上与Zapek提出的方法非常相似,Zapek的方法基本相同,只能用onLayout而不用onMeasure。

我试图使用其他答案的代码,但它并没有解决我的问题,因为如果我快速旋转屏幕,我的video大小被碰撞。 所以我使用了部分代码,我把它放在一个线程中,这个线程可以很快的更新窗口。 所以,这是我的代码:

  new Thread(new Runnable() { @Override public void run() { while(true) { try { Thread.sleep(1); } catch(InterruptedException e){} handler.post(new Runnable() { @Override public void run() { getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); } }); } } }).start(); 

这样,video大小将永远是正确的(在我的情况)。