[toc]java
文章索引git
View.getMatrix()
获取当前播放画面的Matrix,进行矩阵变换:缩放、平移,改变画面位置和大小,实现播放画面缩放功能。mScaleTransMatrix
,计算动画结束应该移动的位scaleEndAnimMatrix
,进行属性动画从mScaleTransMatrix
变化为scaleEndAnimMatrix
。View.onTouchEvent
。分别监听手指按下(MotionEvent.ACTION_POINTER_DOWN
)、抬起(MotionEvent.ACTION_POINTER_UP
)、移动(MotionEvent.ACTION_MOVE
)ScaleGestureDetector
。直接使用手势缩放检测ScaleGestureDetector
对View#onTouchEvent中的手势变化进行识别,经过ScaleGestureDetector.OnScaleGestureListener
获得onScaleBegin-onScale-onScale ... -onScaleEnd的缩放回调,在回调中处理响应的缩放逻辑。public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction() & MotionEvent.ACTION_MASK;
switch (action) {
case MotionEvent.ACTION_POINTER_DOWN:
onScaleBegin(event);
break;
case MotionEvent.ACTION_POINTER_UP:
onScaleEnd(event);
break;
case MotionEvent.ACTION_MOVE:
onScale(event);
break;
case MotionEvent.ACTION_CANCEL:
cancelScale(event);
break;
}
return true;
}
复制代码
使用ScaleGestureDetector
来识别onTouchEvent中的手势触摸操做,获得onScaleBegin
、onScale
、onScaleEnd
三种回调,在回调里面经过VideoTouchScaleHandler
对视频进行缩放、平移操做。github
添加手势触摸层GestureLayer
,使用ScaleGestureDetector
识别手势缓存
/** * 手势处理layer层 */
public final class GestureLayer implements IGestureLayer, GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener {
private static final String TAG = "GestureLayer";
private Context mContext;
private FrameLayout mContainer;
/** 手势检测 */
private GestureDetector mGestureDetector;
/** 手势缩放 检测 */
private ScaleGestureDetector mScaleGestureDetector;
/** 手势缩放 监听 */
private VideoScaleGestureListener mScaleGestureListener;
/** 手势缩放 处理 */
private VideoTouchScaleHandler mScaleHandler;
private IVideoTouchAdapter mVideoTouchAdapter;
public GestureLayer(Context context, IVideoTouchAdapter videoTouchAdapter) {
mContext = context;
mVideoTouchAdapter = videoTouchAdapter;
initContainer();
initTouchHandler();
}
private void initContainer() {
mContainer = new FrameLayout(mContext) {
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean isConsume = onGestureTouchEvent(event);
if (isConsume) {
return true;
} else {
return super.onTouchEvent(event);
}
}
};
}
public void initTouchHandler() {
mGestureDetector = new GestureDetector(mContext, this);
mGestureDetector.setOnDoubleTapListener(this);
// 手势缩放
mScaleGestureListener = new VideoScaleGestureListener(this);
mScaleGestureDetector = new ScaleGestureDetector(getContext(), mScaleGestureListener);
// 缩放 处理
mScaleHandler = new VideoTouchScaleHandler(getContext(), mContainer, mVideoTouchAdapter);
mScaleGestureListener.mScaleHandler = mScaleHandler;
}
@Override
public void onLayerRelease() {
if (mGestureDetector != null) {
mGestureDetector.setOnDoubleTapListener(null);
}
}
@Override
public boolean onGestureTouchEvent(MotionEvent event) {
try {
int pointCount = event.getPointerCount();
if (pointCount == 1 && event.getAction() == MotionEvent.ACTION_UP) {
if (mScaleHandler.isScaled()) {
mScaleHandler.showScaleReset();
}
}
if (pointCount > 1) {
boolean isConsume = mScaleGestureDetector.onTouchEvent(event);
if (isConsume) {
return true;
}
}
} catch (Exception e) {
Log.e(TAG, "", e);
}
if (event.getAction() == MotionEvent.ACTION_DOWN) {
return true;
}
return false;
}
...
}
复制代码
ScaleGestureDetector.OnScaleGestureListener 手势缩放回调处理markdown
/** * 手势缩放 播放画面 */
public class VideoScaleGestureListener implements ScaleGestureDetector.OnScaleGestureListener {
private static final String TAG = "VideoScaleGestureListener";
private IGestureLayer mGestureLayer;
public VideoTouchScaleHandler mScaleHandler;
public VideoScaleGestureListener(IGestureLayer gestureLayer) {
mGestureLayer = gestureLayer;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
if (mScaleHandler != null) {
return mScaleHandler.onScale(detector);
}
return false;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
if (mScaleHandler != null) {
boolean isConsume = mScaleHandler.onScaleBegin(detector);
if (isConsume) {
return true;
}
}
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
if (mScaleHandler != null) {
mScaleHandler.onScaleEnd(detector);
}
}
}
复制代码
Matrix.postScale(float sx, float sy, float px, float py)
,这里有几个参数,前两个指定x,y轴上的缩放倍数,后两个指定缩放中心点位置。
currentDiffScale = detector.getCurrentSpan() / mLastSpan
onScaleBegin
时,scaleCenterX = detector.getFocusX(); scaleCenterY = detector.getFocusY();
M.postTranslate(tx, ty); // 等价 M' = T * M
M.preTranslate(tx, ty); // 等价 M' = M * T
复制代码
`,dx和dy表示相对当前的Matrix的位置须要移动的距离,注意必定是相对于当前的Matrix位置,而不是相对onScaleBegin时的Matrix初始位置。ide
本次移动距离 = 本次中心点 - 上次中心点
```java
dx = detector.getFocusX() - mLastCenterX
dy = detector.getFocusY() - mLastCenterY
```
复制代码
默认不处理,暂停画面状况下,Matrix变换后,更新到TextureView上,画面是不会发生变化的,要想画面实时更新,调用TextureView.invalidate()
便可。oop
缩放结束后(onScaleEnd),为了加强交互体验,须要根据缩放的大小、位置,从新调整画面,动画移动到指定位置。指定位置主要有居中和吸附屏幕边缘两种。 动画的移动,主要采用属性动画ValueAnimator
.post
缩放结束后,画面若是处于缩小模式,须要将画面移动到屏幕中央。动画
mScaleTransMatrix
,这也是动画的起始值,如今要推导动画的结束位置矩阵scaleEndAnimMatrix
,要求在屏幕中居中,若是要直接用mScaleTransMatrix
进行变换获得动画结束矩阵, 须要在xy上平移必定距离,可是该距离具体指并很差计算。 这里咱们从另外一个方向下手,知道当前的缩放倍速mScale
,视频TextureView占的区域,那么直接以该区域中心点进行矩阵缩放变化,就能够获得中心位置矩阵scaleEndAnimMatrix
RectF videoRectF = new RectF(0, 0, mTextureView.getWidth(), mTextureView.getHeight());
if (mScale > 0 && mScale <= 1.0f) { // 缩小居中
scaleEndAnimMatrix.reset();
scaleEndAnimMatrix.postScale(mScale, mScale, videoRectF.right / 2, videoRectF.bottom / 2);
}
复制代码
mScaleTransMatrix
;scaleEndAnimMatrix
;mScaleTransMatrix
动画移动到scaleEndAnimMatrix
位置时,中间的矩阵无非就是在x、y上位移了必定距离。以x轴为例:
缩放结束后,若是画面处于放大,且有画面边缘在屏幕内的,须要自动吸附到屏幕边缘。this
如何判断是否有画面边缘在屏幕内部? 须要考虑四边:left、top、right、bottom位置的状况。若是要考虑画面在屏幕内部的总状况数,比较繁琐和复杂,好比以left为例:有3种状况:
总共有8种状况,那有没有简单的方法? 有的,实际上,无论哪一种状况,咱们只须要关注画面的x、y方向须要移动的距离便可。问题简化为求画面在x、y轴上移动的距离:transAnimX
、transAnimY
只要知道上述两个值,将当前画面位移进行位移,便可获得动画结束位置矩阵scaleEndAnimMatrix
。
scaleEndAnimMatrix.set(mScaleTransMatrix);
scaleEndAnimMatrix.postTranslate(transAnimX, transAnimY);
复制代码
如何计算画面在屏幕内部须要移动到各屏幕边缘的距离transAnimX
、transAnimY
? 要解决这个问题,须要知道屏幕位置,播放画面位置。 屏幕的位置很好办,实际上就是画面原始大小位置:RectF videoRectF = new RectF(0, 0, mTextureView.getWidth(), mTextureView.getHeight());
当前缩放移动后画面的位置呢? 它对应的矩阵变化是mScaleTransMatrix
,那能不能根据这个矩阵推导出当前画面的位置? 能够的,咱们去找Matrix对外提供的接口,会发现有一个Matrix.mapRect(RectF)
方法,这个方法就是用来测量矩形区域通过矩阵变化后,新的矩形区域所在位置。直接上代码:
if (mScale > 1.0F) { // 放大,检测4边是否有在屏幕内部,有的话自动吸附到屏幕边缘
RectF rectF = new RectF(0, 0, mTextureView.getWidth(), mTextureView.getHeight());
mScaleTransMatrix.mapRect(rectF);
float transAnimX = 0f;
float transAnimY = 0f;
scaleEndAnimMatrix.set(mScaleTransMatrix);
if (rectF.left > videoRectF.left
|| rectF.right < videoRectF.right
|| rectF.top > videoRectF.top
|| rectF.bottom < videoRectF.bottom) { // 放大状况下,有一边缩放后在屏幕内部,自动吸附到屏幕边缘
if (rectF.left > videoRectF.left) { // 左移吸边
transAnimX = videoRectF.left - rectF.left;
} else if (rectF.right < videoRectF.right) { // 右移吸边
transAnimX = videoRectF.right - rectF.right;
}
// 注意这里的处理方式:分别处理x轴位移和y轴位移便可所有覆盖上述8种状况
if (rectF.top > videoRectF.top) { // 上移吸边
transAnimY = videoRectF.top - rectF.top;
} else if (rectF.bottom < videoRectF.bottom) { // 下移吸边
transAnimY = videoRectF.bottom - rectF.bottom;
}
// 计算移动到屏幕边缘位置后的矩阵
scaleEndAnimMatrix.postTranslate(transAnimX, transAnimY);
}
复制代码
/** * 播放器画面双指手势缩放处理: * <p> * 1. 双指缩放 * 2. 双指平移 * 3. 缩放结束后,若为缩小画面,居中动效 * 4. 缩放结束后,若为放大画面,自动吸附屏幕边缘动效 * 5. 暂停播放下,实时更新缩放画面 * * @author yinxuming * @date 2020/12/2 */
public class VideoTouchScaleHandler implements IVideoTouchHandler, ScaleGestureDetector.OnScaleGestureListener {
private static final String TAG = "VideoTouchScaleHandler";
private Context mContext;
public FrameLayout mContainer;
private boolean openScaleTouch = true; // 开启缩放
private boolean mIsScaleTouch;
private Matrix mScaleTransMatrix; // 缓存了上次的矩阵值,因此须要计算每次变化量
private float mStartCenterX, mStartCenterY, mLastCenterX, mLastCenterY, centerX, centerY;
private float mStartSpan, mLastSpan, mCurrentSpan;
private float mScale;
private float[] mMatrixValue = new float[9];
private float mMinScale = 0.1F, mMaxScale = 3F;
private VideoScaleEndAnimator mScaleAnimator;
IVideoTouchAdapter mTouchAdapter;
TouchScaleResetView mScaleRestView;
public VideoTouchScaleHandler(Context context, FrameLayout container, IVideoTouchAdapter videoTouchAdapter) {
mContext = context;
mContainer = container;
mTouchAdapter = videoTouchAdapter;
initView();
}
private void initView() {
mScaleRestView = new TouchScaleResetView(mContext, mContainer) {
@Override
public void clickResetScale() {
mScaleRestView.setVisibility(View.GONE);
if (isScaled()) {
cancelScale();
}
}
};
}
private Context getContext() {
return mContext;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
TextureView mTextureView = mTouchAdapter.getTextureView();
if (mTextureView != null) {
mIsScaleTouch = true;
if (mScaleTransMatrix == null) {
mScaleTransMatrix = new Matrix(mTextureView.getMatrix());
onScaleMatrixUpdate(mScaleTransMatrix);
}
}
mStartCenterX = detector.getFocusX();
mStartCenterY = detector.getFocusY();
mStartSpan = detector.getCurrentSpan();
mLastCenterX = mStartCenterX;
mLastCenterY = mStartCenterY;
mLastSpan = mStartSpan;
return true;
}
private void updateMatrixToTexture(Matrix newMatrix) {
TextureView mTextureView = mTouchAdapter.getTextureView();
if (mTextureView != null) {
mTextureView.setTransform(newMatrix);
}
onScaleMatrixUpdate(newMatrix);
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
if (mIsScaleTouch && openScaleTouch) {
mCurrentSpan = detector.getCurrentSpan();
centerX = detector.getFocusX();
centerY = detector.getFocusY();
if (processOnScale(detector)) {
mLastCenterX = centerX;
mLastCenterY = centerY;
mLastSpan = mCurrentSpan;
}
}
return false;
}
private boolean processOnScale(ScaleGestureDetector detector) {
float diffScale = mCurrentSpan / mLastSpan;
if (mTouchAdapter.isFullScreen()) {
if (mScaleTransMatrix != null) {
postScale(mScaleTransMatrix, diffScale, mStartCenterX, mStartCenterY);
mScaleTransMatrix.postTranslate(detector.getFocusX() - mLastCenterX,
detector.getFocusY() - mLastCenterY);
onScaleMatrixUpdate(mScaleTransMatrix);
TextureView mTextureView = mTouchAdapter.getTextureView();
if (mTextureView != null) {
Matrix matrix = new Matrix(mTextureView.getMatrix());
matrix.set(mScaleTransMatrix);
mTextureView.setTransform(matrix);
}
int scaleRatio = (int) (mScale * 100);
Toast.makeText(getContext(), "" + scaleRatio + "%", Toast.LENGTH_SHORT).show();
return true;
}
}
return false;
}
private void postScale(Matrix matrix, float scale, float x, float y) {
matrix.getValues(mMatrixValue);
float curScale = mMatrixValue[Matrix.MSCALE_X];
if (scale < 1 && Math.abs(curScale - mMinScale) < 0.001F) {
scale = 1;
} else if (scale > 1 && Math.abs(curScale - mMaxScale) < 0.001F) {
scale = 1;
} else {
curScale *= scale;
if (scale < 1 && curScale < mMinScale) {
curScale = mMinScale;
scale = curScale / mMatrixValue[Matrix.MSCALE_X];
} else if (scale > 1 && curScale > mMaxScale) {
curScale = mMaxScale;
scale = curScale / mMatrixValue[Matrix.MSCALE_X];
}
matrix.postScale(scale, scale, x, y);
}
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
if (mIsScaleTouch) { // 取消多手势操做
mIsScaleTouch = false;
doScaleEndAnim();
}
}
public void cancelScale() {
TextureView mTextureView = mTouchAdapter.getTextureView();
if (mScaleTransMatrix != null && mTextureView != null) {
mIsScaleTouch = false;
mScaleTransMatrix.reset();
onScaleMatrixUpdate(mScaleTransMatrix);
Matrix matrix = new Matrix(mTextureView.getMatrix());
matrix.reset();
mTextureView.setTransform(matrix);
}
}
/** * 计算缩放结束后动画位置:scaleEndAnimMatrix */
private void doScaleEndAnim() {
TextureView mTextureView = mTouchAdapter.getTextureView();
if (mTextureView == null) {
return;
}
Matrix scaleEndAnimMatrix = new Matrix();
RectF videoRectF = new RectF(0, 0, mTextureView.getWidth(), mTextureView.getHeight());
if (mScale > 0 && mScale <= 1.0f) { // 缩小居中
scaleEndAnimMatrix.postScale(mScale, mScale, videoRectF.right / 2, videoRectF.bottom / 2);
startTransToAnimEnd(mScaleTransMatrix, scaleEndAnimMatrix);
} else if (mScale > 1.0F) { // 放大,检测4边是否有在屏幕内部,有的话自动吸附到屏幕边缘
RectF rectF = new RectF(0, 0, mTextureView.getWidth(), mTextureView.getHeight());
// 测量通过缩放位移变换后的播放画面位置
mScaleTransMatrix.mapRect(rectF);
float transAnimX = 0f;
float transAnimY = 0f;
scaleEndAnimMatrix.set(mScaleTransMatrix);
if (rectF.left > videoRectF.left
|| rectF.right < videoRectF.right
|| rectF.top > videoRectF.top
|| rectF.bottom < videoRectF.bottom) { // 放大状况下,有一边缩放后在屏幕内部,自动吸附到屏幕边缘
if (rectF.left > videoRectF.left) { // 左移吸边
transAnimX = videoRectF.left - rectF.left;
} else if (rectF.right < videoRectF.right) { // 右移吸边
transAnimX = videoRectF.right - rectF.right;
}
if (rectF.top > videoRectF.top) { // 上移吸边
transAnimY = videoRectF.top - rectF.top;
} else if (rectF.bottom < videoRectF.bottom) { // 下移吸边
transAnimY = videoRectF.bottom - rectF.bottom;
}
scaleEndAnimMatrix.postTranslate(transAnimX, transAnimY);
startTransToAnimEnd(mScaleTransMatrix, scaleEndAnimMatrix);
}
}
}
private void startTransToAnimEnd(Matrix startMatrix, Matrix endMatrix) {
LogUtil.d(TAG, "startTransToAnimEnd \nstart=" + startMatrix + "\nend=" + endMatrix);
// 令 A = startMatrix;B = endMatrix
// 方法1:直接将画面更新为结束矩阵位置B
// updateMatrixToView(endMatrix); //
// 方法2:将画面从现有位置A,移动到结束矩阵位置B,移动的距离T。B = T * A; 根据矩阵乘法的计算规则,反推出:T(x) = B(x) - A(x); T(y) = B(y) - A(y)
// float[] startArray = new float[9];
// float[] endArray = new float[9];
// startMatrix.getValues(startArray);
// endMatrix.getValues(endArray);
// float transX = endArray[Matrix.MTRANS_X] - startArray[Matrix.MTRANS_X];
// float transY = endArray[Matrix.MTRANS_Y] - startArray[Matrix.MTRANS_Y];
// startMatrix.postTranslate(transX, transY);
// LogUtil.d(TAG, "transToCenter1 \nstart=" + startMatrix + "\nend" + endMatrix);
// updateMatrixToView(startMatrix);
// 方法3:在方法2基础上,增长动画移动效果
if (mScaleAnimator != null) {
mScaleAnimator.cancel();
mScaleAnimator = null;
}
if (mScaleAnimator == null) {
mScaleAnimator = new VideoScaleEndAnimator(startMatrix, endMatrix) {
@Override
protected void updateMatrixToView(Matrix transMatrix) {
updateMatrixToTexture(transMatrix);
}
};
mScaleAnimator.start();
}
mScaleTransMatrix = endMatrix;
}
public void showScaleReset() {
if (isScaled() && mTouchAdapter != null && mTouchAdapter.isFullScreen()) {
if (mScaleRestView != null && mScaleRestView.getVisibility() != View.VISIBLE) {
mScaleRestView.setVisibility(View.VISIBLE);
}
}
}
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// 缩放模式下,是否须要单手滚动
// if (isScaled(mScale) && mScaleTransMatrix != null) {
// TextureView mTextureView = mTouchAdapter.getTextureView();
// if (mTextureView != null) {
// postTranslate(mScaleTransMatrix, -distanceX, -distanceY);
// onScaleMatrixUpdate(mScaleTransMatrix);
// Matrix matrix = new Matrix(mTextureView.getMatrix());
// matrix.set(mScaleTransMatrix);
// mTextureView.setTransform(matrix);
// return true;
// }
// }
return false;
}
private void onScaleMatrixUpdate(Matrix matrix) {
matrix.getValues(mMatrixValue);
mScale = mMatrixValue[Matrix.MSCALE_X];
// 暂停下,实时更新缩放画面
if (!mTouchAdapter.isPlaying()) {
TextureView mTextureView = mTouchAdapter.getTextureView();
if (mTextureView != null) {
mTextureView.invalidate();
}
}
}
/** * 是否处于已缩放 or 缩放中 * * @return */
public boolean isInScaleStatus() {
return isScaled(mScale) || mIsScaleTouch;
}
public boolean isScaled() {
return isScaled(mScale);
}
private boolean isScaled(float scale) {
return scale > 0 && scale <= 0.99F || scale >= 1.01F;
}
}
复制代码
/** * 缩放动画 * <p> * 在给定时间内从一个矩阵的变化逐渐动画到另外一个矩阵的变化 */
public abstract class VideoScaleEndAnimator extends ValueAnimator implements ValueAnimator.AnimatorUpdateListener {
private static final String TAG = "VideoScaleEndAnimator";
/** * 图片缩放动画时间 */
public static final int SCALE_ANIMATOR_DURATION = 300;
Matrix mTransMatrix = new Matrix();
float[] mTransSpan = new float[2];
float mLastValue;
/** * 构建一个缩放动画 * <p> * 从一个矩阵变换到另一个矩阵 * * @param start 开始矩阵 * @param end 结束矩阵 */
public VideoScaleEndAnimator(Matrix start, Matrix end) {
this(start, end, SCALE_ANIMATOR_DURATION);
}
/** * 构建一个缩放动画 * <p> * 从一个矩阵变换到另一个矩阵 * * @param start 开始矩阵 * @param end 结束矩阵 * @param duration 动画时间 */
public VideoScaleEndAnimator(Matrix start, Matrix end, long duration) {
super();
setFloatValues(0, 1f);
setDuration(duration);
addUpdateListener(this);
float[] startValues = new float[9];
float[] endValues = new float[9];
start.getValues(startValues);
end.getValues(endValues);
mTransSpan[0] = endValues[Matrix.MTRANS_X] - startValues[Matrix.MTRANS_X];
mTransSpan[1] = endValues[Matrix.MTRANS_Y] - startValues[Matrix.MTRANS_Y];
mTransMatrix.set(start);
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 获取动画进度
float value = (Float) animation.getAnimatedValue();
// 计算相对于上次位置的偏移量
float transX = mTransSpan[0] * (value - mLastValue);
float transY = mTransSpan[1] * (value - mLastValue);
mTransMatrix.postTranslate(transX, transY);
updateMatrixToView(mTransMatrix);
mLastValue = value;
}
protected abstract void updateMatrixToView(Matrix transMatrix);
}
复制代码