├── app
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── values
│ │ │ ├── themes.xml
│ │ │ ├── strings.xml
│ │ │ └── colors.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
│ │ ├── drawable
│ │ │ ├── small_new_pip_play.png
│ │ │ ├── small_new_pip_pause.png
│ │ │ ├── touch_scale_rest_background.xml
│ │ │ └── ic_launcher_background.xml
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── layout
│ │ │ ├── touch_scale_rest_view.xml
│ │ │ └── activity_scale_video.xml
│ │ └── drawable-v24
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── java
│ │ └── cn
│ │ │ └── yinxm
│ │ │ └── media
│ │ │ └── video
│ │ │ ├── gesture
│ │ │ ├── touch
│ │ │ │ ├── handler
│ │ │ │ │ ├── IVideoTouchHandler.java
│ │ │ │ │ └── VideoTouchScaleHandler.java
│ │ │ │ ├── listener
│ │ │ │ │ ├── IVideoGestureListener.java
│ │ │ │ │ └── VideoScaleGestureListener.java
│ │ │ │ ├── adapter
│ │ │ │ │ ├── IVideoTouchAdapter.java
│ │ │ │ │ └── GestureVideoTouchAdapterImpl.java
│ │ │ │ ├── IGestureLayer.java
│ │ │ │ ├── ui
│ │ │ │ │ └── TouchScaleResetView.java
│ │ │ │ └── anim
│ │ │ │ │ └── VideoScaleEndAnimator.java
│ │ │ └── GestureLayer.java
│ │ │ ├── controller
│ │ │ ├── VideoPlayController.java
│ │ │ └── SimpleVideoController.java
│ │ │ ├── util
│ │ │ └── VideoUrlTest.java
│ │ │ ├── ScaleVideoActivity.java
│ │ │ └── surface
│ │ │ └── SimpleTextureViewPlayer.java
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── doc
└── scale.gif
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── gradle.properties
├── gradlew.bat
├── gradlew
└── README.md
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | rootProject.name = "VideoTouchScale"
--------------------------------------------------------------------------------
/doc/scale.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yinxuming/VideoTouchScale/HEAD/doc/scale.gif
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
5 | *
6 | * @author yinxuming
7 | * @date 2020/5/19
8 | */
9 | public interface IVideoTouchHandler {
10 | }
11 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
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/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/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
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/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/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 |
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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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/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/res/layout/activity_scale_video.xml:
--------------------------------------------------------------------------------
1 |
2 |
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/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/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
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/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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------