Android VideoView使用缓冲视频更改方向
我试图在Android marketplace中复制最新YouTube应用程序的功能。观看视频时,有两种不同的布局,一种在纵向中提供附加信息,另一种在横向中提供视频的全屏视图Android VideoView使用缓冲视频更改方向,android,android-orientation,android-videoview,Android,Android Orientation,Android Videoview,我试图在Android marketplace中复制最新YouTube应用程序的功能。观看视频时,有两种不同的布局,一种在纵向中提供附加信息,另一种在横向中提供视频的全屏视图 YouTupe应用程序处于纵向模式 处于横向模式的YouTube应用程序 (很抱歉照片的随机性,但它们是我能找到的第一张实际布局的照片) 这通常很容易做到——只需在布局区域中指定一个备用布局,一切都会很好。YouTube应用程序做得非常好的一件事(我正在尝试复制)是,在方向改变时,视频会继续播放,并且不必从一开始就重新
YouTupe应用程序处于纵向模式
处于横向模式的YouTube应用程序 (很抱歉照片的随机性,但它们是我能找到的第一张实际布局的照片) 这通常很容易做到——只需在布局区域中指定一个备用布局,一切都会很好。YouTube应用程序做得非常好的一件事(我正在尝试复制)是,在方向改变时,视频会继续播放,并且不必从一开始就重新缓冲 我发现重写onConfigurationChange()并设置新的LayoutParameters将允许我调整视频大小,而无需强制回绝-但是,在多次旋转屏幕时,视频将随机缩放到不同的宽度/高度。我尝试过在VideoView上执行各种invalidate()调用,尝试过在parent RelativeLayout容器上调用RequestLayout(),并尝试了尽可能多的不同操作,但我似乎无法让它正常工作。任何建议都将不胜感激 这是我的密码:
@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));
}
}
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();
编辑:我在logcat中发现了一些有趣的输出,当我的视频旋转时会出现,这似乎是罪魁祸首-尽管我不知道如何修复它:
正确调整大小时的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输出(占用整个屏幕的一小部分)
注意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
也许有人知道什么是“叠加”以及为什么它没有得到正确的高度值?编辑:(2016年6月)
这个答案非常古老(我认为android 2.2/2.3),可能没有下面的其他答案那么重要!首先关注它们,除非您正在开发遗留Android:)
我能够将问题缩小到VideoView类中的onMeasure函数。通过创建一个子类并重写onMeasure函数,我能够获得所需的功能
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,视频仍然不会调整大小
您需要做的另一件事是确保在onCreate()方法中也调用setDimensions(),否则视频不会开始缓冲,因为视频不会被设置为在任何大小的曲面上绘制
// onCreate()
questionVideo.setDimensions(initialWidth, initialHeight);
最后一个键部分-如果您发现自己想知道为什么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>
使用这种布局,我从来没有遇到过问题(另外,视频是居中的,这就是它应该是什么样子;):
它还可以处理wrap_content
而不是match_parent
(视频仍然占用所有空间),这对我来说没有多大意义
无论如何,我对此没有任何解释-这在我看来像是一个VideoView bug。这里有一个非常简单的方法,可以用最少的代码完成您想要的内容: AndroidManifest.xml:
android:configChanges="orientation|keyboard|keyboardHidden|screenSize|screenLayout|uiMode"
注意:根据您的API需要进行编辑,这包括10+,但较低的API需要删除此行的“screenSize | screenLayout | uiMode”部分
在“OnCreate”方法中,通常在“super.OnCreate”下添加:
然后在某个地方,通常在底部,添加:
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
这将在方向改变而不中断播放时将视频调整为全屏,并且只需要覆盖配置方法。使用以下方法:
@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"
复制YouTube应用程序
我成功构建了一个示例项目,该项目不需要android:configChanges=“orientation”或自定义的视频视图。由此产生的体验与YouTube应用程序在视频播放过程中处理旋转的方式相同。换句话说,视频不需要暂停、重新缓冲或重新加载,并且在设备方向改变时不跳过或丢弃任何音频帧
优化方法
此方法使用TextureView及其附带的SurfaceTexture
作为MediaPlayer
当前视频帧的接收器。由于SurfaceTexture使用GL纹理对象(仅由GL上下文中的整数引用),请相信通过配置更改保留对SurfaceTexture的引用是可以的。TextureView本身在配置更改期间(以及备份活动)被销毁和重新创建,新创建的TextureView在附加之前仅使用SurfaceTexture引用进行更新
android:configChanges="orientation|keyboard|keyboardHidden|screenSize"
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();
}
// ...
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
getHolder().setSizeFromLayout();
}
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());
}
}
videoLayout = (RelativeLayout) videoView.findViewById(R.id.videoFrame);
@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);
}
}
// AndroidManifest.xml
android:configChanges="screenSize|orientation|keyboardHidden"
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();
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
...
setPlayerViewLayoutParams();
...
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
setPlayerViewLayoutParamsForLandScape();
} else {
setPlayerViewLayoutParamsForPortrait();
}
}
private void setPlayerViewLayoutParamsForLandScape() {
ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams(
ConstraintLayout.LayoutParams.MATCH_PARENT, ConstraintLayout.LayoutParams.MATCH_PARENT);
mPlayerView.setLayoutParams(lp);
}
private void setPlayerViewLayoutParamsForPortrait() {
Display display = myContext.getWindowManager().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
int width = size.x;
Double doubleHeight = width / 1.5;
Integer height = doubleHeight.intValue();
ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams(
ConstraintLayout.LayoutParams.MATCH_PARENT, height);
mPlayerView.setLayoutParams(lp);
}
private void setPlayerViewLayoutParams() {
if (myContext.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
setPlayerViewLayoutParamsForLandScape();
} else {
setPlayerViewLayoutParamsForPortrait();
}
}
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorBlack"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.0"/>
</android.support.constraint.ConstraintLayout>
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.image_button_full_screen:
if (myContext.getResources().getConfiguration().orientation
== Configuration.ORIENTATION_LANDSCAPE) {
myContext.setRequestedOrientation(
ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
} else {
myContext.setRequestedOrientation(
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
}
break;
}
}
private void setOrientationListener() {
OrientationEventListener orientationEventListener = new OrientationEventListener(this) {
@Override
public void onOrientationChanged(int orientation) {
// 94 > 90 - 10 and 94 < 90 + 10 // 80 < orientation < 100. Rotating to the LEFT.
// 278 > 270 - 10 and 278 < 270 + 10 // 260 < orientation < 280. Rotating to the RIGHT.
int epsilon = 10;
int leftLandscape = 90;
int rightLandscape = 270;
if (epsilonCheck(orientation, leftLandscape, epsilon) ||
epsilonCheck(orientation, rightLandscape, epsilon)) {
if (android.provider.Settings.System.getInt(getContentResolver(),
Settings.System.ACCELEROMETER_ROTATION, 0) == 1) {
// Phone's auto rotation is ON
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
}
}
}
private boolean epsilonCheck(int a, int b, int epsilon) {
return a > b - epsilon && a < b + epsilon;
}
};
orientationEventListener.enable();
}