Подтвердить что ты не робот

Изменение ориентации Android VideoView с помощью буферизованного видео

Я пытаюсь существенно расширить функциональность последнего приложения YouTube на рынке Android. При просмотре видео есть два отдельных макета, один в портрете, который предоставляет дополнительную информацию, и один в ландшафте, который обеспечивает полный просмотр видео.

YouTube app portrait layout
Приложение YouTupe в портретном режиме

YouTube app landscape layout
Приложение YouTube в ландшафтном режиме

(Извините за случайность фотографий, но это были первые фотографии, которые я мог найти в фактическом макете)

Это довольно легко сделать нормально - просто укажите альтернативный макет в макете, и все будет хорошо. То, что приложение YouTube действительно хорошо (и то, что я пытаюсь реплицировать), заключается в том, что при изменении ориентации видео продолжает воспроизведение и не нужно повторно перезаписывать с самого начала.

Я понял, что переопределение onConfigurationChange() и установка новых LayoutParameters позволит мне изменять размер видео без принудительного восстановления, но при случайном повороте экрана несколько раз будет масштабироваться по разным ширинам/высоте. Я пробовал делать всевозможные вызовы invalidate() в VideoView, пытался вызвать RequestLayout() в родительском контейнере RelativeLayout и просто пытаться как можно больше разных вещей, но я не могу заставить его работать правильно. Любые советы будут очень признательны!

Здесь мой код:

@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: я обнаружил в 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

Может быть, кто-то знает, что такое "Overlay" и почему он не получает правильное значение высоты?

4b9b3361

Ответ 1

Мне удалось сузить проблему до функции onMeasure в классе VideoView. Создав дочерний класс и переопределив функцию 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 без этого парня, видео по-прежнему не будет изменяться.

Единственное, что вам нужно сделать, это убедиться, что вы вызываете setDimensions() внутри метода onCreate(), иначе ваше видео не начнет буферизацию, поскольку видео не будет установлено для рисования на поверхности любого размера.

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

Одна последняя клавиша - если вы когда-нибудь задаетесь вопросом, почему VideoView не изменяет размер при вращении, вам необходимо убедиться, что размеры, размер которых вы изменяете, либо точно равно видимой области или меньше. У меня была действительно большая проблема, когда я устанавливал ширину/высоту VideoView для всего размера экрана, когда у меня все еще была панель уведомлений/строка заголовка на экране, и она вообще не меняла размер VideoView. Проблема заключается в простое удаление панели уведомлений и заголовка.

Надеюсь, это поможет кому-то в будущем!

EDIT: (июнь 2016 года)

Этот ответ очень старый (я думаю, Android 2.2/2.3) и, вероятно, не так уместен, как другие ответы ниже! Посмотрите на них сначала, если вы не разрабатываете устаревший Android:)

Ответ 2

Прежде всего, большое спасибо за ваш собственный обширный ответ.

У меня была та же проблема, что видео в большинстве случаев будет меньше или больше или искажено внутри VideoView после поворота.

Я попробовал ваше решение, но я также пробовал случайные вещи, и случайно заметил, что если мой 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>

С этим одним макетом у меня никогда не было проблемы (плюс, видео сосредоточено, как и должно быть в любом случае;):

<?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 (видео все еще занимает все пространство), что не имеет для меня никакого смысла.

Во всяком случае, у меня нет никаких объяснений для этого - это похоже на ошибку VideoView для меня.

Ответ 3

Вот очень простой способ выполнить то, что вы хотите, с минимальным кодом:

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

Это приведет к изменению размера видео в полноэкранном режиме, когда ориентация изменится без прерывания воспроизведения и требует только переопределения метода настройки.

Ответ 4

VideoView использует то, что называется наложением, которое является областью, в которой отображается видео. Этот оверлей находится за окном, в котором находится VideoView. VideoView пробивает отверстие в своем окне, чтобы наложение было видимым. Затем он синхронизируется с макетом (например, если вы перемещаете или изменяете размер VideoView, наложение также нужно перемещать и изменять).

Там где-то на этапе компоновки появляется ошибка, из-за которой наложение использует предыдущий размер, заданный 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.

Ответ 5

Репликация приложения YouTube

Мне удалось создать образец проекта, который не требует android:configChanges="orientation" или пользовательский VideoView. Полученный опыт идентичен тому, как приложение YouTube обрабатывает вращение во время воспроизведения видео. Другими словами, видео не нужно приостанавливать, повторно буферизовать или перезагружать, а также не пропускать и не удалять любые звуковые кадры при изменении ориентации устройства.

Оптимальный метод

Этот метод использует TextureView и сопровождающий его SurfaceTexture как приемник для текущего видеокарта MediaPlayer. Поскольку SurfaceTexture использует объект текстуры GL (просто ссылается на целое число из контекста GL), верьте, что в порядке сохранения ссылки на SurfaceTexture через изменения конфигурации. Сам TextureView уничтожается и воссоздается во время изменения конфигурации (вместе с активностью резервного копирования), а вновь созданный TextureView просто обновляется с помощью ссылки SurfaceTexture до его прикрепления.

Я создал полный рабочий пример показывающий, как и когда инициализировать ваш 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();
    }

    // ...

}

Поскольку в решении используется сохраненный фрагмент, а не активность, которая вручную обрабатывает изменения конфигурации, вы можете полностью использовать систему инфляции ресурсов, зависящую от конфигурации, как вы, естественно. К сожалению, если ваш минимальный уровень sdk ниже API 16, вам по существу понадобится backport TextureView (которого я еще не сделал).

Наконец, если вам интересно, проверить мой первоначальный вопрос, детализируя: мой оригинальный подход, стек Android для Android, почему он не работает, и альтернативный обходной путь.

Ответ 6

используйте это:

@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"

Ответ 7

Я привел примеры из некоторых ответов здесь и попытался сделать это по-своему. Похоже на простое решение.

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

onConfigurationChange внутри фрагмента. Где видео отображается в полноэкранном режиме в ландшафтном режиме. Обратите внимание, что я скрываю панель действий.

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

У меня есть другое relativelayout ниже этого, которого нет в этом макете. Но это не имеет никакого значения.

Ответ 8

см. мой пример кода, он работает для меня

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 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"

portrain

пейзаж landscape

мой исходный код здесь

Ответ 9

В то время как Mark37 (очень полезно) answer работает, требуется установка размеров вручную (с помощью setDimensions). Это может быть хорошо в приложении, где требуемые размеры известны заранее, но если вы хотите, чтобы представления определяли размер автоматически на основе параметров видео (например, чтобы убедиться, что исходное соотношение сторон сохранено), необходим другой подход.

К счастью, оказывается, что часть 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());
    }
}

Эта версия не требует дополнительного кода в вызывающем абоненте и в полной мере использует существующую реализацию onMeasure в VideoView.

На самом деле это похоже на подход предложенный Zapek, который делает в основном то же самое, только с onLayout, а не onMeasure.

Ответ 10

Я попытался использовать код других ответов, но это не решило мою проблему, потому что, если бы я быстро повернул экран, мой размер видео был набит. Поэтому я частично использую их код, который я вставляю в поток, который очень быстро обновляет окно. Итак, вот мой код:

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

Таким образом, размер видео всегда будет правильным (в моем случае).