VideoView将匹配父级高度并保持纵横比

38

我有一个VideoView,设置如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 
  android:id="@+id/player" 
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">

    <VideoView
        android:id="@+id/video"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_centerInParent="true"
        android:layout_centerVertical="true" />

    <ProgressBar
        android:id="@+id/loader"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true" />

</RelativeLayout>

然而,VideoView会自适应父容器的宽度,并根据载入视频的纵横比例设置高度。
我希望做的恰好相反,也就是让VideoView与父容器的高度相同,同时保持纵横比例不变,因此视频将被剪裁在两侧。

我尝试将VideoView拉伸以填充父容器,但这样会破坏纵横比例。

另外一件事是,我要像这样将MediaController添加到VideoView中:

MediaController controllers = new MediaController(this) {
    @Override
    public void hide() {
        if (state != State.Hidden) {
            this.show();
        }
        else {
            super.hide();
        }
    }
};
controllers.setAnchorView(videoView);
videoView.setMediaController(controllers);

videoView.setOnPreparedListener(new OnPreparedListener() {
    @Override
    public void onPrepared(MediaPlayer mediaPlayer) {
        controllers.show();
    }
});

这个方法很好用,控制器总是保持开启状态,但在计算视频位置时没有考虑到控制器的高度(因为它垂直居中)。

那么我的两个问题是:

  1. 如何使VideoView适应父容器的高度但保持纵横比?
  2. 如何让VideoView考虑其控制器的高度?

谢谢。


你是在说 WebView 还是 VideoView?你指的是中途更改吗? - George Baker
第一次一个问题回答了我的问题,而不是答案! 我的问题是在全屏模式下视频下方有一个空白区域。我将layout_height设置为match_parent。解决方法是将其设置为wrap_content并给父元素设置黑色背景。此外,还要使VideoView在其父元素中垂直居中。 - Abdelalim Hassouna
保持你的VideoView宽度和高度为match_parent,并且设置android:scaleType="fitXY",这样你就可以按照你的要求得到结果。 - Prince Dholakiya
13个回答

31

你应该继承内置的视频视图。

在显示视频视图之前调用setVideoSize,可以从视频缩略图中提取视频大小。

这样,当视频视图的onMeasure被调用时,mVideoWidthmVideoHeight都大于0。

如果你想要考虑控制器的高度,可以在onMeasure方法中自己完成。

希望会有所帮助。

public class MyVideoView extends VideoView {

        private int mVideoWidth;
        private int mVideoHeight;

        public MyVideoView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }

        public MyVideoView(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
        }

        public MyVideoView(Context context) {
            super(context);
        }

        public void setVideoSize(int width, int height) {
            mVideoWidth = width;
            mVideoHeight = height;
        }

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            // Log.i("@@@", "onMeasure");
            int width = getDefaultSize(mVideoWidth, widthMeasureSpec);
            int height = getDefaultSize(mVideoHeight, heightMeasureSpec);
            if (mVideoWidth > 0 && mVideoHeight > 0) {
                if (mVideoWidth * height > width * mVideoHeight) {
                    // Log.i("@@@", "image too tall, correcting");
                    height = width * mVideoHeight / mVideoWidth;
                } else if (mVideoWidth * height < width * mVideoHeight) {
                    // Log.i("@@@", "image too wide, correcting");
                    width = height * mVideoWidth / mVideoHeight;
                } else {
                    // Log.i("@@@", "aspect ratio is correct: " +
                    // width+"/"+height+"="+
                    // mVideoWidth+"/"+mVideoHeight);
                }
            }
            // Log.i("@@@", "setting size: " + width + 'x' + height);
            setMeasuredDimension(width, height);
        }
}

