Android VideoView orientation change with buffered

2019-01-01 03:25发布

I'm trying to replicate the functionality of the latest YouTube app in the Android marketplace. When watching a video there's two separate layouts, one in portrait which provides additional info, and one in landscape which provides a full screen view of the video.

YouTube app portrait layout
YouTupe app in portrait mode

YouTube app landscape layout
YouTube app in landscape mode

(Sorry for the randomness of the photos, but they were the first pics I could find of the actual layout)

This is pretty easy to do normally - just specify an alternate layout in layout-land and all will be good. The thing that the YouTube app does really well (and what I'm trying to replicate) is that on orientation change, the video continues playing and doesn't have to re-buffer from the beginning.

I've figured out that overriding onConfigurationChange() and setting new LayoutParameters will allow me to resize the video without forcing a rebuffer - however the video will randomly scale to different widths/heights when rotating the screen multiple times. I've tried doing all sorts of invalidate() calls on the VideoView, tried calling RequestLayout() on the parent RelativeLayout container and just trying as many different things as I can, but I can't seem to get it to work properly. Any advice would be greatly appreciated!

Here's my code:

@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));
    }
}

EDIT: I've discovered in logcat some interesting output that comes up when my video is rotated which seems to be the culprit - although I have no idea how to fix it:

Logcat output when resizing properly (takes up entire window)

notice the 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 output when resizing incorrectly (takes up tiny portion of full screen)

notice the 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

Maybe someone knows what 'Overlay' is and why it's not getting the correct height value?

11条回答
浅入江南
2楼-- · 2019-01-01 03:39

While Mark37's (very useful) answer does work, it requires setting the dimensions manually (using setDimensions). This may be fine in an app where the desired dimensions are known in advance, but if you want the views to determine the size automatically based on the video's parameters (e.g. to make sure the original aspect ratio is kept), a different approach is needed.

Fortunately, it turns out the setDimensions part isn't actually necessary. VideoView's onMeasure already includes all the necessary logic, so instead of relying on someone to call setDimensions, it's possible to just call super.onMeasure, and then use the view's getMeasuredWidth/getMeasuredHeight to get the fixed size.

So, the VideoViewCustom class becomes simply:

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());
    }
}

This version doesn't require any additional code in the caller, and takes full advantage of VideoView's existing onMeasure implementation.

This is in fact rather similar to the approach suggested by Zapek, which does basically the same thing, only with onLayout instead of onMeasure.

查看更多
孤独总比滥情好
3楼-- · 2019-01-01 03:41

EDIT: (June 2016)

This answer is very old (I think android 2.2/2.3) and probably is not as relevant as the other answers below! Look to them first unless you're dev-ing on legacy Android :)


I was able to narrow down the problem to the onMeasure function in the VideoView class. By creating a child class and overriding the onMeasure function, I was able to get the desired functionality.

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);
    }
}

Then inside my Activity I just did the following:

@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);

    }
}

The line:

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

is key in order to make this work. If you do the setDimensions call without this guy, the video still will not resize.

The only other thing you need to do is ensure that you call setDimensions() inside the onCreate() method as well or your video will not start buffering as the video won't be set to draw on a surface of any size.

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

One last key part - if you ever find yourself wondering why the VideoView isn't resizing on rotation, you need to ensure the dimensions you're resizing to are either exactly equal to the visible area or less than it. I had a really big problem where I was setting the VideoView's width/height to the entire display size when I still had the notification bar/title bar on the screen and it was not resizing the VideoView at all. Simply removing the notification bar and title bar fixed the problem.

Hopefully this helps someone in the future!

查看更多
倾城一夜雪
4楼-- · 2019-01-01 03:41

Here is a really easy way to accomplish what you want with minimal code:

AndroidManifest.xml:

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

Note: Edit as needed for your API, this covers 10+, but lower APIs require removing the "screenSize|screenLayout|uiMode" portion of this line

Inside the "OnCreate" method, usually under "super.onCreate", add:

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

And then somewhere, usually at the bottom, add:

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

This will resize the video to fullscreen whenever the orientation has changed without interrupting playback and only requires overriding the configuration method.

查看更多
人气声优
5楼-- · 2019-01-01 03:41

I tried to use the code of other answers, but it didn't solve my problem, because if I rotated the screen quickly, my video size was bumped. So I use in part their code, which I put in a thread which updates the window very quickly. So, here is my code:

  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();

In this way, the video size will be always right (in my case).

查看更多
路过你的时光
6楼-- · 2019-01-01 03:42

use this :

@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);

        }
    }

Also dont forget to add the line below to your Activity in the Manifest:

android:configChanges="orientation|keyboard|keyboardHidden|screenSize"
查看更多
大哥的爱人
7楼-- · 2019-01-01 03:45

VideoView uses what's called an overlay which is an area where the video is rendered. That overlay is behind the window holding the VideoView. VideoView punches a hole in its window so that the overlay is visible. It then keeps it in sync with the layout (eg. if you move or resize VideoView, the overlay has to be moved and resized as well).

There's a bug somewhere during the layout phase which makes the overlay use the previous size set by VideoView.

To fix it, subclass VideoView and override onLayout:

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

and the overlay will have the correct size from VideoView's layout dimensions.

查看更多
登录 后发表回答