You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ExoPlayer2可缩放视图实现及宽高比适配问题求助

解决ExoPlayer2中TextureView的宽高比与触摸交互问题

问题梳理

针对ExoPlayer第一版和原生安卓媒体播放器,存在不少重复的解决方案,但由于ExoPlayer2重构了大量内部代码,这些方案无法在ExoPlayer2上编译运行。经细致搜索,未找到适用于ExoPlayer2的捏合缩放、滚动等功能的库或示例代码,而静态图片(如通过Picasso获取的图片)的相关实现代码却很丰富。

更新:问题似乎在于无法对SimpleExoPlayer实例进行子类化或附加VideoListener;尝试操作后无任何效果,因为该实例已附加自身的监听器,而该监听器在使用TextureView时完全不考虑宽高比,导致视频无法正常使用。本可通过监听器轻松修正此问题,但相关方法已标记为废弃,强行使用会导致无视频输出。

以下代码可渲染并运行ua.pohohalo.zoomabletextureview(或普通TextureView),但无法为其附加VideoListener,且初始化时默认在竖屏模式下将视频垂直适配视图尺寸,破坏了宽高比。若将视频缩放到显示窗口尺寸以下还会出现严重故障,但此问题可在polohalo的代码中测试并修复。目前尚未解决的是如何让初始显示遵循原始宽高比,或在初始化时附加VideoListener进行设置——使用PlayerView时可正常运行,但无法扩展以支持平移。代码块中的“VideoListener”原型本应能解决宽高比问题,但无法附加,也找不到在原始视图上设置标记的方法(该方法也可实现需求),告知ExoPlayer遵循原始宽高比并适配屏幕尺寸。

在PlayerView上有效的simpleExoPlayerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT)调用,在TextureView上无效——缩放模式似乎默认设为RESIZE_MODE_FILL,且找不到将其设置为FIT的方法。

核心问题分析

  1. 废弃的setVideoListener方法:ExoPlayer2中SimpleExoPlayer.setVideoListener()已被废弃,直接使用会覆盖内部监听器导致异常,正确姿势是用addVideoListener()添加自定义监听器。
  2. TextureView无内置宽高比控制:PlayerView内部封装了AspectRatioFrameLayout处理比例适配,直接用TextureView需要手动计算视频尺寸与视图的匹配关系。
  3. 触摸交互缺失:TextureView本身不支持捏合缩放和平移,需要自定义触摸事件结合矩阵变换实现该功能。

解决方案步骤

1. 正确添加视频尺寸监听器

替换废弃的设置方法,改用addVideoListener确保监听器正常工作:

// 初始化播放器后添加监听器
player.addVideoListener(new VideoListener() {
    @Override
    public void onRenderedFirstFrame() {
        // 第一帧渲染完成后触发初始尺寸适配
        adjustVideoSize();
    }

    @Override
    public void onVideoSizeChanged(int width, int height, int rotation, float pixelWidthHeightRatio) {
        adjustVideoSize(width, height, pixelWidthHeightRatio);
    }
});

2. 手动实现宽高比适配

编写方法计算视频与视图的比例,生成变换矩阵实现RESIZE_MODE_FIT效果:

private void adjustVideoSize(int videoWidth, int videoHeight, float pixelRatio) {
    if (videoWidth <= 0 || videoHeight <= 0 || mPlayerView == null) return;

    // 计算视频实际宽高比(考虑像素密度比)
    float videoAspectRatio = (videoWidth * pixelRatio) / videoHeight;
    int viewWidth = mPlayerView.getWidth();
    int viewHeight = mPlayerView.getHeight();
    float viewAspectRatio = (float) viewWidth / viewHeight;

    Matrix matrix = new Matrix();
    float scaleX, scaleY;

    if (videoAspectRatio > viewAspectRatio) {
        // 视频更宽,按视图宽度适配,高度居中
        scaleX = 1f;
        scaleY = viewAspectRatio / videoAspectRatio;
    } else {
        // 视频更高,按视图高度适配,宽度居中
        scaleX = videoAspectRatio / viewAspectRatio;
        scaleY = 1f;
    }

    // 缩放后将视频居中显示
    matrix.setScale(scaleX, scaleY);
    float translateX = (viewWidth - viewWidth * scaleX) / 2f;
    float translateY = (viewHeight - viewHeight * scaleY) / 2f;
    matrix.postTranslate(translateX, translateY);

    mPlayerView.setTransform(matrix);
}

3. 自定义ZoomableTextureView实现触摸交互

基于普通TextureView扩展,添加捏合缩放和平移功能:

public class ZoomableTextureView extends TextureView {
    private static final float MIN_SCALE = 1.0f;
    private static final float MAX_SCALE = 3.0f;

    private Matrix transformMatrix = new Matrix();
    private float currentScale = MIN_SCALE;
    private float lastTouchX, lastTouchY;
    private int activePointerId = MotionEvent.INVALID_POINTER_ID;

    public ZoomableTextureView(Context context) {
        super(context);
        init();
    }

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