1
这确实有所帮助,但我还需要设置视图比屏幕更大,就像这里:https://dev59.com/jm865IYBdhLWcg3wcOLG - Kibi
1
@ax003d 我已经尝试了这个代码: videoView.setLayoutParams(params); videoView.setDimensions(params.width,params.width); videoView.getHolder().setFixedSize(params.width,params.height); 但仍然无法设置我的videoview高度。 - Erum
12
这是一个很好的回答,但我建议不要手动使用setVideoSize(),而是应该覆盖(load video)加载视频的方法: @Override public void setVideoURI(Uri uri) { MediaMetadataRetriever retriever = new MediaMetadataRetriever(); retriever.setDataSource(this.getContext(), uri); mVideoWidth = Integer.parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)); mVideoHeight = Integer.parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)); super.setVideoURI(uri); } - Arthez
运行得非常好。只有一个小提示:如果您正在使用 mediaPlayer 将视频托管到 videoView,则无法使用 setVideoUri。对于这种情况,setVideoSize 方法完全可以胜任。(顺便说一句,我正在使用 Xamarin(C#))。 - Csharpest
"setMeasuredDimension" 没有效果,它没有改变视频的大小,我不知道为什么。 - Mahmoud Mabrok

21

我通过布局解决了这个问题。当固定在角落时,它似乎工作得很好,但会导致视频倾斜。为了测试,我将相对布局的背景更改为#990000,以查看红色是否透过。

<?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"
    android:id="@+id/relative_parent"
    android:background="#000000">
    <VideoView
        android:layout_alignParentLeft="true"
        android:layout_alignParentRight="true"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:focusable="false"
        android:focusableInTouchMode="false"
        android:id="@+id/videoView" />
</RelativeLayout>

1
好的,它能够工作,但我不明白为什么它不能在其他布局下工作,比如帧布局和约束布局。如果您有使用其他布局的解决方案,请分享一下。 - Rucha Bhatt Joshi
1
这是因为 android:layout_alignParentLeft="true" 和android:layout_alignParentRight="true" 的作用,videoview将适应RelativeLayout的左右两侧。 - livemaker
简单的黑客技巧!谢谢。 - Noor Hossain

14

关于问题1,我惊讶地发现没有人提到MediaPlayer的缩放模式的可能使用。 https://developer.android.com/reference/android/media/MediaPlayer.html#setVideoScalingMode(int)

它有两种模式。它们都始终填充视图区域。要让它在保留纵横比的同时填满空间,从而裁剪长边,您需要切换到第二个模式,VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING。这解决了问题的一部分。另一个部分是改变VideoView的测量行为,就像其他答案所示。这是我做的方式,主要是因为懒惰而不熟悉其他人使用的元数据API,您可以使用此方法或其中一种方法来修复视图的大小。如果在mMediaPlayer存在之前调用此函数(因为它可能被多次调用),则全局catch确保安全,并且如果字段名称更改,则会回退到旧行为。

class FixedSizeVideoView : VideoView {
    constructor(ctx: Context) : super(ctx)
    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)

    // rather than shrink down to fit, stay at the size requested by layout params. Let the scaling mode
    // of the media player shine through. If the scaling mode on the media player is set to the one
    // with cropping, you can make a player similar to AVLayerVideoGravityResizeAspectFill on iOS
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        try {
            val mpField = VideoView::class.java.getDeclaredField("mMediaPlayer")
            mpField.isAccessible = true
            val mediaPlayer: MediaPlayer = mpField.get(this) as MediaPlayer

            val width = View.getDefaultSize(mediaPlayer.videoWidth, widthMeasureSpec)
            val height = View.getDefaultSize(mediaPlayer.videoHeight, heightMeasureSpec)
            setMeasuredDimension(width, height)
        }
        catch (ex: Exception) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        }
    }
}

因此,在布局中使用这个类,你只需要在任何可能的情况下更改媒体播放器的缩放模式即可,例如:

        video.setOnPreparedListener { mp: MediaPlayer ->
            mp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING)
            mp.isLooping = true
            mp.setScreenOnWhilePlaying(false)
        }
        video.start()

这不是安卓系统吧? - jobbert
17
Kotlin,亲爱的先生/女士 :) - androidguy
由于这个答案仍然相关,我们应该记住自 Android 9 开始的新 SDK 反射限制。你可以考虑这个问题:https://dev59.com/6Wgu5IYBdhLWcg3wpYYj。此外,VideoView.mMediaPlayer 字段在 Android 11 中仍然是“灰名单”。 - androidguy
如何在iOS上获得AVLayerVideoGravityResizeAspect效果 - Mahmoud Mabrok

8

快速高效的解决方法:

无需创建一个扩展自VideoView的自定义视图。只需设置足够大的值来android:layout_width属性。这将把视频视图的widthSpecMode设置为View.MeasureSpec.AT_MOST,然后VideoView的onMeasure()方法会自动调整其宽度以保持比例。

<VideoView
     android:id="@+id/video"
     android:layout_width="2000dp"
     android:layout_height="match_parent" />

