[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);
}
複製代碼