    private void init() {
        setTransform(transformMatrix);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                lastTouchX = event.getX();
                lastTouchY = event.getY();
                activePointerId = event.getPointerId(0);
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                // 双指触摸时记录初始距离
                float initialDistance = calculateDistance(event);
                currentScale = initialDistance;
                activePointerId = event.getPointerId(1);
                break;
            case MotionEvent.ACTION_MOVE:
                if (event.getPointerCount() == 2) {
                    // 捏合缩放逻辑
                    float newDistance = calculateDistance(event);
                    float scaleDelta = newDistance / currentScale;
                    currentScale = newDistance;

                    float targetScale = getCurrentScale() * scaleDelta;
                    targetScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, targetScale));
                    scaleDelta = targetScale / getCurrentScale();

                    // 以视图中心为基准缩放
                    transformMatrix.postScale(scaleDelta, scaleDelta, getWidth()/2f, getHeight()/2f);
                    setTransform(transformMatrix);
                } else if (activePointerId != MotionEvent.INVALID_POINTER_ID) {
                    // 单指平移逻辑
                    int pointerIndex = event.findPointerIndex(activePointerId);
                    float x = event.getX(pointerIndex);
                    float y = event.getY(pointerIndex);
                    float dx = x - lastTouchX;
                    float dy = y - lastTouchY;

                    transformMatrix.postTranslate(dx, dy);
                    setTransform(transformMatrix);

                    lastTouchX = x;
                    lastTouchY = y;
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                activePointerId = MotionEvent.INVALID_POINTER_ID;
                break;
        }
        return true;
    }

    private float calculateDistance(MotionEvent event) {
        float xDiff = event.getX(1) - event.getX(0);
        float yDiff = event.getY(1) - event.getY(0);
        return (float) Math.sqrt(xDiff * xDiff + yDiff * yDiff);
    }

    private float getCurrentScale() {
        float[] matrixValues = new float[9];
        transformMatrix.getValues(matrixValues);
        return matrixValues[Matrix.MSCALE_X];
    }

    // 重置缩放和平移状态
    public void resetTransform() {
        transformMatrix.reset();
        currentScale = MIN_SCALE;
        setTransform(transformMatrix);
    }
}

4. 整合到Activity中

修改布局使用自定义ZoomableTextureView,完成播放器初始化和逻辑绑定:

public class MainActivity extends AppCompatActivity {
    private String mURL = "http://point-at-an-mp4-file";
    ZoomableTextureView mPlayerView;
    SimpleExoPlayer player = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mPlayerView = findViewById(R.id.video_view2);
        initializePlayer();
    }

    private void initializePlayer() {
        DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON);
        player = ExoPlayerFactory.newSimpleInstance(
                renderersFactory,
                new DefaultTrackSelector(),
                new DefaultLoadControl());

        // 添加视频监听器处理宽高比
        player.addVideoListener(new VideoListener() {
            @Override
            public void onRenderedFirstFrame() {
                // 第一帧渲染后重置变换,确保初始比例正确
                mPlayerView.resetTransform();
                // 手动触发一次尺寸适配
                VideoSize size = player.getVideoFormat().videoSize;
                if (size != null) {
                    adjustVideoSize(size.width, size.height, size.pixelWidthHeightRatio);
                }
            }

            @Override
            public void onVideoSizeChanged(int width, int height, int rotation, float pixelWidthHeightRatio) {
                mPlayerView.resetTransform();
                adjustVideoSize(width, height, pixelWidthHeightRatio);
            }
        });

        player.setVideoTextureView(mPlayerView);
        // 准备媒体源
        MediaSource mediaSource = new ExtractorMediaSource.Factory(
                new DefaultHttpDataSourceFactory(Util.getUserAgent(this, "VideoTest")))
                .createMediaSource(Uri.parse(mURL));
        player.prepare(mediaSource);
        player.setPlayWhenReady(true);
    }

    private void adjustVideoSize(int videoWidth, int videoHeight, float pixelRatio) {
        if (videoWidth <= 0 || videoHeight <= 0 || mPlayerView == null) return;

        float videoAspectRatio = (videoWidth * pixelRatio) / videoHeight;
        int viewWidth = mPlayerView.getWidth();
        int viewHeight = mPlayerView.getHeight();
        float viewAspectRatio = (float) viewWidth / viewHeight;

        Matrix matrix = new Matrix();
        float scaleX, scaleY;

        if (videoAspectRatio > viewAspectRatio) {
            scaleX = 1f;
            scaleY = viewAspectRatio / videoAspectRatio;
        } else {
            scaleX = videoAspectRatio / viewAspectRatio;
            scaleY = 1f;
        }

        matrix.setScale(scaleX, scaleY);
        float translateX = (viewWidth - viewWidth * scaleX) / 2f;
        float translateY = (viewHeight - viewHeight * scaleY) / 2f;
        matrix.postTranslate(translateX, translateY);

        mPlayerView.setTransform(matrix);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (player != null) {
            player.release();
            player = null;
        }
    }
}

关键注意事项

  • 永远不要使用废弃的setVideoListener,改用addVideoListener避免覆盖ExoPlayer内部监听器,保证视频正常输出。
  • 自定义触摸处理时,注意矩阵变换的顺序(先缩放后平移),避免出现偏移异常。
  • 在onRenderedFirstFrame中手动触发尺寸适配,确保初始显示的宽高比正确。

内容的提问来源于stack exchange,提问作者Tickerguy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 07:36:25