├── .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 | ![avatar](/doc/scale.gif) 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 | 4 | 5 | 6 | 7 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/cn/yinxm/media/video/ScaleVideoActivity.java: -------------------------------------------------------------------------------- 1 | package cn.yinxm.media.video; 2 | 3 | import android.media.MediaPlayer; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.ImageButton; 8 | 9 | import androidx.fragment.app.FragmentActivity; 10 | 11 | 12 | import cn.yinxm.lib.screen.StatusBarUtils; 13 | import cn.yinxm.media.video.gesture.GestureLayer; 14 | import cn.yinxm.media.video.gesture.touch.adapter.GestureVideoTouchAdapterImpl; 15 | import cn.yinxm.media.video.surface.SimpleTextureViewPlayer; 16 | 17 | import static cn.yinxm.media.video.util.VideoUrlTest.getPlayUrl; 18 | 19 | 20 | public class ScaleVideoActivity extends FragmentActivity { 21 | private ViewGroup mVideoContent; 22 | public SimpleTextureViewPlayer mTextureViewPlayer; 23 | private ImageButton mPlayPauseView; 24 | private boolean isPaused = false; 25 | 26 | @Override 27 | protected void onCreate(Bundle savedInstanceState) { 28 | super.onCreate(savedInstanceState); 29 | StatusBarUtils.fullScreen(getWindow()); 30 | setContentView(R.layout.activity_scale_video); 31 | mVideoContent = findViewById(R.id.video_content); 32 | mTextureViewPlayer = findViewById(R.id.texture_player); 33 | mPlayPauseView = findViewById(R.id.btn_play_pause); 34 | initPlayer(); 35 | initGesture(mVideoContent); 36 | initData(); 37 | } 38 | 39 | private void initData() { 40 | mPlayPauseView.setOnClickListener(new View.OnClickListener() { 41 | @Override 42 | public void onClick(View v) { 43 | if (mTextureViewPlayer.isPlaying()) { 44 | mTextureViewPlayer.pause(); 45 | changePlayBtnStyle(false); 46 | } else if (isPaused) { 47 | mTextureViewPlayer.start(); 48 | isPaused = false; 49 | changePlayBtnStyle(true); 50 | } else { 51 | mTextureViewPlayer.startPlay(getPlayUrl()); 52 | changePlayBtnStyle(true); 53 | } 54 | } 55 | }); 56 | // mTextureViewPlayer.startPlay(getPlayUrl()); 57 | } 58 | 59 | 60 | private void changePlayBtnStyle(boolean isPlaying) { 61 | if (isPlaying) { 62 | mPlayPauseView.setImageResource(R.drawable.small_new_pip_pause); 63 | } else { 64 | mPlayPauseView.setImageResource(R.drawable.small_new_pip_play); 65 | } 66 | } 67 | 68 | private void initGesture(ViewGroup videoContent) { 69 | GestureLayer gestureLayer = new GestureLayer(this, 70 | new GestureVideoTouchAdapterImpl(mTextureViewPlayer) { 71 | @Override 72 | public boolean isFullScreen() { 73 | return true; 74 | } 75 | }); 76 | videoContent.addView(gestureLayer.getContainer()); 77 | } 78 | 79 | private void initPlayer() { 80 | mTextureViewPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { 81 | @Override 82 | public void onPrepared(MediaPlayer mp) { 83 | mp.start(); 84 | } 85 | }); 86 | mTextureViewPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() { 87 | @Override 88 | public void onVideoSizeChanged(MediaPlayer mp, int width, int height) { 89 | 90 | } 91 | }); 92 | mTextureViewPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { 93 | @Override 94 | public void onCompletion(MediaPlayer mp) { 95 | String newUrl = getPlayUrl(0); 96 | if (mTextureViewPlayer.getPlayUrl().equals(getPlayUrl(0))) { 97 | newUrl = getPlayUrl(1); 98 | } 99 | mTextureViewPlayer.startPlay(newUrl); 100 | } 101 | }); 102 | } 103 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/yinxm/media/video/controller/SimpleVideoController.java: -------------------------------------------------------------------------------- 1 | package cn.yinxm.media.video.controller; 2 | 3 | /** 4 | *

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 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/small_new_pip_pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinxuming/VideoTouchScale/9d2cf34c657e6122ed510042be121ad19f7701ef/app/src/main/res/drawable/small_new_pip_pause.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/small_new_pip_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinxuming/VideoTouchScale/9d2cf34c657e6122ed510042be121ad19f7701ef/app/src/main/res/drawable/small_new_pip_play.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/touch_scale_rest_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_scale_video.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/layout/touch_scale_rest_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinxuming/VideoTouchScale/9d2cf34c657e6122ed510042be121ad19f7701ef/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinxuming/VideoTouchScale/9d2cf34c657e6122ed510042be121ad19f7701ef/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinxuming/VideoTouchScale/9d2cf34c657e6122ed510042be121ad19f7701ef/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinxuming/VideoTouchScale/9d2cf34c657e6122ed510042be121ad19f7701ef/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinxuming/VideoTouchScale/9d2cf34c657e6122ed510042be121ad19f7701ef/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinxuming/VideoTouchScale/9d2cf34c657e6122ed510042be121ad19f7701ef/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinxuming/VideoTouchScale/9d2cf34c657e6122ed510042be121ad19f7701ef/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinxuming/VideoTouchScale/9d2cf34c657e6122ed510042be121ad19f7701ef/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinxuming/VideoTouchScale/9d2cf34c657e6122ed510042be121ad19f7701ef/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinxuming/VideoTouchScale/9d2cf34c657e6122ed510042be121ad19f7701ef/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | VideoTouchScale 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath "com.android.tools.build:gradle:4.1.0" 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | maven {url 'https://jitpack.io/'} 20 | } 21 | } 22 | 23 | task clean(type: Delete) { 24 | delete rootProject.buildDir 25 | } 26 | 27 | ext { 28 | compileSdkVersion = 29 29 | buildToolsVersion = "29.0.3" 30 | 31 | minSdkVersion = 16 32 | targetSdkVersion = 28 33 | 34 | androidx_appcompat = '1.2.0' 35 | androidx_constraintlayout = '2.0.4' 36 | Lib_Android = '0.0.5' 37 | } -------------------------------------------------------------------------------- /doc/scale.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinxuming/VideoTouchScale/9d2cf34c657e6122ed510042be121ad19f7701ef/doc/scale.gif -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinxuming/VideoTouchScale/9d2cf34c657e6122ed510042be121ad19f7701ef/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Dec 02 17:21:47 CST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | rootProject.name = "VideoTouchScale" --------------------------------------------------------------------------------