├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── cn │ │ └── yinxm │ │ └── media │ │ └── video │ │ ├── ScaleVideoActivity.java │ │ ├── controller │ │ ├── SimpleVideoController.java │ │ └── VideoPlayController.java │ │ ├── gesture │ │ ├── GestureLayer.java │ │ └── touch │ │ │ ├── IGestureLayer.java │ │ │ ├── adapter │ │ │ ├── GestureVideoTouchAdapterImpl.java │ │ │ └── IVideoTouchAdapter.java │ │ │ ├── anim │ │ │ └── VideoScaleEndAnimator.java │ │ │ ├── handler │ │ │ ├── IVideoTouchHandler.java │ │ │ └── VideoTouchScaleHandler.java │ │ │ ├── listener │ │ │ ├── IVideoGestureListener.java │ │ │ └── VideoScaleGestureListener.java │ │ │ └── ui │ │ │ └── TouchScaleResetView.java │ │ ├── surface │ │ └── SimpleTextureViewPlayer.java │ │ └── util │ │ └── VideoUrlTest.java │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── ic_launcher_background.xml │ ├── small_new_pip_pause.png │ ├── small_new_pip_play.png │ └── touch_scale_rest_background.xml │ ├── layout │ ├── activity_scale_video.xml │ └── touch_scale_rest_view.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml ├── build.gradle ├── doc └── scale.gif ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [toc] 2 | 3 | # Android 视频手势缩放与回弹动效实现(一) 4 | 5 | 文章索引 6 | 7 | 1. [Android 视频手势缩放与回弹动效实现(一)](https://github.com/yinxuming/VideoTouchScale/blob/master/README.md):主要是实现视频双指:缩放、平移、回弹动效 8 | 1. [Android 视频旋转、缩放与回弹动效实现(二)](https://github.com/yinxuming/VideoTouchScaleRotate/blob/main/README.md):主要是实现视频双指:**旋转**、缩放、平移、回弹动效 9 | 10 | ## 1. 功能需求 11 | 12 |  13 | 1. 双指缩放视频播放画面,支持设定最小、最大缩放范围 14 | 2. 双指拖动画面可任意方向移动 15 | 3. 如果是缩小画面,最后需要在屏幕居中显示,并且需要有动画效果 16 | 4. 如果是放大画面,有画面边缘在屏幕内的,需要自动吸附到屏幕边缘 17 | 5. 视频暂停状态下也能缩放 18 | 19 | ## 2. 实现原理 20 | 1. 先进行缩放平移。 21 | 通过`View.getMatrix()`获取当前播放画面的Matrix,进行矩阵变换:缩放、平移,改变画面位置和大小,实现播放画面缩放功能。 22 | 2. 缩放结束后,进行属性动画。 23 | 当前画面对应的矩阵变换为`mScaleTransMatrix`,计算动画结束应该移动的位`scaleEndAnimMatrix`,进行属性动画从`mScaleTransMatrix`变化为`scaleEndAnimMatrix`。 24 | 25 | ### 2.1 如何检测手势缩放? 26 | 1. `View.onTouchEvent`。分别监听手指按下(`MotionEvent.ACTION_POINTER_DOWN`)、抬起(`MotionEvent.ACTION_POINTER_UP`)、移动(`MotionEvent.ACTION_MOVE`) 27 | 1. `ScaleGestureDetector`。直接使用手势缩放检测`ScaleGestureDetector`对View#onTouchEvent中的手势变化进行识别,通过`ScaleGestureDetector.OnScaleGestureListener`得到onScaleBegin-onScale-onScale ... -onScaleEnd的缩放回调,在回调中处理响应的缩放逻辑。 28 | 29 | #### 1. View.onTouchEvent关键代码 30 | ```java 31 | public boolean onTouchEvent(MotionEvent event) { 32 | int action = event.getAction() & MotionEvent.ACTION_MASK; 33 | switch (action) { 34 | case MotionEvent.ACTION_POINTER_DOWN: 35 | onScaleBegin(event); 36 | break; 37 | case MotionEvent.ACTION_POINTER_UP: 38 | onScaleEnd(event); 39 | break; 40 | case MotionEvent.ACTION_MOVE: 41 | onScale(event); 42 | break; 43 | case MotionEvent.ACTION_CANCEL: 44 | cancelScale(event); 45 | break; 46 | } 47 | return true; 48 | } 49 | ``` 50 | #### 2. ScaleGestureDetector 51 | 使用`ScaleGestureDetector`来识别onTouchEvent中的手势触摸操作,得到`onScaleBegin`、`onScale`、`onScaleEnd`三种回调,在回调里面通过`VideoTouchScaleHandler`对视频进行缩放、平移操作。 52 | 53 | 1. 添加手势触摸层`GestureLayer`,使用`ScaleGestureDetector`识别手势 54 | ```java 55 | /** 56 | * 手势处理layer层 57 | */ 58 | public final class GestureLayer implements IGestureLayer, GestureDetector.OnGestureListener, 59 | GestureDetector.OnDoubleTapListener { 60 | private static final String TAG = "GestureLayer"; 61 | 62 | private Context mContext; 63 | private FrameLayout mContainer; 64 | 65 | /** 手势检测 */ 66 | private GestureDetector mGestureDetector; 67 | 68 | /** 手势缩放 检测 */ 69 | private ScaleGestureDetector mScaleGestureDetector; 70 | /** 手势缩放 监听 */ 71 | private VideoScaleGestureListener mScaleGestureListener; 72 | /** 手势缩放 处理 */ 73 | private VideoTouchScaleHandler mScaleHandler; 74 | ``` 75 | 76 | 77 | private IVideoTouchAdapter mVideoTouchAdapter; 78 | 79 | public GestureLayer(Context context, IVideoTouchAdapter videoTouchAdapter) { 80 | mContext = context; 81 | mVideoTouchAdapter = videoTouchAdapter; 82 | initContainer(); 83 | initTouchHandler(); 84 | } 85 | 86 | private void initContainer() { 87 | mContainer = new FrameLayout(mContext) { 88 | @Override 89 | public boolean dispatchTouchEvent(MotionEvent ev) { 90 | return super.dispatchTouchEvent(ev); 91 | } 92 | 93 | @Override 94 | public boolean onInterceptTouchEvent(MotionEvent ev) { 95 | return super.onInterceptTouchEvent(ev); 96 | } 97 | 98 | @Override 99 | public boolean onTouchEvent(MotionEvent event) { 100 | boolean isConsume = onGestureTouchEvent(event); 101 | if (isConsume) { 102 | return true; 103 | } else { 104 | return super.onTouchEvent(event); 105 | } 106 | } 107 | }; 108 | } 109 | 110 | public void initTouchHandler() { 111 | mGestureDetector = new GestureDetector(mContext, this); 112 | mGestureDetector.setOnDoubleTapListener(this); 113 | 114 | // 手势缩放 115 | mScaleGestureListener = new VideoScaleGestureListener(this); 116 | mScaleGestureDetector = new ScaleGestureDetector(getContext(), mScaleGestureListener); 117 | 118 | // 缩放 处理 119 | mScaleHandler = new VideoTouchScaleHandler(getContext(), mContainer, mVideoTouchAdapter); 120 | mScaleGestureListener.mScaleHandler = mScaleHandler; 121 | 122 | } 123 | 124 | @Override 125 | public void onLayerRelease() { 126 | if (mGestureDetector != null) { 127 | mGestureDetector.setOnDoubleTapListener(null); 128 | } 129 | } 130 | 131 | @Override 132 | public boolean onGestureTouchEvent(MotionEvent event) { 133 | try { 134 | int pointCount = event.getPointerCount(); 135 | if (pointCount == 1 && event.getAction() == MotionEvent.ACTION_UP) { 136 | if (mScaleHandler.isScaled()) { 137 | mScaleHandler.showScaleReset(); 138 | } 139 | } 140 | if (pointCount > 1) { 141 | boolean isConsume = mScaleGestureDetector.onTouchEvent(event); 142 | if (isConsume) { 143 | return true; 144 | } 145 | } 146 | } catch (Exception e) { 147 | Log.e(TAG, "", e); 148 | } 149 | 150 | if (event.getAction() == MotionEvent.ACTION_DOWN) { 151 | return true; 152 | } 153 | return false; 154 | } 155 | 156 | ... 157 | } 158 | ``` 159 | 160 | 2. **ScaleGestureDetector.OnScaleGestureListener** 手势缩放回调处理 161 | 162 | ```java 163 | /** 164 | * 手势缩放 播放画面 165 | */ 166 | public class VideoScaleGestureListener implements ScaleGestureDetector.OnScaleGestureListener { 167 | private static final String TAG = "VideoScaleGestureListener"; 168 | private IGestureLayer mGestureLayer; 169 | public VideoTouchScaleHandler mScaleHandler; 170 | 171 | public VideoScaleGestureListener(IGestureLayer gestureLayer) { 172 | mGestureLayer = gestureLayer; 173 | } 174 | 175 | @Override 176 | public boolean onScale(ScaleGestureDetector detector) { 177 | if (mScaleHandler != null) { 178 | return mScaleHandler.onScale(detector); 179 | } 180 | return false; 181 | } 182 | 183 | @Override 184 | public boolean onScaleBegin(ScaleGestureDetector detector) { 185 | if (mScaleHandler != null) { 186 | boolean isConsume = mScaleHandler.onScaleBegin(detector); 187 | if (isConsume) { 188 | return true; 189 | } 190 | } 191 | return true; 192 | } 193 | 194 | @Override 195 | public void onScaleEnd(ScaleGestureDetector detector) { 196 | if (mScaleHandler != null) { 197 | mScaleHandler.onScaleEnd(detector); 198 | } 199 | 200 | } 201 | } 202 | ``` 203 | 204 | ### 2.2 缩放平移处理 205 | 1. **双指缩放** 206 | 使用`Matrix.postScale(float sx, float sy, float px, float py)`,这里有几个参数,前两个指定x,y轴上的缩放倍数,后两个指定缩放中心点位置。 207 | - 如何计算**缩放倍数**? 208 | 本次缩放倍数 = 本次两指间距 / 上次两指间距:`currentDiffScale = detector.getCurrentSpan() / mLastSpan` 209 | - 如何确定**缩放中心点**? 210 | 缩放中心为两指开始触摸时的中心位置点,即`onScaleBegin`时,`scaleCenterX = detector.getFocusX(); scaleCenterY = detector.getFocusY();` 211 | - **postXXX**和**preXXX**的区别? 212 | postXXX为右乘,preXXX为前乘。出现这两种操作,主要是**矩阵乘法不满足交换律**,实际使用过程中,固定选择一种方式即可。为了方便理解,直接来段代码,令:原矩阵M,位移变换矩阵T(x, y),则: 213 | ```java 214 | M.postTranslate(tx, ty); // 等价 M' = T * M 215 | M.preTranslate(tx, ty); // 等价 M' = M * T 216 | ``` 217 | 2. **双指平移** 218 | 双指可平移拖动画面到新位置,平移使用:`Matrix.postTranslate(float dx, float dy) 219 | `,dx和dy表示相对当前的Matrix的位置需要移动的**距离**,注意一定是相对于当前的Matrix位置,而不是相对onScaleBegin时的Matrix初始位置。 220 | - 如何确定**平移距离**? 221 | 本次移动距离 = 本次中心点 - 上次中心点 222 | ```java 223 | dx = detector.getFocusX() - mLastCenterX 224 | dy = detector.getFocusY() - mLastCenterY 225 | ``` 226 | ### 2.3 暂停画面下缩放 227 | 默认不处理,暂停画面情况下,Matrix变换后,更新到TextureView上,画面是不会发生变化的,要想画面实时更新,调用`TextureView.invalidate()`即可。 228 | 229 | 230 | ### 2.4 缩放移动结束后动效 231 | 缩放结束后(onScaleEnd),为了增强交互体验,需要根据缩放的大小、位置,重新调整画面,动画移动到指定位置。指定位置主要有**居中**和**吸附屏幕边缘**两种。 232 | 动画的移动,主要采用属性动画`ValueAnimator`. 233 | 234 | #### 1. 缩小居中 235 | 缩放结束后,画面如果处于缩小模式,需要将画面移动到屏幕中央。 236 | 1. 如何计算**居中位置矩阵**变换值? 237 | 缩放位移结束后得到变换后的矩阵`mScaleTransMatrix`,这也是动画的起始值,现在要推导动画的结束位置矩阵`scaleEndAnimMatrix`,要求在屏幕中居中,如果要直接用`mScaleTransMatrix`进行变换得到动画结束矩阵, 238 | 需要在xy上平移一定距离,但是该距离具体指并不好计算。 239 | 这里我们从另一个方向下手,知道当前的缩放倍速`mScale`,视频TextureView占的区域,那么直接以该区域中心点进行矩阵缩放变化,就可以得到中心位置矩阵`scaleEndAnimMatrix` 240 | ```java 241 | RectF videoRectF = new RectF(0, 0, mTextureView.getWidth(), mTextureView.getHeight()); 242 | if (mScale > 0 && mScale <= 1.0f) { // 缩小居中 243 | scaleEndAnimMatrix.reset(); 244 | scaleEndAnimMatrix.postScale(mScale, mScale, videoRectF.right / 2, videoRectF.bottom / 2); 245 | } 246 | ``` 247 | 2. 属性动画中间值,如何得到中间位置变换矩阵? 248 | - 动画开始矩阵:`mScaleTransMatrix`; 249 | - 动画开始矩阵:`scaleEndAnimMatrix`; 250 | 当从`mScaleTransMatrix`动画移动到`scaleEndAnimMatrix`位置时,中间的矩阵无非就是在x、y上位移了一定距离。以x轴为例: 251 | 1. x轴总位移:totalTransX = scaleEndAnimMatrix矩阵中取出MTRANS_X分量值 - mScaleTransMatrix矩阵中取出MTRANS_X分量值 252 | 1. 本次x轴移动距离:transX = totalTransX * 本次动画变化值 = totalTransX * (animation.getAnimatedValue() - mLastValue); 253 | 254 | #### 2. 放大吸边 255 | 缩放结束后,如果画面处于放大,且有画面边缘在屏幕内的,需要自动吸附到屏幕边缘。 256 | 1. 如何判断是否有**画面边缘在屏幕内部**? 257 | 需要考虑四边:left、top、right、bottom位置的情况。如果要考虑画面在屏幕内部的总情况数,比较繁琐和复杂,比如以left为例:有3种情况: 258 | 1. left:仅left边在屏幕内部,top、bottom边在屏幕外部,只需要移动画面left边到**屏幕左边**即可 259 | 2. left + top:left边和top边在屏幕内部,需要移动画面到屏幕**左上角**顶点位置 260 | 3. left + bottom:同上,需要移动画面到屏幕**左下角**顶点位置 261 | 262 | 总共有8种情况,那有没有简单的方法? 263 | 有的,实际上,不管哪种情况,我们只需要关注**画面的x、y方向需要移动的距离**即可。问题简化为求画面在x、y轴上移动的距离:`transAnimX`、`transAnimY` 264 | 只要知道上述两个值,将当前画面位移进行位移,即可得到动画结束位置矩阵`scaleEndAnimMatrix`。 265 | ```java 266 | scaleEndAnimMatrix.set(mScaleTransMatrix); 267 | scaleEndAnimMatrix.postTranslate(transAnimX, transAnimY); 268 | ``` 269 | 2. 如何计算画面在屏幕内部需要移动到各屏幕边缘的距离`transAnimX`、`transAnimY`? 270 | 要解决这个问题,需要知道**屏幕位置**,**播放画面位置**。 271 | 屏幕的位置很好办,实际上就是画面原始大小位置:`RectF videoRectF = new RectF(0, 0, mTextureView.getWidth(), mTextureView.getHeight());` 272 | 当前缩放移动后画面的位置呢? 273 | 它对应的矩阵变化是`mScaleTransMatrix`,那能不能**根据这个矩阵推导出当前画面的位置**? 274 | 可以的,我们去找Matrix对外提供的接口,会发现有一个`Matrix.mapRect(RectF)`方法,这个方法就是用来测量**矩形区域经过矩阵变化**后,新的矩形区域所在**位置**。直接上代码: 275 | ```java 276 | if (mScale > 1.0F) { // 放大,检测4边是否有在屏幕内部,有的话自动吸附到屏幕边缘 277 | RectF rectF = new RectF(0, 0, mTextureView.getWidth(), mTextureView.getHeight()); 278 | mScaleTransMatrix.mapRect(rectF); 279 | 280 | float transAnimX = 0f; 281 | float transAnimY = 0f; 282 | scaleEndAnimMatrix.set(mScaleTransMatrix); 283 | if (rectF.left > videoRectF.left 284 | || rectF.right < videoRectF.right 285 | || rectF.top > videoRectF.top 286 | || rectF.bottom < videoRectF.bottom) { // 放大情况下,有一边缩放后在屏幕内部,自动吸附到屏幕边缘 287 | if (rectF.left > videoRectF.left) { // 左移吸边 288 | transAnimX = videoRectF.left - rectF.left; 289 | } else if (rectF.right < videoRectF.right) { // 右移吸边 290 | transAnimX = videoRectF.right - rectF.right; 291 | } 292 | // 注意这里的处理方式:分别处理x轴位移和y轴位移即可全部覆盖上述8种情况 293 | if (rectF.top > videoRectF.top) { // 上移吸边 294 | transAnimY = videoRectF.top - rectF.top; 295 | } else if (rectF.bottom < videoRectF.bottom) { // 下移吸边 296 | transAnimY = videoRectF.bottom - rectF.bottom; 297 | } 298 | // 计算移动到屏幕边缘位置后的矩阵 299 | scaleEndAnimMatrix.postTranslate(transAnimX, transAnimY); 300 | } 301 | ``` 302 | 303 | ## 3. 项目完整代码 304 | [github完整源码](https://github.com/yinxuming/VideoTouchScale) 305 | 306 | ### 3.1 手势缩放处理:VideoTouchScaleHandler 307 | ```java 308 | /** 309 | * 播放器画面双指手势缩放处理: 310 | *
311 | * 1. 双指缩放 312 | * 2. 双指平移 313 | * 3. 缩放结束后,若为缩小画面,居中动效 314 | * 4. 缩放结束后,若为放大画面,自动吸附屏幕边缘动效 315 | * 5. 暂停播放下,实时更新缩放画面 316 | * 317 | * @author yinxuming 318 | * @date 2020/12/2 319 | */ 320 | public class VideoTouchScaleHandler implements IVideoTouchHandler, ScaleGestureDetector.OnScaleGestureListener { 321 | private static final String TAG = "VideoTouchScaleHandler"; 322 | 323 | 324 | private Context mContext; 325 | public FrameLayout mContainer; 326 | private boolean openScaleTouch = true; // 开启缩放 327 | private boolean mIsScaleTouch; 328 | private Matrix mScaleTransMatrix; // 缓存了上次的矩阵值,所以需要计算每次变化量 329 | private float mStartCenterX, mStartCenterY, mLastCenterX, mLastCenterY, centerX, centerY; 330 | private float mStartSpan, mLastSpan, mCurrentSpan; 331 | private float mScale; 332 | private float[] mMatrixValue = new float[9]; 333 | private float mMinScale = 0.1F, mMaxScale = 3F; 334 | private VideoScaleEndAnimator mScaleAnimator; 335 | 336 | IVideoTouchAdapter mTouchAdapter; 337 | TouchScaleResetView mScaleRestView; 338 | 339 | public VideoTouchScaleHandler(Context context, FrameLayout container, 340 | IVideoTouchAdapter videoTouchAdapter) { 341 | mContext = context; 342 | mContainer = container; 343 | mTouchAdapter = videoTouchAdapter; 344 | initView(); 345 | } 346 | 347 | private void initView() { 348 | mScaleRestView = new TouchScaleResetView(mContext, mContainer) { 349 | @Override 350 | public void clickResetScale() { 351 | mScaleRestView.setVisibility(View.GONE); 352 | if (isScaled()) { 353 | cancelScale(); 354 | } 355 | } 356 | }; 357 | } 358 | 359 | private Context getContext() { 360 | return mContext; 361 | } 362 | 363 | 364 | @Override 365 | public boolean onScaleBegin(ScaleGestureDetector detector) { 366 | 367 | TextureView mTextureView = mTouchAdapter.getTextureView(); 368 | if (mTextureView != null) { 369 | mIsScaleTouch = true; 370 | if (mScaleTransMatrix == null) { 371 | mScaleTransMatrix = new Matrix(mTextureView.getMatrix()); 372 | onScaleMatrixUpdate(mScaleTransMatrix); 373 | } 374 | } 375 | mStartCenterX = detector.getFocusX(); 376 | mStartCenterY = detector.getFocusY(); 377 | mStartSpan = detector.getCurrentSpan(); 378 | 379 | mLastCenterX = mStartCenterX; 380 | mLastCenterY = mStartCenterY; 381 | mLastSpan = mStartSpan; 382 | return true; 383 | } 384 | 385 | private void updateMatrixToTexture(Matrix newMatrix) { 386 | TextureView mTextureView = mTouchAdapter.getTextureView(); 387 | if (mTextureView != null) { 388 | mTextureView.setTransform(newMatrix); 389 | } 390 | onScaleMatrixUpdate(newMatrix); 391 | } 392 | 393 | @Override 394 | public boolean onScale(ScaleGestureDetector detector) { 395 | if (mIsScaleTouch && openScaleTouch) { 396 | mCurrentSpan = detector.getCurrentSpan(); 397 | centerX = detector.getFocusX(); 398 | centerY = detector.getFocusY(); 399 | if (processOnScale(detector)) { 400 | mLastCenterX = centerX; 401 | mLastCenterY = centerY; 402 | mLastSpan = mCurrentSpan; 403 | } 404 | } 405 | 406 | return false; 407 | } 408 | 409 | private boolean processOnScale(ScaleGestureDetector detector) { 410 | float diffScale = mCurrentSpan / mLastSpan; 411 | if (mTouchAdapter.isFullScreen()) { 412 | if (mScaleTransMatrix != null) { 413 | postScale(mScaleTransMatrix, diffScale, mStartCenterX, mStartCenterY); 414 | mScaleTransMatrix.postTranslate(detector.getFocusX() - mLastCenterX, 415 | detector.getFocusY() - mLastCenterY); 416 | onScaleMatrixUpdate(mScaleTransMatrix); 417 | TextureView mTextureView = mTouchAdapter.getTextureView(); 418 | if (mTextureView != null) { 419 | Matrix matrix = new Matrix(mTextureView.getMatrix()); 420 | matrix.set(mScaleTransMatrix); 421 | mTextureView.setTransform(matrix); 422 | } 423 | int scaleRatio = (int) (mScale * 100); 424 | Toast.makeText(getContext(), "" + scaleRatio + "%", Toast.LENGTH_SHORT).show(); 425 | return true; 426 | } 427 | } 428 | return false; 429 | } 430 | 431 | private void postScale(Matrix matrix, float scale, float x, float y) { 432 | matrix.getValues(mMatrixValue); 433 | float curScale = mMatrixValue[Matrix.MSCALE_X]; 434 | if (scale < 1 && Math.abs(curScale - mMinScale) < 0.001F) { 435 | scale = 1; 436 | } else if (scale > 1 && Math.abs(curScale - mMaxScale) < 0.001F) { 437 | scale = 1; 438 | } else { 439 | curScale *= scale; 440 | if (scale < 1 && curScale < mMinScale) { 441 | curScale = mMinScale; 442 | scale = curScale / mMatrixValue[Matrix.MSCALE_X]; 443 | } else if (scale > 1 && curScale > mMaxScale) { 444 | curScale = mMaxScale; 445 | scale = curScale / mMatrixValue[Matrix.MSCALE_X]; 446 | } 447 | matrix.postScale(scale, scale, x, y); 448 | } 449 | } 450 | 451 | 452 | @Override 453 | public void onScaleEnd(ScaleGestureDetector detector) { 454 | if (mIsScaleTouch) { // 取消多手势操作 455 | mIsScaleTouch = false; 456 | doScaleEndAnim(); 457 | } 458 | } 459 | 460 | public void cancelScale() { 461 | TextureView mTextureView = mTouchAdapter.getTextureView(); 462 | if (mScaleTransMatrix != null && mTextureView != null) { 463 | mIsScaleTouch = false; 464 | mScaleTransMatrix.reset(); 465 | onScaleMatrixUpdate(mScaleTransMatrix); 466 | Matrix matrix = new Matrix(mTextureView.getMatrix()); 467 | matrix.reset(); 468 | mTextureView.setTransform(matrix); 469 | } 470 | } 471 | 472 | /** 473 | * 计算缩放结束后动画位置:scaleEndAnimMatrix 474 | */ 475 | private void doScaleEndAnim() { 476 | TextureView mTextureView = mTouchAdapter.getTextureView(); 477 | if (mTextureView == null) { 478 | return; 479 | } 480 | Matrix scaleEndAnimMatrix = new Matrix(); 481 | RectF videoRectF = new RectF(0, 0, mTextureView.getWidth(), mTextureView.getHeight()); 482 | if (mScale > 0 && mScale <= 1.0f) { // 缩小居中 483 | scaleEndAnimMatrix.postScale(mScale, mScale, videoRectF.right / 2, videoRectF.bottom / 2); 484 | startTransToAnimEnd(mScaleTransMatrix, scaleEndAnimMatrix); 485 | } else if (mScale > 1.0F) { // 放大,检测4边是否有在屏幕内部,有的话自动吸附到屏幕边缘 486 | RectF rectF = new RectF(0, 0, mTextureView.getWidth(), mTextureView.getHeight()); 487 | // 测量经过缩放位移变换后的播放画面位置 488 | mScaleTransMatrix.mapRect(rectF); 489 | float transAnimX = 0f; 490 | float transAnimY = 0f; 491 | scaleEndAnimMatrix.set(mScaleTransMatrix); 492 | if (rectF.left > videoRectF.left 493 | || rectF.right < videoRectF.right 494 | || rectF.top > videoRectF.top 495 | || rectF.bottom < videoRectF.bottom) { // 放大情况下,有一边缩放后在屏幕内部,自动吸附到屏幕边缘 496 | if (rectF.left > videoRectF.left) { // 左移吸边 497 | transAnimX = videoRectF.left - rectF.left; 498 | } else if (rectF.right < videoRectF.right) { // 右移吸边 499 | transAnimX = videoRectF.right - rectF.right; 500 | } 501 | if (rectF.top > videoRectF.top) { // 上移吸边 502 | transAnimY = videoRectF.top - rectF.top; 503 | } else if (rectF.bottom < videoRectF.bottom) { // 下移吸边 504 | transAnimY = videoRectF.bottom - rectF.bottom; 505 | } 506 | 507 | scaleEndAnimMatrix.postTranslate(transAnimX, transAnimY); 508 | startTransToAnimEnd(mScaleTransMatrix, scaleEndAnimMatrix); 509 | } 510 | } 511 | } 512 | 513 | private void startTransToAnimEnd(Matrix startMatrix, Matrix endMatrix) { 514 | LogUtil.d(TAG, "startTransToAnimEnd \nstart=" + startMatrix + "\nend=" + endMatrix); 515 | // 令 A = startMatrix;B = endMatrix 516 | // 方法1:直接将画面更新为结束矩阵位置B 517 | // updateMatrixToView(endMatrix); // 518 | // 方法2:将画面从现有位置A,移动到结束矩阵位置B,移动的距离T。B = T * A; 根据矩阵乘法的计算规则,反推出:T(x) = B(x) - A(x); T(y) = B(y) - A(y) 519 | // float[] startArray = new float[9]; 520 | // float[] endArray = new float[9]; 521 | // startMatrix.getValues(startArray); 522 | // endMatrix.getValues(endArray); 523 | // float transX = endArray[Matrix.MTRANS_X] - startArray[Matrix.MTRANS_X]; 524 | // float transY = endArray[Matrix.MTRANS_Y] - startArray[Matrix.MTRANS_Y]; 525 | // startMatrix.postTranslate(transX, transY); 526 | // LogUtil.d(TAG, "transToCenter1 \nstart=" + startMatrix + "\nend" + endMatrix); 527 | // updateMatrixToView(startMatrix); 528 | 529 | // 方法3:在方法2基础上,增加动画移动效果 530 | if (mScaleAnimator != null) { 531 | mScaleAnimator.cancel(); 532 | mScaleAnimator = null; 533 | } 534 | if (mScaleAnimator == null) { 535 | mScaleAnimator = new VideoScaleEndAnimator(startMatrix, endMatrix) { 536 | 537 | @Override 538 | protected void updateMatrixToView(Matrix transMatrix) { 539 | updateMatrixToTexture(transMatrix); 540 | } 541 | }; 542 | mScaleAnimator.start(); 543 | } 544 | 545 | mScaleTransMatrix = endMatrix; 546 | } 547 | 548 | public void showScaleReset() { 549 | if (isScaled() && mTouchAdapter != null && mTouchAdapter.isFullScreen()) { 550 | if (mScaleRestView != null && mScaleRestView.getVisibility() != View.VISIBLE) { 551 | mScaleRestView.setVisibility(View.VISIBLE); 552 | } 553 | } 554 | } 555 | 556 | public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 557 | // 缩放模式下,是否需要单手滚动 558 | // if (isScaled(mScale) && mScaleTransMatrix != null) { 559 | // TextureView mTextureView = mTouchAdapter.getTextureView(); 560 | // if (mTextureView != null) { 561 | // postTranslate(mScaleTransMatrix, -distanceX, -distanceY); 562 | // onScaleMatrixUpdate(mScaleTransMatrix); 563 | // Matrix matrix = new Matrix(mTextureView.getMatrix()); 564 | // matrix.set(mScaleTransMatrix); 565 | // mTextureView.setTransform(matrix); 566 | // return true; 567 | // } 568 | // } 569 | return false; 570 | } 571 | 572 | 573 | 574 | private void onScaleMatrixUpdate(Matrix matrix) { 575 | matrix.getValues(mMatrixValue); 576 | mScale = mMatrixValue[Matrix.MSCALE_X]; 577 | // 暂停下,实时更新缩放画面 578 | if (!mTouchAdapter.isPlaying()) { 579 | TextureView mTextureView = mTouchAdapter.getTextureView(); 580 | if (mTextureView != null) { 581 | mTextureView.invalidate(); 582 | } 583 | } 584 | } 585 | 586 | /** 587 | * 是否处于已缩放 or 缩放中 588 | * 589 | * @return 590 | */ 591 | public boolean isInScaleStatus() { 592 | return isScaled(mScale) || mIsScaleTouch; 593 | } 594 | 595 | public boolean isScaled() { 596 | return isScaled(mScale); 597 | } 598 | 599 | private boolean isScaled(float scale) { 600 | return scale > 0 && scale <= 0.99F || scale >= 1.01F; 601 | } 602 | } 603 | 604 | ``` 605 | ### 3.2 动画:VideoScaleEndAnimator 606 | ```java 607 | 608 | /** 609 | * 缩放动画 610 | *
611 | * 在给定时间内从一个矩阵的变化逐渐动画到另一个矩阵的变化 612 | */ 613 | public abstract class VideoScaleEndAnimator extends ValueAnimator implements ValueAnimator.AnimatorUpdateListener { 614 | private static final String TAG = "VideoScaleEndAnimator"; 615 | 616 | /** 617 | * 图片缩放动画时间 618 | */ 619 | public static final int SCALE_ANIMATOR_DURATION = 300; 620 | 621 | Matrix mTransMatrix = new Matrix(); 622 | float[] mTransSpan = new float[2]; 623 | float mLastValue; 624 | 625 | /** 626 | * 构建一个缩放动画 627 | *
628 | * 从一个矩阵变换到另外一个矩阵 629 | * 630 | * @param start 开始矩阵 631 | * @param end 结束矩阵 632 | */ 633 | public VideoScaleEndAnimator(Matrix start, Matrix end) { 634 | this(start, end, SCALE_ANIMATOR_DURATION); 635 | } 636 | 637 | /** 638 | * 构建一个缩放动画 639 | *
640 | * 从一个矩阵变换到另外一个矩阵
641 | *
642 | * @param start 开始矩阵
643 | * @param end 结束矩阵
644 | * @param duration 动画时间
645 | */
646 | public VideoScaleEndAnimator(Matrix start, Matrix end, long duration) {
647 | super();
648 | setFloatValues(0, 1f);
649 | setDuration(duration);
650 | addUpdateListener(this);
651 |
652 | float[] startValues = new float[9];
653 | float[] endValues = new float[9];
654 | start.getValues(startValues);
655 | end.getValues(endValues);
656 | mTransSpan[0] = endValues[Matrix.MTRANS_X] - startValues[Matrix.MTRANS_X];
657 | mTransSpan[1] = endValues[Matrix.MTRANS_Y] - startValues[Matrix.MTRANS_Y];
658 | mTransMatrix.set(start);
659 | }
660 |
661 | @Override
662 | public void onAnimationUpdate(ValueAnimator animation) {
663 | // 获取动画进度
664 | float value = (Float) animation.getAnimatedValue();
665 | // 计算相对于上次位置的偏移量
666 | float transX = mTransSpan[0] * (value - mLastValue);
667 | float transY = mTransSpan[1] * (value - mLastValue);
668 | mTransMatrix.postTranslate(transX, transY);
669 | updateMatrixToView(mTransMatrix);
670 | mLastValue = value;
671 | }
672 |
673 | protected abstract void updateMatrixToView(Matrix transMatrix);
674 | }
675 | ```
676 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | }
4 |
5 | android {
6 | compileSdkVersion rootProject.ext.compileSdkVersion
7 | buildToolsVersion rootProject.ext.buildToolsVersion
8 |
9 | defaultConfig {
10 | applicationId "cn.yinxm.video.scale"
11 | minSdkVersion rootProject.ext.minSdkVersion
12 | targetSdkVersion rootProject.ext.targetSdkVersion
13 | versionCode 1
14 | versionName "1.0"
15 | }
16 |
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | compileOptions {
24 | sourceCompatibility JavaVersion.VERSION_1_8
25 | targetCompatibility JavaVersion.VERSION_1_8
26 | }
27 | }
28 |
29 | dependencies {
30 |
31 | implementation 'androidx.appcompat:appcompat:' + rootProject.ext.androidx_appcompat
32 | implementation 'androidx.constraintlayout:constraintlayout:' + rootProject.ext.androidx_constraintlayout
33 |
34 | implementation 'com.gitee.h4x0r.Lib-Android:android-lib-base:' + rootProject.ext.Lib_Android
35 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | * 6 | * @author yinxuming 7 | * @date 2020/7/6 8 | */ 9 | public class SimpleVideoController implements VideoPlayController { 10 | @Override 11 | public void start() { 12 | 13 | } 14 | 15 | @Override 16 | public void pause() { 17 | 18 | } 19 | 20 | @Override 21 | public int getDuration() { 22 | return 0; 23 | } 24 | 25 | @Override 26 | public int getCurrentPosition() { 27 | return 0; 28 | } 29 | 30 | @Override 31 | public void seekTo(int pos) { 32 | 33 | } 34 | 35 | @Override 36 | public boolean isPlaying() { 37 | return false; 38 | } 39 | 40 | @Override 41 | public int getBufferPercentage() { 42 | return 0; 43 | } 44 | 45 | @Override 46 | public boolean canPause() { 47 | return false; 48 | } 49 | 50 | @Override 51 | public boolean canSeekBackward() { 52 | return false; 53 | } 54 | 55 | @Override 56 | public boolean canSeekForward() { 57 | return false; 58 | } 59 | 60 | @Override 61 | public int getAudioSessionId() { 62 | return 0; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/cn/yinxm/media/video/controller/VideoPlayController.java: -------------------------------------------------------------------------------- 1 | package cn.yinxm.media.video.controller; 2 | 3 | import android.widget.MediaController; 4 | 5 | /** 6 | *
7 | * 8 | * @author yinxuming 9 | * @date 2019-07-17 10 | */ 11 | public interface VideoPlayController extends MediaController.MediaPlayerControl { 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/cn/yinxm/media/video/gesture/GestureLayer.java: -------------------------------------------------------------------------------- 1 | package cn.yinxm.media.video.gesture; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | import android.view.GestureDetector; 6 | import android.view.MotionEvent; 7 | import android.view.ScaleGestureDetector; 8 | import android.widget.FrameLayout; 9 | 10 | import cn.yinxm.media.video.gesture.touch.IGestureLayer; 11 | import cn.yinxm.media.video.gesture.touch.adapter.IVideoTouchAdapter; 12 | import cn.yinxm.media.video.gesture.touch.handler.VideoTouchScaleHandler; 13 | import cn.yinxm.media.video.gesture.touch.listener.VideoScaleGestureListener; 14 | 15 | 16 | /** 17 | * 手势处理layer层 18 | */ 19 | public final class GestureLayer implements IGestureLayer, GestureDetector.OnGestureListener, 20 | GestureDetector.OnDoubleTapListener { 21 | private static final String TAG = "GestureLayer"; 22 | 23 | private Context mContext; 24 | private FrameLayout mContainer; 25 | 26 | /** 手势检测 */ 27 | private GestureDetector mGestureDetector; 28 | 29 | /** 手势缩放 检测 */ 30 | private ScaleGestureDetector mScaleGestureDetector; 31 | /** 手势缩放 监听 */ 32 | private VideoScaleGestureListener mScaleGestureListener; 33 | /** 手势缩放 处理 */ 34 | private VideoTouchScaleHandler mScaleHandler; 35 | 36 | 37 | private IVideoTouchAdapter mVideoTouchAdapter; 38 | 39 | public GestureLayer(Context context, IVideoTouchAdapter videoTouchAdapter) { 40 | mContext = context; 41 | mVideoTouchAdapter = videoTouchAdapter; 42 | initContainer(); 43 | initTouchHandler(); 44 | } 45 | 46 | @Override 47 | public FrameLayout getContainer() { 48 | return mContainer; 49 | } 50 | 51 | protected Context getContext() { 52 | return mContext; 53 | } 54 | 55 | private void initContainer() { 56 | mContainer = new FrameLayout(mContext) { 57 | @Override 58 | public boolean dispatchTouchEvent(MotionEvent ev) { 59 | return super.dispatchTouchEvent(ev); 60 | } 61 | 62 | @Override 63 | public boolean onInterceptTouchEvent(MotionEvent ev) { 64 | return super.onInterceptTouchEvent(ev); 65 | } 66 | 67 | @Override 68 | public boolean onTouchEvent(MotionEvent event) { 69 | boolean isConsume = onGestureTouchEvent(event); 70 | if (isConsume) { 71 | return true; 72 | } else { 73 | return super.onTouchEvent(event); 74 | } 75 | } 76 | }; 77 | } 78 | 79 | public void initTouchHandler() { 80 | mGestureDetector = new GestureDetector(mContext, this); 81 | mGestureDetector.setOnDoubleTapListener(this); 82 | 83 | // 手势缩放 84 | mScaleGestureListener = new VideoScaleGestureListener(this); 85 | mScaleGestureDetector = new ScaleGestureDetector(getContext(), mScaleGestureListener); 86 | 87 | // 缩放 处理 88 | mScaleHandler = new VideoTouchScaleHandler(getContext(), mContainer, mVideoTouchAdapter); 89 | mScaleGestureListener.mScaleHandler = mScaleHandler; 90 | 91 | } 92 | 93 | @Override 94 | public void onLayerRelease() { 95 | if (mGestureDetector != null) { 96 | mGestureDetector.setOnDoubleTapListener(null); 97 | } 98 | } 99 | 100 | @Override 101 | public boolean onGestureTouchEvent(MotionEvent event) { 102 | try { 103 | int pointCount = event.getPointerCount(); 104 | if (pointCount == 1 && event.getAction() == MotionEvent.ACTION_UP) { 105 | if (mScaleHandler.isScaled()) { 106 | mScaleHandler.showScaleReset(); 107 | } 108 | } 109 | if (pointCount > 1) { 110 | boolean isConsume = mScaleGestureDetector.onTouchEvent(event); 111 | if (isConsume) { 112 | return true; 113 | } 114 | } 115 | } catch (Exception e) { 116 | Log.e(TAG, "", e); 117 | } 118 | 119 | if (event.getAction() == MotionEvent.ACTION_DOWN) { 120 | return true; 121 | } 122 | return false; 123 | } 124 | 125 | @Override 126 | public boolean onSingleTapConfirmed(MotionEvent e) { 127 | return onSingleTap(e); 128 | } 129 | 130 | /** 131 | * 单击事件处理 132 | * 133 | * @param event 触摸事件 134 | */ 135 | private boolean onSingleTap(MotionEvent event) { 136 | return true; 137 | } 138 | 139 | @Override 140 | public boolean onDoubleTap(MotionEvent e) { 141 | return true; 142 | } 143 | 144 | @Override 145 | public boolean onDoubleTapEvent(MotionEvent e) { 146 | return false; 147 | } 148 | 149 | @Override 150 | public boolean onDown(MotionEvent e) { 151 | return false; 152 | } 153 | 154 | @Override 155 | public void onShowPress(MotionEvent e) { 156 | 157 | } 158 | 159 | @Override 160 | public boolean onSingleTapUp(MotionEvent e) { 161 | return false; 162 | } 163 | 164 | @Override 165 | public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 166 | if (mScaleHandler.isInScaleStatus()) { 167 | // if (mScaleHandler.isScaled()) { 168 | return mScaleHandler.onScroll(e1, e2, distanceX, distanceY); 169 | // } 170 | } 171 | return false; 172 | } 173 | 174 | @Override 175 | public void onLongPress(MotionEvent e) { 176 | 177 | } 178 | 179 | @Override 180 | public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, 181 | float velocityY) { 182 | return false; 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /app/src/main/java/cn/yinxm/media/video/gesture/touch/IGestureLayer.java: -------------------------------------------------------------------------------- 1 | package cn.yinxm.media.video.gesture.touch; 2 | 3 | import android.view.MotionEvent; 4 | import android.widget.FrameLayout; 5 | 6 | /** 7 | *
8 | * 9 | * @author yinxuming 10 | * @date 2020/11/24 11 | */ 12 | public interface IGestureLayer { 13 | FrameLayout getContainer(); 14 | 15 | /** 16 | * 事件处理器 17 | */ 18 | void initTouchHandler(); 19 | 20 | /** 21 | * 分发touch事件 22 | * 23 | * @param event 24 | * @return 25 | */ 26 | boolean onGestureTouchEvent(MotionEvent event); 27 | 28 | void onLayerRelease(); 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/cn/yinxm/media/video/gesture/touch/adapter/GestureVideoTouchAdapterImpl.java: -------------------------------------------------------------------------------- 1 | package cn.yinxm.media.video.gesture.touch.adapter; 2 | 3 | import android.view.TextureView; 4 | 5 | import cn.yinxm.media.video.controller.VideoPlayController; 6 | 7 | 8 | /** 9 | * 播放器手势触摸适配,兼容HkBaseVideoView升级到新播放器BaseVideoPlayer 10 | *
11 | * 12 | * @author yinxuming 13 | * @date 2020/5/18 14 | */ 15 | public class GestureVideoTouchAdapterImpl implements IVideoTouchAdapter { 16 | VideoPlayController mPlayController; 17 | 18 | public GestureVideoTouchAdapterImpl(VideoPlayController playController) { 19 | mPlayController = playController; 20 | } 21 | 22 | @Override 23 | public TextureView getTextureView() { 24 | if (mPlayController instanceof TextureView) { 25 | return (TextureView) mPlayController; 26 | } 27 | return null; 28 | } 29 | 30 | @Override 31 | public boolean isPlaying() { 32 | return mPlayController.isPlaying(); 33 | } 34 | 35 | 36 | 37 | @Override 38 | public boolean isFullScreen() { 39 | return false; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/cn/yinxm/media/video/gesture/touch/adapter/IVideoTouchAdapter.java: -------------------------------------------------------------------------------- 1 | package cn.yinxm.media.video.gesture.touch.adapter; 2 | 3 | import android.view.TextureView; 4 | 5 | /** 6 | * 播放器手势触摸适配,手势与播放器之间的适配层 7 | *
8 | * 9 | * @author yinxuming 10 | * @date 2020/5/14 11 | */ 12 | public interface IVideoTouchAdapter { 13 | TextureView getTextureView(); 14 | 15 | boolean isPlaying(); 16 | 17 | boolean isFullScreen(); 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/cn/yinxm/media/video/gesture/touch/anim/VideoScaleEndAnimator.java: -------------------------------------------------------------------------------- 1 | package cn.yinxm.media.video.gesture.touch.anim; 2 | 3 | import android.animation.ValueAnimator; 4 | import android.graphics.Matrix; 5 | 6 | /** 7 | * 缩放动画 8 | *
9 | * 在给定时间内从一个矩阵的变化逐渐动画到另一个矩阵的变化 10 | */ 11 | public abstract class VideoScaleEndAnimator extends ValueAnimator implements ValueAnimator.AnimatorUpdateListener { 12 | private static final String TAG = "VideoScaleEndAnimator"; 13 | 14 | /** 15 | * 图片缩放动画时间 16 | */ 17 | public static final int SCALE_ANIMATOR_DURATION = 300; 18 | 19 | Matrix mTransMatrix = new Matrix(); 20 | float[] mTransSpan = new float[2]; 21 | float mLastValue; 22 | 23 | /** 24 | * 构建一个缩放动画 25 | *
26 | * 从一个矩阵变换到另外一个矩阵 27 | * 28 | * @param start 开始矩阵 29 | * @param end 结束矩阵 30 | */ 31 | public VideoScaleEndAnimator(Matrix start, Matrix end) { 32 | this(start, end, SCALE_ANIMATOR_DURATION); 33 | } 34 | 35 | /** 36 | * 构建一个缩放动画 37 | *
38 | * 从一个矩阵变换到另外一个矩阵 39 | * 40 | * @param start 开始矩阵 41 | * @param end 结束矩阵 42 | * @param duration 动画时间 43 | */ 44 | public VideoScaleEndAnimator(Matrix start, Matrix end, long duration) { 45 | super(); 46 | setFloatValues(0, 1f); 47 | setDuration(duration); 48 | addUpdateListener(this); 49 | 50 | float[] startValues = new float[9]; 51 | float[] endValues = new float[9]; 52 | start.getValues(startValues); 53 | end.getValues(endValues); 54 | mTransSpan[0] = endValues[Matrix.MTRANS_X] - startValues[Matrix.MTRANS_X]; 55 | mTransSpan[1] = endValues[Matrix.MTRANS_Y] - startValues[Matrix.MTRANS_Y]; 56 | mTransMatrix.set(start); 57 | } 58 | 59 | @Override 60 | public void onAnimationUpdate(ValueAnimator animation) { 61 | // 获取动画进度 62 | float value = (Float) animation.getAnimatedValue(); 63 | // 计算相对于上次位置的偏移量 64 | float transX = mTransSpan[0] * (value - mLastValue); 65 | float transY = mTransSpan[1] * (value - mLastValue); 66 | mTransMatrix.postTranslate(transX, transY); 67 | updateMatrixToView(mTransMatrix); 68 | mLastValue = value; 69 | } 70 | 71 | protected abstract void updateMatrixToView(Matrix transMatrix); 72 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/yinxm/media/video/gesture/touch/handler/IVideoTouchHandler.java: -------------------------------------------------------------------------------- 1 | package cn.yinxm.media.video.gesture.touch.handler; 2 | 3 | /** 4 | *
5 | * 6 | * @author yinxuming 7 | * @date 2020/5/19 8 | */ 9 | public interface IVideoTouchHandler { 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/cn/yinxm/media/video/gesture/touch/handler/VideoTouchScaleHandler.java: -------------------------------------------------------------------------------- 1 | package cn.yinxm.media.video.gesture.touch.handler; 2 | 3 | import android.content.Context; 4 | import android.graphics.Matrix; 5 | import android.graphics.RectF; 6 | import android.view.MotionEvent; 7 | import android.view.ScaleGestureDetector; 8 | import android.view.TextureView; 9 | import android.view.View; 10 | import android.widget.FrameLayout; 11 | import android.widget.Toast; 12 | 13 | import cn.yinxm.lib.utils.log.LogUtil; 14 | import cn.yinxm.media.video.gesture.touch.adapter.IVideoTouchAdapter; 15 | import cn.yinxm.media.video.gesture.touch.anim.VideoScaleEndAnimator; 16 | import cn.yinxm.media.video.gesture.touch.ui.TouchScaleResetView; 17 | 18 | 19 | /** 20 | * 播放器画面双指手势缩放处理: 21 | *
22 | * 1. 双指缩放 23 | * 2. 双指平移 24 | * 3. 缩放结束后,若为缩小画面,居中动效 25 | * 4. 缩放结束后,若为放大画面,自动吸附屏幕边缘动效 26 | * 5. 暂停播放下,实时更新缩放画面 27 | * 28 | * @author yinxuming 29 | * @date 2020/12/2 30 | */ 31 | public class VideoTouchScaleHandler implements IVideoTouchHandler, ScaleGestureDetector.OnScaleGestureListener { 32 | private static final String TAG = "VideoTouchScaleHandler"; 33 | 34 | 35 | private Context mContext; 36 | public FrameLayout mContainer; 37 | private boolean openScaleTouch = true; // 开启缩放 38 | private boolean mIsScaleTouch; 39 | private Matrix mScaleTransMatrix; // 缓存了上次的矩阵值,所以需要计算每次变化量 40 | private float mStartCenterX, mStartCenterY, mLastCenterX, mLastCenterY, centerX, centerY; 41 | private float mStartSpan, mLastSpan, mCurrentSpan; 42 | private float mScale; 43 | private float[] mMatrixValue = new float[9]; 44 | private float mMinScale = 0.1F, mMaxScale = 3F; 45 | private VideoScaleEndAnimator mScaleAnimator; 46 | 47 | IVideoTouchAdapter mTouchAdapter; 48 | TouchScaleResetView mScaleRestView; 49 | 50 | public VideoTouchScaleHandler(Context context, FrameLayout container, 51 | IVideoTouchAdapter videoTouchAdapter) { 52 | mContext = context; 53 | mContainer = container; 54 | mTouchAdapter = videoTouchAdapter; 55 | initView(); 56 | } 57 | 58 | private void initView() { 59 | mScaleRestView = new TouchScaleResetView(mContext, mContainer) { 60 | @Override 61 | public void clickResetScale() { 62 | mScaleRestView.setVisibility(View.GONE); 63 | if (isScaled()) { 64 | cancelScale(); 65 | } 66 | } 67 | }; 68 | } 69 | 70 | private Context getContext() { 71 | return mContext; 72 | } 73 | 74 | 75 | @Override 76 | public boolean onScaleBegin(ScaleGestureDetector detector) { 77 | 78 | TextureView mTextureView = mTouchAdapter.getTextureView(); 79 | if (mTextureView != null) { 80 | mIsScaleTouch = true; 81 | if (mScaleTransMatrix == null) { 82 | mScaleTransMatrix = new Matrix(mTextureView.getMatrix()); 83 | onScaleMatrixUpdate(mScaleTransMatrix); 84 | } 85 | } 86 | mStartCenterX = detector.getFocusX(); 87 | mStartCenterY = detector.getFocusY(); 88 | mStartSpan = detector.getCurrentSpan(); 89 | 90 | mLastCenterX = mStartCenterX; 91 | mLastCenterY = mStartCenterY; 92 | mLastSpan = mStartSpan; 93 | return true; 94 | } 95 | 96 | private void updateMatrixToTexture(Matrix newMatrix) { 97 | TextureView mTextureView = mTouchAdapter.getTextureView(); 98 | if (mTextureView != null) { 99 | mTextureView.setTransform(newMatrix); 100 | } 101 | onScaleMatrixUpdate(newMatrix); 102 | } 103 | 104 | @Override 105 | public boolean onScale(ScaleGestureDetector detector) { 106 | if (mIsScaleTouch && openScaleTouch) { 107 | mCurrentSpan = detector.getCurrentSpan(); 108 | centerX = detector.getFocusX(); 109 | centerY = detector.getFocusY(); 110 | if (processOnScale(detector)) { 111 | mLastCenterX = centerX; 112 | mLastCenterY = centerY; 113 | mLastSpan = mCurrentSpan; 114 | } 115 | } 116 | 117 | return false; 118 | } 119 | 120 | private boolean processOnScale(ScaleGestureDetector detector) { 121 | float diffScale = mCurrentSpan / mLastSpan; 122 | if (mTouchAdapter.isFullScreen()) { 123 | if (mScaleTransMatrix != null) { 124 | postScale(mScaleTransMatrix, diffScale, mStartCenterX, mStartCenterY); 125 | mScaleTransMatrix.postTranslate(detector.getFocusX() - mLastCenterX, 126 | detector.getFocusY() - mLastCenterY); 127 | onScaleMatrixUpdate(mScaleTransMatrix); 128 | TextureView mTextureView = mTouchAdapter.getTextureView(); 129 | if (mTextureView != null) { 130 | Matrix matrix = new Matrix(mTextureView.getMatrix()); 131 | matrix.set(mScaleTransMatrix); 132 | mTextureView.setTransform(matrix); 133 | } 134 | int scaleRatio = (int) (mScale * 100); 135 | Toast.makeText(getContext(), "" + scaleRatio + "%", Toast.LENGTH_SHORT).show(); 136 | return true; 137 | } 138 | } 139 | return false; 140 | } 141 | 142 | private void postScale(Matrix matrix, float scale, float x, float y) { 143 | matrix.getValues(mMatrixValue); 144 | float curScale = mMatrixValue[Matrix.MSCALE_X]; 145 | if (scale < 1 && Math.abs(curScale - mMinScale) < 0.001F) { 146 | scale = 1; 147 | } else if (scale > 1 && Math.abs(curScale - mMaxScale) < 0.001F) { 148 | scale = 1; 149 | } else { 150 | curScale *= scale; 151 | if (scale < 1 && curScale < mMinScale) { 152 | curScale = mMinScale; 153 | scale = curScale / mMatrixValue[Matrix.MSCALE_X]; 154 | } else if (scale > 1 && curScale > mMaxScale) { 155 | curScale = mMaxScale; 156 | scale = curScale / mMatrixValue[Matrix.MSCALE_X]; 157 | } 158 | matrix.postScale(scale, scale, x, y); 159 | } 160 | } 161 | 162 | 163 | @Override 164 | public void onScaleEnd(ScaleGestureDetector detector) { 165 | if (mIsScaleTouch) { // 取消多手势操作 166 | mIsScaleTouch = false; 167 | doScaleEndAnim(); 168 | } 169 | } 170 | 171 | public void cancelScale() { 172 | TextureView mTextureView = mTouchAdapter.getTextureView(); 173 | if (mScaleTransMatrix != null && mTextureView != null) { 174 | mIsScaleTouch = false; 175 | mScaleTransMatrix.reset(); 176 | onScaleMatrixUpdate(mScaleTransMatrix); 177 | Matrix matrix = new Matrix(mTextureView.getMatrix()); 178 | matrix.reset(); 179 | mTextureView.setTransform(matrix); 180 | } 181 | } 182 | 183 | /** 184 | * 计算缩放结束后动画位置:scaleEndAnimMatrix 185 | */ 186 | private void doScaleEndAnim() { 187 | TextureView mTextureView = mTouchAdapter.getTextureView(); 188 | if (mTextureView == null) { 189 | return; 190 | } 191 | Matrix scaleEndAnimMatrix = new Matrix(); 192 | RectF videoRectF = new RectF(0, 0, mTextureView.getWidth(), mTextureView.getHeight()); 193 | if (mScale > 0 && mScale <= 1.0f) { // 缩小居中 194 | scaleEndAnimMatrix.postScale(mScale, mScale, videoRectF.right / 2, videoRectF.bottom / 2); 195 | startTransToAnimEnd(mScaleTransMatrix, scaleEndAnimMatrix); 196 | } else if (mScale > 1.0F) { // 放大,检测4边是否有在屏幕内部,有的话自动吸附到屏幕边缘 197 | RectF rectF = new RectF(0, 0, mTextureView.getWidth(), mTextureView.getHeight()); 198 | // 测量经过缩放位移变换后的播放画面位置 199 | mScaleTransMatrix.mapRect(rectF); 200 | float transAnimX = 0f; 201 | float transAnimY = 0f; 202 | scaleEndAnimMatrix.set(mScaleTransMatrix); 203 | if (rectF.left > videoRectF.left 204 | || rectF.right < videoRectF.right 205 | || rectF.top > videoRectF.top 206 | || rectF.bottom < videoRectF.bottom) { // 放大情况下,有一边缩放后在屏幕内部,自动吸附到屏幕边缘 207 | if (rectF.left > videoRectF.left) { // 左移吸边 208 | transAnimX = videoRectF.left - rectF.left; 209 | } else if (rectF.right < videoRectF.right) { // 右移吸边 210 | transAnimX = videoRectF.right - rectF.right; 211 | } 212 | if (rectF.top > videoRectF.top) { // 上移吸边 213 | transAnimY = videoRectF.top - rectF.top; 214 | } else if (rectF.bottom < videoRectF.bottom) { // 下移吸边 215 | transAnimY = videoRectF.bottom - rectF.bottom; 216 | } 217 | 218 | scaleEndAnimMatrix.postTranslate(transAnimX, transAnimY); 219 | startTransToAnimEnd(mScaleTransMatrix, scaleEndAnimMatrix); 220 | } 221 | } 222 | } 223 | 224 | private void startTransToAnimEnd(Matrix startMatrix, Matrix endMatrix) { 225 | LogUtil.d(TAG, "startTransToAnimEnd \nstart=" + startMatrix + "\nend=" + endMatrix); 226 | // 令 A = startMatrix;B = endMatrix 227 | // 方法1:直接将画面更新为结束矩阵位置B 228 | // updateMatrixToView(endMatrix); // 229 | // 方法2:将画面从现有位置A,移动到结束矩阵位置B,移动的距离T。B = T * A; 根据矩阵乘法的计算规则,反推出:T(x) = B(x) - A(x); T(y) = B(y) - A(y) 230 | // float[] startArray = new float[9]; 231 | // float[] endArray = new float[9]; 232 | // startMatrix.getValues(startArray); 233 | // endMatrix.getValues(endArray); 234 | // float transX = endArray[Matrix.MTRANS_X] - startArray[Matrix.MTRANS_X]; 235 | // float transY = endArray[Matrix.MTRANS_Y] - startArray[Matrix.MTRANS_Y]; 236 | // startMatrix.postTranslate(transX, transY); 237 | // LogUtil.d(TAG, "transToCenter1 \nstart=" + startMatrix + "\nend" + endMatrix); 238 | // updateMatrixToView(startMatrix); 239 | 240 | // 方法3:在方法2基础上,增加动画移动效果 241 | if (mScaleAnimator != null) { 242 | mScaleAnimator.cancel(); 243 | mScaleAnimator = null; 244 | } 245 | if (mScaleAnimator == null) { 246 | mScaleAnimator = new VideoScaleEndAnimator(startMatrix, endMatrix) { 247 | 248 | @Override 249 | protected void updateMatrixToView(Matrix transMatrix) { 250 | updateMatrixToTexture(transMatrix); 251 | } 252 | }; 253 | mScaleAnimator.start(); 254 | } 255 | 256 | mScaleTransMatrix = endMatrix; 257 | } 258 | 259 | public void showScaleReset() { 260 | if (isScaled() && mTouchAdapter != null && mTouchAdapter.isFullScreen()) { 261 | if (mScaleRestView != null && mScaleRestView.getVisibility() != View.VISIBLE) { 262 | mScaleRestView.setVisibility(View.VISIBLE); 263 | } 264 | } 265 | } 266 | 267 | public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 268 | // 缩放模式下,是否需要单手滚动 269 | // if (isScaled(mScale) && mScaleTransMatrix != null) { 270 | // TextureView mTextureView = mTouchAdapter.getTextureView(); 271 | // if (mTextureView != null) { 272 | // postTranslate(mScaleTransMatrix, -distanceX, -distanceY); 273 | // onScaleMatrixUpdate(mScaleTransMatrix); 274 | // Matrix matrix = new Matrix(mTextureView.getMatrix()); 275 | // matrix.set(mScaleTransMatrix); 276 | // mTextureView.setTransform(matrix); 277 | // return true; 278 | // } 279 | // } 280 | return false; 281 | } 282 | 283 | 284 | 285 | private void onScaleMatrixUpdate(Matrix matrix) { 286 | matrix.getValues(mMatrixValue); 287 | mScale = mMatrixValue[Matrix.MSCALE_X]; 288 | // 暂停下,实时更新缩放画面 289 | if (!mTouchAdapter.isPlaying()) { 290 | TextureView mTextureView = mTouchAdapter.getTextureView(); 291 | if (mTextureView != null) { 292 | mTextureView.invalidate(); 293 | } 294 | } 295 | } 296 | 297 | /** 298 | * 是否处于已缩放 or 缩放中 299 | * 300 | * @return 301 | */ 302 | public boolean isInScaleStatus() { 303 | return isScaled(mScale) || mIsScaleTouch; 304 | } 305 | 306 | public boolean isScaled() { 307 | return isScaled(mScale); 308 | } 309 | 310 | private boolean isScaled(float scale) { 311 | return scale > 0 && scale <= 0.99F || scale >= 1.01F; 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /app/src/main/java/cn/yinxm/media/video/gesture/touch/listener/IVideoGestureListener.java: -------------------------------------------------------------------------------- 1 | package cn.yinxm.media.video.gesture.touch.listener; 2 | 3 | import android.view.GestureDetector; 4 | import android.view.MotionEvent; 5 | 6 | public interface IVideoGestureListener extends GestureDetector.OnGestureListener, 7 | GestureDetector.OnDoubleTapListener { 8 | boolean onTouchEvent(MotionEvent event); 9 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/yinxm/media/video/gesture/touch/listener/VideoScaleGestureListener.java: -------------------------------------------------------------------------------- 1 | package cn.yinxm.media.video.gesture.touch.listener; 2 | 3 | import android.view.ScaleGestureDetector; 4 | 5 | import cn.yinxm.lib.utils.log.LogUtil; 6 | import cn.yinxm.media.video.gesture.touch.IGestureLayer; 7 | import cn.yinxm.media.video.gesture.touch.handler.VideoTouchScaleHandler; 8 | 9 | /** 10 | * 手势缩放 播放画面 11 | */ 12 | public class VideoScaleGestureListener implements ScaleGestureDetector.OnScaleGestureListener { 13 | private static final String TAG = "VideoScaleGestureListener"; 14 | private IGestureLayer mGestureLayer; 15 | public VideoTouchScaleHandler mScaleHandler; 16 | 17 | public VideoScaleGestureListener(IGestureLayer gestureLayer) { 18 | mGestureLayer = gestureLayer; 19 | } 20 | 21 | @Override 22 | public boolean onScale(ScaleGestureDetector detector) { 23 | if (mScaleHandler != null) { 24 | return mScaleHandler.onScale(detector); 25 | } 26 | return false; 27 | } 28 | 29 | @Override 30 | public boolean onScaleBegin(ScaleGestureDetector detector) { 31 | if (mScaleHandler != null) { 32 | boolean isConsume = mScaleHandler.onScaleBegin(detector); 33 | if (isConsume) { 34 | return true; 35 | } 36 | } 37 | return true; 38 | } 39 | 40 | @Override 41 | public void onScaleEnd(ScaleGestureDetector detector) { 42 | if (mScaleHandler != null) { 43 | mScaleHandler.onScaleEnd(detector); 44 | } 45 | 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/cn/yinxm/media/video/gesture/touch/ui/TouchScaleResetView.java: -------------------------------------------------------------------------------- 1 | package cn.yinxm.media.video.gesture.touch.ui; 2 | 3 | import android.content.Context; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | 8 | import cn.yinxm.media.video.R; 9 | 10 | /** 11 | *
12 | * 13 | * @author yinxuming 14 | * @date 2020/11/25 15 | */ 16 | public abstract class TouchScaleResetView implements View.OnClickListener { 17 | private Context mContext; 18 | private View mScaleResetContent; 19 | private View mScaleResetView; 20 | 21 | public TouchScaleResetView(Context context, ViewGroup container) { 22 | mContext = context; 23 | View view = LayoutInflater.from(mContext).inflate(R.layout.touch_scale_rest_view, container); 24 | mScaleResetContent = view.findViewById(R.id.view_scale_reset); 25 | mScaleResetView = view.findViewById(R.id.tv_scale_reset); 26 | mScaleResetView.setOnClickListener(this); 27 | } 28 | 29 | public void setVisibility(int visibility) { 30 | mScaleResetContent.setVisibility(visibility); 31 | } 32 | 33 | public int getVisibility() { 34 | return mScaleResetContent.getVisibility(); 35 | } 36 | 37 | @Override 38 | public void onClick(View v) { 39 | switch (v.getId()) { 40 | case R.id.tv_scale_reset: 41 | clickResetScale(); 42 | break; 43 | } 44 | } 45 | 46 | public abstract void clickResetScale(); 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/cn/yinxm/media/video/surface/SimpleTextureViewPlayer.java: -------------------------------------------------------------------------------- 1 | package cn.yinxm.media.video.surface; 2 | 3 | import android.content.Context; 4 | import android.graphics.SurfaceTexture; 5 | import android.media.MediaPlayer; 6 | import android.util.AttributeSet; 7 | import android.util.Log; 8 | import android.view.KeyEvent; 9 | import android.view.MotionEvent; 10 | import android.view.Surface; 11 | import android.view.TextureView; 12 | import android.view.ViewGroup; 13 | import android.widget.MediaController; 14 | 15 | import java.io.IOException; 16 | 17 | import cn.yinxm.lib.utils.log.LogUtil; 18 | import cn.yinxm.media.video.controller.VideoPlayController; 19 | 20 | /** 21 | * MediaPlayer + TextureView 播放视频 22 | *
23 | *
24 | * @author yinxuming
25 | * @date 2020/7/6
26 | */
27 | public class SimpleTextureViewPlayer extends TextureView implements TextureView.SurfaceTextureListener,
28 | VideoPlayController {
29 | private static final String TAG = "SimpleTextureViewPlayer";
30 |
31 | private MediaPlayer mMediaPlayer;
32 | SurfaceTexture mSurfaceTexture;
33 | Surface mSurface;
34 | String mPlayUrl;
35 | private int mVideoWidth, mVideoHeight;
36 | private MediaController mMediaController;
37 |
38 |
39 | private MediaPlayer.OnPreparedListener mOnPreparedListener;
40 | private MediaPlayer.OnCompletionListener mOnCompletionListener;
41 | private MediaPlayer.OnVideoSizeChangedListener mOnVideoSizeChangedListener;
42 |
43 |
44 | public SimpleTextureViewPlayer(Context context) {
45 | this(context, null);
46 | }
47 |
48 | public SimpleTextureViewPlayer(Context context, AttributeSet attrs) {
49 | this(context, attrs, 0);
50 | }
51 |
52 | public SimpleTextureViewPlayer(Context context, AttributeSet attrs, int defStyleAttr) {
53 | super(context, attrs, defStyleAttr);
54 | init(context);
55 | }
56 |
57 | private void init(Context context) {
58 | setSurfaceTextureListener(this);
59 | }
60 |
61 | private void initMedia() {
62 |
63 | if (mMediaPlayer != null) {
64 | try {
65 | mMediaPlayer.release();
66 | } catch (Exception e) {
67 | e.printStackTrace();
68 | }
69 | mMediaPlayer = null;
70 | }
71 |
72 | mMediaPlayer = new MediaPlayer();
73 | updateSurface(mSurface);
74 | mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
75 | @Override
76 | public void onPrepared(MediaPlayer mp) {
77 | // if (getVisibility() != VISIBLE) {
78 | // setVisibility(View.VISIBLE);
79 | //// mTextureView.requestLayout(mp.getVideoWidth(), mp.getVideoHeight());
80 | // }
81 | // mp.start();
82 |
83 | if (mOnPreparedListener != null) {
84 | mOnPreparedListener.onPrepared(mp);
85 | }
86 | }
87 | });
88 | mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
89 | @Override
90 | public boolean onError(MediaPlayer mp, int what, int extra) {
91 | Log.e(TAG, "onError " + mp + ", what=" + what + ", " + extra);
92 |
93 | return true;
94 | }
95 | });
96 |
97 | mMediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
98 | @Override
99 | public void onBufferingUpdate(MediaPlayer mp, int percent) {
100 | //此方法获取的是缓冲的状态
101 | Log.e(TAG, "缓冲中:" + percent);
102 | }
103 | });
104 |
105 | //播放完成的监听
106 | mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
107 | @Override
108 | public void onCompletion(MediaPlayer mp) {
109 | Log.d(TAG, "onCompletion " + mp);
110 | // mState = VideoState.init;
111 | if (mOnCompletionListener != null) {
112 | mOnCompletionListener.onCompletion(mp);
113 | }
114 | }
115 | });
116 |
117 | mMediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
118 | @Override
119 | public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
120 | LogUtil.d(TAG, "onVideoSizeChanged w=" + width + ", h=" + height);
121 | mVideoWidth = mp.getVideoWidth();
122 | mVideoHeight = mp.getVideoHeight();
123 | if (mOnVideoSizeChangedListener != null) {
124 | mOnVideoSizeChangedListener.onVideoSizeChanged(mp, width, height);
125 | }/* else {
126 | updateVideoSize(mVideoWidth, mVideoHeight);
127 | }*/
128 | }
129 | });
130 | }
131 |
132 | @Override
133 | public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
134 | Log.e(TAG, "onSurfaceTextureAvailable surface=" + surface + ", w=" + width + ", h=" + height);
135 | mSurfaceTexture = surface;
136 | mSurface = new Surface(mSurfaceTexture);
137 | updateSurface(mSurface);
138 | }
139 |
140 | @Override
141 | public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
142 | Log.e(TAG, "onSurfaceTextureSizeChanged surface=" + surface + ", w=" + width + ", h=" + height);
143 |
144 | }
145 |
146 | @Override
147 | public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
148 | Log.e(TAG, "onSurfaceTextureDestroyed surface=" + surface);
149 | updateSurface(null);
150 | return false;
151 | }
152 |
153 | @Override
154 | public void onSurfaceTextureUpdated(SurfaceTexture surface) {
155 | Log.d(TAG, "onSurfaceTextureUpdated surface=" + surface); // 会不断回调
156 |
157 | }
158 |
159 | private void updateSurface(Surface surface) {
160 | if (mMediaPlayer == null) {
161 | return;
162 | }
163 | mMediaPlayer.setSurface(surface);
164 | }
165 |
166 | @Override
167 | public void start() {
168 | if (isPlayerReady()) {
169 | mMediaPlayer.start();
170 | }
171 | }
172 |
173 | @Override
174 | public void pause() {
175 | if (isPlaying()) {
176 | mMediaPlayer.pause();
177 | }
178 | }
179 |
180 | @Override
181 | public int getDuration() {
182 | return isPlayerReady() ? mMediaPlayer.getDuration() : 0;
183 | }
184 |
185 | @Override
186 | public int getCurrentPosition() {
187 | return isPlayerReady() ? mMediaPlayer.getCurrentPosition() : 0;
188 | }
189 |
190 | @Override
191 | public void seekTo(int pos) {
192 | if (isPlayerReady()) {
193 | mMediaPlayer.seekTo(pos);
194 | }
195 | }
196 |
197 | @Override
198 | public boolean isPlaying() {
199 | return isPlayerReady() && mMediaPlayer.isPlaying();
200 | }
201 |
202 | @Override
203 | public int getBufferPercentage() {
204 | return 0;
205 | }
206 |
207 | @Override
208 | public boolean canPause() {
209 | return true;
210 | }
211 |
212 | @Override
213 | public boolean canSeekBackward() {
214 | return true;
215 | }
216 |
217 | @Override
218 | public boolean canSeekForward() {
219 | return true;
220 | }
221 |
222 | @Override
223 | public int getAudioSessionId() {
224 | return 0;
225 | }
226 |
227 | // public void bindTextureView(HkTextureView textureView) {
228 | // mTextureView = textureView;
229 | // mTextureView.setSurfaceTextureListener(this);
230 | // }
231 |
232 | public void startPlay(String url) {
233 | mPlayUrl = url;
234 | try {
235 | initMedia();
236 | mMediaPlayer.setDataSource(mPlayUrl);
237 | } catch (IOException e) {
238 | e.printStackTrace();
239 | }
240 | mMediaPlayer.prepareAsync();
241 | updateSurface(mSurface);
242 | }
243 |
244 | public String getPlayUrl() {
245 | return mPlayUrl;
246 | }
247 |
248 | public void setOnCompletionListener(MediaPlayer.OnCompletionListener onCompletionListener) {
249 | mOnCompletionListener = onCompletionListener;
250 | }
251 |
252 | public void setOnPreparedListener(MediaPlayer.OnPreparedListener onPreparedListener) {
253 | mOnPreparedListener = onPreparedListener;
254 | }
255 |
256 | public void setOnVideoSizeChangedListener(MediaPlayer.OnVideoSizeChangedListener onVideoSizeChangedListener) {
257 | mOnVideoSizeChangedListener = onVideoSizeChangedListener;
258 | }
259 |
260 | /**
261 | * 缩放画面尺寸:根据视频匡高比例,以及显示区域匡高比例,取最小比例值,缩放
262 | *
263 | * @param width
264 | * @param height
265 | */
266 | public void updateVideoSize(int width, int height) {
267 | if (width <= 0 || height <= 0) {
268 | return;
269 | }
270 | int surWidth = getWidth();
271 | int surHeight = getHeight();
272 | if (surHeight <= 0 || surHeight <= 0) {
273 | return;
274 | }
275 | LogUtil.d(TAG, "video:(" + width + ", " + height + "), view:(" + surWidth + ", " + surHeight + ")");
276 | // 等比例缩放
277 | int wSca = surWidth / width;
278 | int hSca = surHeight / height;
279 |
280 | int scale = Math.min(wSca, hSca);
281 | ViewGroup.LayoutParams params = getLayoutParams();
282 | params.width = scale * width;
283 | params.height = scale * height;
284 | setLayoutParams(params);
285 | }
286 |
287 |
288 | // 按键相关
289 | public void setMediaController(MediaController controller) {
290 | mMediaController = controller;
291 | }
292 |
293 | @Override
294 | public boolean onKeyDown(int keyCode, KeyEvent event) {
295 | boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK &&
296 | keyCode != KeyEvent.KEYCODE_VOLUME_UP &&
297 | keyCode != KeyEvent.KEYCODE_VOLUME_DOWN &&
298 | keyCode != KeyEvent.KEYCODE_VOLUME_MUTE &&
299 | keyCode != KeyEvent.KEYCODE_MENU &&
300 | keyCode != KeyEvent.KEYCODE_CALL &&
301 | keyCode != KeyEvent.KEYCODE_ENDCALL;
302 | if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) {
303 | if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK ||
304 | keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
305 | if (mMediaPlayer.isPlaying()) {
306 | pause();
307 | mMediaController.show();
308 | } else {
309 | start();
310 | mMediaController.hide();
311 | }
312 | return true;
313 | } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
314 | if (!mMediaPlayer.isPlaying()) {
315 | start();
316 | mMediaController.hide();
317 | }
318 | return true;
319 | } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
320 | || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
321 | if (mMediaPlayer.isPlaying()) {
322 | pause();
323 | mMediaController.show();
324 | }
325 | return true;
326 | } else {
327 | toggleMediaControlsVisiblity();
328 | }
329 | }
330 |
331 | return super.onKeyDown(keyCode, event);
332 | }
333 |
334 | @Override
335 | public boolean onTrackballEvent(MotionEvent ev) {
336 | if (ev.getAction() == MotionEvent.ACTION_DOWN
337 | && isInPlaybackState() && mMediaController != null) {
338 | toggleMediaControlsVisiblity();
339 | }
340 | return super.onTrackballEvent(ev);
341 | }
342 |
343 | @Override
344 | public boolean onTouchEvent(MotionEvent ev) {
345 | if (ev.getAction() == MotionEvent.ACTION_DOWN
346 | && isInPlaybackState() && mMediaController != null) {
347 | toggleMediaControlsVisiblity();
348 | }
349 | return super.onTouchEvent(ev);
350 | }
351 |
352 | private boolean isInPlaybackState() {
353 | return true;
354 | }
355 |
356 | private void toggleMediaControlsVisiblity() {
357 | if (mMediaController.isShowing()) {
358 | mMediaController.hide();
359 | } else {
360 | mMediaController.show();
361 | }
362 | }
363 |
364 | private boolean isPlayerReady() {
365 | return mMediaPlayer != null;
366 | }
367 | }
368 |
--------------------------------------------------------------------------------
/app/src/main/java/cn/yinxm/media/video/util/VideoUrlTest.java:
--------------------------------------------------------------------------------
1 | package cn.yinxm.media.video.util;
2 |
3 | import android.text.TextUtils;
4 |
5 | /**
6 | *
7 | */
8 | public class VideoUrlTest {
9 | private static String[] URLS = new String[]{
10 | "https://vd2.bdstatic.com/mda-kfnpxpc86wm7xa9t/v2-patchcae/mda-kfnpxpc86wm7xa9t.mp4",
11 | "https://video19.ifeng.com/video06/2012/04/11/629da9ec-60d4-4814-a940-997e6487804a.mp4"
12 | };
13 |
14 | public static String getPlayUrl() {
15 | return getPlayUrl(0);
16 | }
17 |
18 | public static String getPlayUrl(int index) {
19 | return URLS[index];
20 | }
21 |
22 | public static String getNextUrl(String playUrl) {
23 | if (!TextUtils.isEmpty(playUrl)) {
24 | for (int i = 0; i < URLS.length; i++) {
25 | if (playUrl.equals(URLS[i])) {
26 | return getPlayUrl((i + 1) % URLS.length);
27 | }
28 | }
29 | }
30 | return getPlayUrl(0);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |