ExoPlayer2可缩放视图实现及宽高比适配问题求助
问题梳理
针对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的方法。
核心问题分析
- 废弃的
setVideoListener方法:ExoPlayer2中SimpleExoPlayer.setVideoListener()已被废弃,直接使用会覆盖内部监听器导致异常,正确姿势是用addVideoListener()添加自定义监听器。 - TextureView无内置宽高比控制:PlayerView内部封装了
AspectRatioFrameLayout处理比例适配,直接用TextureView需要手动计算视频尺寸与视图的匹配关系。 - 触摸交互缺失: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