3
我非常希望这个工作能够奏效,但它毫无作用。 - Martin Erlic

7
public class MyVideoView extends VideoView {
    private int mVideoWidth;
    private int mVideoHeight;

    public MyVideoView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyVideoView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public MyVideoView(Context context) {
        super(context);
    }


    @Override
    public void setVideoURI(Uri uri) {
        MediaMetadataRetriever retriever = new MediaMetadataRetriever();
        retriever.setDataSource(this.getContext(), uri);
        mVideoWidth = Integer.parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));
        mVideoHeight = Integer.parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT));
        super.setVideoURI(uri);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Log.i("@@@", "onMeasure");
        int width = getDefaultSize(mVideoWidth, widthMeasureSpec);
        int height = getDefaultSize(mVideoHeight, heightMeasureSpec);
        if (mVideoWidth > 0 && mVideoHeight > 0) {
            if (mVideoWidth * height > width * mVideoHeight) {
                // Log.i("@@@", "image too tall, correcting");
                height = width * mVideoHeight / mVideoWidth;
            } else if (mVideoWidth * height < width * mVideoHeight) {
                // Log.i("@@@", "image too wide, correcting");
                width = height * mVideoWidth / mVideoHeight;
            } else {
                // Log.i("@@@", "aspect ratio is correct: " +
                // width+"/"+height+"="+
                // mVideoWidth+"/"+mVideoHeight);
            }
        }
        // Log.i("@@@", "setting size: " + width + 'x' + height);
        setMeasuredDimension(width, height);
    }
}

5
使用ConstraintLayout,我们可以实现此目的,请参阅以下xml代码。
当layout_width和layout_height为0dp时,VideoView的大小和位置是根据其他约束动态计算的。 layout_constraintDimensionRatio属性指示应用程序计算VideoView的大小时,宽度与高度的比率应为3:4。该约束保持了视频的纵横比并防止视图在任何方向上过度拉伸(取决于设备如何旋转)。
根据需求更改layout_constraintDimensionRatio值,竖屏/横屏不同。
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <VideoView
        android:id="@+id/videoView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintDimensionRatio="3:4"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

这与问题所要求的相反。他希望它被裁剪。 - Carlos López Marí

2
这是一个回答问题的好方法,而不是提供答案!我的问题是在全屏模式下视频下方有一个白色空间。我将layout_height设置为match_parent。解决方法是将其设置为wrap_content并给父元素设置黑色背景。此外,要使VideoView在其父元素中垂直居中。

我最初将其写成评论,但后来想到可能会有人遇到与我相同的问题,因此也将其作为答案发布。


1

我尝试了很多解决方案,但我的视频始终是1000*1000格式,因此我为那些知道自己纵横比的人创建了一个简单的解决方案。首先,在RelativeLayout中创建一个VideoView,如下所示:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:id="@+id/video_holder"
      android:layout_width="wrap_content"
      android:layout_height="fill_parent"
      android:clipToPadding="false">

      <VideoView  
          android:id="@+id/videoView"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:layout_alignParentBottom="true"
          android:layout_alignParentEnd="true"
          android:layout_alignParentStart="true"
          android:layout_alignParentTop="true" />
</RelativeLayout>

在加载视频之前,像这样以编程方式更改高度和宽度:

    int i = videoView.getHeight() > videoView.getWidth() ? videoView.getHeight() : videoView.getWidth();
    video_holder.setLayoutParams(new ConstraintLayout.LayoutParams(i, i));

当然,这只适用于1:1的宽高比,但是你可以使用你的宽高比来更改高度或宽度。

这里应该使用RelativeLayout而不是ConstraintLayout,但它仍然无法工作。 - Martin Erlic
@MartinErlic 你的限制条件是正确的,但它对我来说有效,这就是我发布它的原因。如果您愿意,在GitHub / Lab上发布您的代码并在此处链接,我会为您检查它。 - jobbert

1

以下是Jobbert在Kotlin中的答案,如果有人需要:

val max = if (videoView.height > videoView.width) videoView.height else videoView.width
videoView.layoutParams = ConstraintLayout.LayoutParams(max, max)

0

最好的方法是将Videoview的宽度设置为最大值。 我曾经遇到过同样的问题。 我只是将宽度设置为999像素, 高度与父级匹配。 它有效果了。 我们可以得到像Tik Tok或Instagram Reels一样完美的视图。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接