[toc]

The index

  1. Android video gesture zooming and rebound dynamic effect implementation (A) : mainly to achieve video double fingers: zooming, translation, rebound dynamic effect
  2. Android video rotation, zooming and rebound dynamic effect realization (TWO) : mainly to achieve video double fingers: rotation, zooming, translation, rebound dynamic effect

In Android video gesture zooming and rebound dynamic effect implementation (I), we have achieved the following requirements in part 1-5. In fact, for two-finger gesture touch, it can not only zoom, pan, but also rotate. Now we will transform on the original basis, add video gesture rotation and rebound dynamic effect, to achieve requirement 6.

The functional requirements

  1. Double finger zoom video playback screen, support setting the minimum and maximum zoom range
  2. Double finger drag screen can move in any direction
  3. If it is zoomed out, it should be centered on the screen and animated
  4. If the screen is enlarged, the edge of the screen should be automatically attached to the edge of the screen
  5. Video can also be zoomed in and out when paused
  6. Double finger rotation screen, rotation Angle to 45 degrees as the dividing line, automatic correction (more than 45 degrees rotation 90 degrees; Not more than 45 degrees rotation 0 degrees)

Implementation approach

Main idea of implementation

  1. Gesture rotation recognition. Receive onTouchEvent to identify two-finger rotation: rotation Angle, rotation center
  2. Gesture rotation processing. After recognizing the gesture rotation, passMatrix.postRotateCarry out picture rotation transformation
  3. Scaling multiple logic transformation. It’s also caused by rotationMatrix#MSCALE_XWhen the value changes, the scaling factor cannot be calculated by directly obtaining the component value of the matrix, and the calculated scaling factor needs to be saved independently. Reference:Android Matrix takes you to control thunder and LightningThe rotation part is rightMatrixThe influence of
  4. Springback dynamic effect trigger. The original springback dynamic effect is after scalingonScaleEndTrigger, add rotation, need to make sure at the end of the zoomonScaleEndAnd the end of rotationonRotateEndAfter the trigger, select hereACTION_UPTo trigger the rebound effect.
  5. Calculation of springback dynamic effect. After rotation is added, the springback dynamic effect is in the original two data:transAnimX,transAnimY(translation compensation on the x and y axes) based on the increase of rotation compensation AnglerotateEndFixDegrees.

1. Rotation recognition

We use in the scaling, translation class ScaleGestureDetector zoom system identification. The onTouchEvent (event) to identify the current touch events, get onScaleBegin, onScale, onScaleEnd scaling back, Then use VideoTouchScaleHandler to handle zooming and panning accordingly.

After adding the rotation function, since the system does not have its own rotation recognition class, we need to customize the gesture recognition rotation: RotateGestureDetector. The main logic inside the RotateGestureDetector is how to identify the rotation center and rotation Angle.

  1. Center of rotation The center of rotation is easy to calculate by obtaining the center point of the line between two fingers. But in fact, in the video rotation, we will not use the two-finger center to rotate, but directly use the center of the picture to rotate. Why?

    If the two-finger center is used for rotation, since the center point of each rotation is not fixed, the displacement component in the transformation matrix will also be affected during rotation. In this way, when calculating the springback dynamic effect later, the displacement transAnimX and transAnimY that need to be compensated cannot be accurately calculated due to the change of the rotation center point.

     // Calculate the rotation center point of two fingers
     float pivotX = (event.getX(0) + event.getX(1)) / 2;
     float pivotY = (event.getY(0) + event.getY(1)) / 2;
    
     // The rotation center of the actual screen is the screen center
     mRotateCenter = new PointF(mTouchAdapter.getTextureView().getWidth() / 2,
                     mTouchAdapter.getTextureView().getHeight() / 2);
    Copy the code
  2. The calculation of rotation Angle is a little more complicated. We need to calculate the current Angle of the connection between the two fingers first and subtract the last recorded Angle of the connection between the fingers last time, so as to get the diffDegree = degrees-last degree of rotation. As shown in the figure:

How to calculate the degree between two fingers?

Here we need to obtain the Delta x, Delta y\Delta x, \Delta y δ x, Delta y δ x, δ y. Since tan(θ)= δ y δ xtan(\theta)=\frac{\Delta y}{\Delta x}tan(θ)= δ x δ y Offset Angle θ= Arctan δ y δ x theta=arctan is obtained by calculating the arctangent trigonometric function arctangent of the Angle Y Delta \ frac {\} {\ Delta x} theta equals arctan Δ Δ y x. A few points to note:

  1. Math.atan2(y, x) and Math.atan(v) differences.

    The inverse tangent can be computed. In order to avoid didivisions of 0 (and other differences, such as the range of results), math.atan2 (y, x) is used.

  2. Math.atan2(y, x) result.

    The calculated value is radians. The value ranges from **[-math. PI, math.pi]. The Matrix. PostRotate input is an Angle, so we need to convert radians to angles **[-180, 180] through math. toDegrees.

  3. A sharp change in Angle caused by slight dithering in the denominator.

    Just think about it. If the δ y δ x\frac{\Delta y}{\Delta x} δ x δ y, a slight change in the offset of the δ x\Delta y δ y will cause a drastic change in the Angle, which is obviously not in accordance with the user’s real intention. We can think of it as just changing the Angle a little bit, plus or minus 5 degrees.

  4. If diffDegree>0, the Matrix. PostRotate feeds in the Angle for rotation. If the result is positive, the rotation is clockwise; otherwise, the rotation is counterclockwise

 // Calculate the rotation center of two fingers, so that the gesture recognition tool class has the ability to recognize the rotation center, but this time it will not be used, but always use the center of the screen to rotate
 float pivotX = (event.getX(0) + event.getX(1)) / 2;
 float pivotY = (event.getY(0) + event.getY(1)) / 2;
float deltaX = event.getX(0) - event.getX(1);
 float deltaY = event.getY(0) - event.getY(1);
 float degrees = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));

 // Calculate the rotation Angle
 float diffDegree = degrees - mLastDegrees;  // Rotation Angle = Current Angle between two fingers - last Angle between two fingers The result is greater than 0 and rotates clockwise; Less than 0, counterclockwise
 if (diffDegree > 45) {  // y/x denominator slight jitter caused by sharp Angle change, fixed
     diffDegree = -5;
 } else if (diffDegree < -45) {
  diffDegree = 5;
 }
Copy the code

Below is all the source code for the RotateGestureDetector class

Rotation detection: RotateGestureDetector

/** * Gesture rotation recognition * <p> **@author yinxuming
 * @date2020/12/22 * /
public class RotateGestureDetector {

    private OnRotateGestureListener mRotateGestureListener;
    private boolean mIsRotate = false;
    private float lastDegrees;


    public boolean onTouchEvent(MotionEvent event) {
        if(event.getPointerCount() ! =2) {
            mIsRotate = false;
            return false;
        }
        float pivotX = (event.getX(0) + event.getX(1)) / 2;
        float pivotY = (event.getY(0) + event.getY(1)) / 2;
        float deltaX = event.getX(0) - event.getX(1);
        float deltaY = event.getY(0) - event.getY(1);
        float degrees = (float) Math.toDegrees(Math.atan2(deltaY, deltaX)); // The Angle between the two fingers

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_POINTER_DOWN:
                lastDegrees = degrees;
                mIsRotate = false;
                break;
            case MotionEvent.ACTION_MOVE:
                if(! mIsRotate) { mIsRotate =true;
                    notifyRotateBegin();
                }
                float diffDegree = degrees - lastDegrees;  // Rotation Angle = Current Angle between two fingers - last Angle between two fingers The result is greater than 0 and rotates clockwise; Less than 0, counterclockwise
                if (diffDegree > 45) { // y/x denominator slight jitter caused by sharp Angle change, fixed
                    diffDegree = -5;
                } else if (diffDegree < -45) {
                    diffDegree = 5;
                }
                notifyRotate(diffDegree, pivotX, pivotY);
                lastDegrees = degrees;
                break;
            case MotionEvent.ACTION_POINTER_UP:
            case MotionEvent.ACTION_CANCEL:
                lastDegrees = 0;
                mIsRotate = false;
                notifyRotateEnd();
                break;
        }

        return true;
    }

    private void notifyRotateBegin(a) {
        if(mRotateGestureListener ! =null) {
            mRotateGestureListener.onRotateBegin(this); }}private void notifyRotate(float diffDegree, float pivotX, float pivotY) {
        if(mRotateGestureListener ! =null) {
            mRotateGestureListener.onRotate(this, diffDegree, pivotX, pivotY); }}private void notifyRotateEnd(a) {
        if(mRotateGestureListener ! =null) {
            mRotateGestureListener.onRotateEnd(this); }}public void setRotateGestureListener(OnRotateGestureListener rotateGestureListener) {
        mRotateGestureListener = rotateGestureListener;
    }

    public interface OnRotateGestureListener {
        boolean onRotateBegin(RotateGestureDetector detector);

        boolean onRotate(RotateGestureDetector detector, float degrees, float px, float py);

        void onRotateEnd(RotateGestureDetector detector);
    }

    public static class SimpleOnRotateGestureListener implements OnRotateGestureListener {

        @Override
        public boolean onRotateBegin(RotateGestureDetector detector) {
            return false;
        }

        @Override
        public boolean onRotate(RotateGestureDetector detector, float degrees, float px, float py) {
            return false;
        }


        @Override
        public void onRotateEnd(RotateGestureDetector detector) {}}}Copy the code

2. Rotate

Rotation, we’re going to do that with VideoTouchRotateHandler. Main function is to realize the recognition rotating RotateGestureDetector. OnRotateGestureListener interface, then to register the recognition tool RotateGestureDetector, rotation to receive the recognition result.

Finally, use Matrix. PostRotate (degrees, px, py) to rotate the image. A few notes:

  1. PostRotate (degrees, px, py) is different from Matrix. PostRotate (degrees). The default rotation center is the upper left corner of the control itself, so we must use the method with the rotation center parameter and set the rotation center to the TextureView center position.

  2. Cumulative rotation Angle error processing. If the cumulative rotation Angle exceeds 360, we need to take the mode conversion to avoid the continuous accumulation of rotation Angle errors, which will lead to inaccurate positioning of the picture. RotateDegrees = rotateDegrees % 360.

  3. Matrix is updated synchronously. How do I synchronize the Matrix that I’m using to rotate VideoTouchRotateHandler with the Matrix that I’m using to scale VideoTouchScaleHandler?

    Matrix = textureVie. GetTransform (NULL) is used to obtain the transform matrix of the current video frame. Whether scaling or rotation is performed on the same TextureView, it is obvious that the latest value is obtained. One error prone point here is to use view.getmatrix to get the Matrix.

    Of course, according to actual requirements, Matrix can also be cached, as long as the latest Matrix can be obtained at the same time as scaling and rotation, and the Matrix can be updated to the other party in real time.

  4. Calculation of compensating rotateEndFixDegrees after rotation.

     /** * Calculate the Angle to be compensated after the rotation is complete *@param currentRotateDegree
      * @return* /
     public static float computeRoteEndDegree(float currentRotateDegree) {
         float rotateEndFixDegrees = currentRotateDegree % 90;
         if(rotateEndFixDegrees ! =0) {
             if (rotateEndFixDegrees >= 45) { // If it is greater than 45 degrees, go straight to 90 and calculate the Angle required to go to 90
                 rotateEndFixDegrees = 90 - rotateEndFixDegrees;
             } else if (rotateEndFixDegrees > -45 && rotateEndFixDegrees < 45) { // (-45, 45), bounce back to 0 degrees
                 rotateEndFixDegrees = -rotateEndFixDegrees;
             } else if (rotateEndFixDegrees < -45) { < -45, go straight to -90, calculate the Angle required to go to 90
                 rotateEndFixDegrees = -90- rotateEndFixDegrees; }}return rotateEndFixDegrees;
     }
    Copy the code
  5. After the rotation rebound dynamic effect is over, the actual Angle position is updated. At the end of the rotation, you need to compensate rotateEndFixDegrees, do a rebound animation, make sure the picture is always in a multiple of 90 degrees, after the animation is complete, update the previously recorded rotation Angle VideoTouchRotateHandler#mRotateDegrees

Rotate handle: VideoTouchRotateHandler

/** * gesture rotation processing * <p> **@author yinxuming
 * @date2020/12/23 * /
public class VideoTouchRotateHandler implements IVideoRotateHandler.RotateGestureDetector.OnRotateGestureListener {
    private static final String TAG = "VideoTouchRotateHandler";

    private IVideoTouchAdapter mTouchAdapter;
    private boolean mIsRotating;  // Whether it is rotating
    private float mRotateDegrees;
    private PointF mRotateCenter;   // Just rotate around the center of the screen

    public VideoTouchRotateHandler(IVideoTouchAdapter videoTouchAdapter) {
        mTouchAdapter = videoTouchAdapter;
    }

    @Override
    public boolean onRotateBegin(RotateGestureDetector detector) {
         if (isTextureViewValid()) {
            mTouchAdapter.getVideoTouchEndAnim().endPrevAnim();
            mIsRotating = true;
            mRotateCenter = new PointF(mTouchAdapter.getTextureView().getWidth() / 2,
                    mTouchAdapter.getTextureView().getHeight() / 2);
        }
        return true;
    }

    @Override
    public boolean onRotate(RotateGestureDetector detector, float degrees, float px, float py) {
        if (isRotating()) {
            postRotate(degrees); // Always use the center of the screen to rotate, to avoid position changes caused by the rotation of the center point, and the final rebound dynamic effect can not align with the edge
        }
        return true;

    }

    private void postRotate(float rotateDegree) {
        Matrix matrix = getTransformMatrix();
        matrix.postRotate(rotateDegree, mRotateCenter.x, mRotateCenter.y);
        updateMatrixToTexture(matrix);
        setRotateDegrees(getCurrentRotateDegree() + rotateDegree);
    }


    @Override
    public void onRotateEnd(RotateGestureDetector detector) {
        LogUtil.e(TAG, "onRotateEnd " + mRotateDegrees);
        if(isRotating() && mTouchAdapter.getVideoTouchEndAnim() ! =null) {
            mTouchAdapter.getVideoTouchEndAnim().setEndAnimRotate(getCurrentRotateDegree(),
                    computeRoteEndDegree(getCurrentRotateDegree()));
        }
        mIsRotating = false;
    }

    public boolean isRotating(a) {
        return mIsRotating;
    }

    @Override
    public boolean isRotated(a) {
        returnmRotateDegrees ! =0 || mIsRotating;
    }

    @Override
    public float getCurrentRotateDegree(a) {
        return mRotateDegrees;
    }

    private void setRotateDegrees(float rotateDegrees) {
        rotateDegrees = rotateDegrees % 360; // If the value is greater than 360 degrees, take its modulus to avoid a sharp increase in accumulation error
        mRotateDegrees = rotateDegrees;

    }

    @Override
    public float getTargetRotateDegree(a) {
        return getCurrentRotateDegree() + computeRoteEndDegree(getCurrentRotateDegree());
    }

    /** * Update compensated Angle * after rotation rebound animation ends@param rotateDegree
     */
    @Override
    public void fixRotateEndAnim(float rotateDegree) {
        setRotateDegrees(getCurrentRotateDegree() + rotateDegree);
    }

    @Override
    public void cancelRotate(a) {
        setRotateDegrees(0);
        mRotateCenter = null;
    }

    private boolean isTextureViewValid(a) {
        returnmTouchAdapter.getTextureView() ! =null && mTouchAdapter.getTextureView().isAvailable();
    }

    private Matrix getTransformMatrix(a) {
        if (isTextureViewValid()) {
            return mTouchAdapter.getTextureView().getTransform(null);
        }
        return null;
    }

    private void updateMatrixToTexture(Matrix newMatrix) {
        if (isTextureViewValid()) {
            TextureView textureView = mTouchAdapter.getTextureView();
            textureView.setTransform(newMatrix);
            if(! mTouchAdapter.isPlaying()) { textureView.invalidate(); }}}/** * Calculate the Angle to be compensated after the rotation is complete *@param currentRotateDegree
     * @return* /
    public static float computeRoteEndDegree(float currentRotateDegree) {
        float rotateEndFixDegrees = currentRotateDegree % 90;
        if(rotateEndFixDegrees ! =0) {
            if (rotateEndFixDegrees >= 45) { // If it is greater than 45 degrees, go straight to 90 and calculate the Angle required to go to 90
                rotateEndFixDegrees = 90 - rotateEndFixDegrees;
            } else if (rotateEndFixDegrees > -45 && rotateEndFixDegrees < 45) { // (-45, 45), bounce back to 0 degrees
                rotateEndFixDegrees = -rotateEndFixDegrees;
            } else if (rotateEndFixDegrees < -45) { < -45, go straight to -90, calculate the Angle required to go to 90
                rotateEndFixDegrees = -90- rotateEndFixDegrees; }}returnrotateEndFixDegrees; }}Copy the code

3. Springback dynamic effect

The most difficult part of the whole video to deal with zooming, rotation and displacement is probably calculating the rebound dynamic effect. There are two main ideas to realize springback dynamic effect:

  1. Rebound dynamic effect idea 1

    Given the position of the current screen startAnimMatrix, to perform the rebound dynamic effect to the final position, we mainly do two types of operations, displacement and rotation, that is, we only need to calculate the displacement compensation and rotation Angle compensation, can construct the property animation, linear gradient displacement and rotation. In practice, there are some problems with this scheme, for it may not be completely centered when shrinking. It is speculated that when calculating displacement compensation, rotation is carried out first and then displacement compensation is calculated. However, in practice, the animation executes rotation and displacement at the same time, resulting in inaccurate displacement values.

  2. Rebound dynamic effect idea 2

    For another idea, see custom rotatable, paneled, scaled ImageView or PinchImageView. Its main idea is also known as startAnimMatrix, and then the focus becomes to directly calculate the kinetic end Matrix endAnimMatrix, and then when the animation is executed to manipulate the value of the 9 components of the Matrix, so that startAnimMatrix finally reaches endAnimMatrix.

Here, we mainly implement the dynamic effect of springback according to idea 2, and the following problems need to be solved:

  1. Timing of dynamic effect triggering
  2. Dynamic parameter calculation: caused by rotation compensation AngletransAnimX,transAnimYHow is the change of calculated?
  3. How to handle continuous animation: that is, the animation has not been executed, the next animation has arrived, how to handle?

1. Trigger time of dynamic effect

The time to bounce back is after scaling ends onScaleEnd, now you need to make sure that both onScaleEnd ends after scaling and onRotateEnd ends after rotation. Let’s observe the callback order of onTouchEvent event and conclude that the springback action can be triggered in ACTION_UP.

Touch the onTouchEvent callback sequence with two fingers

By looking at the sequence of event touch callbacks, we can see that all we need to do is trigger the rebound action in ACTION_UP.

int count = event.getPointerCount(); // The finger touches the point
int action = event.getActionMasked;  // Touch action
Copy the code

OnTouchEvent Two-finger touch event callback order:

count action
1 ACTION_DOWN Single refers to the press
2 ACTION_POINTER_DOWN Press the second finger
2 ACTION_MOVE Touch mobile
2 ACTION_MOVE
. .
2 ACTION_POINTER_UP Lifting one finger triggers:

OnScaleEnd, onRotateEnd
1 ACTION_UP Trigger by lifting the last finger

2. Dynamic effect parameter calculation

Dynamic effect parameters of springback mainly involve two types:

  1. Rotation compensation: compensation AnglerotateEndFixDegrees, rotation center (screen center), this already know how to calculate
  2. Displacement compensation:transAnimX,transAnimY

The question to be solved here is how to calculate the displacement compensation after adding rotation transAnimX, transAnimY? The main ideas are as follows:

  1. After the rotation, the screen’s transformation matrix currentTransformMatrix = TextuReview.getTransform (NULL) is known, which corresponds to startAnimMatrix, the starting position of the springback dynamic effect

  2. To calculate endAnimMatrix of end position of springback dynamic effect, we first need to compensate the rotation Angle of the current matrix, namely: CurrentTransformMatrix. PostRotate (fixDegrees, center x, center, y), get endAnimMatrix, at this time the location of the image from the position of the end of the animation, the difference is: Then calculate transAnimX and transAnimY.

  3. TransAnimX, transAnimY calculation, we use endAnimmatrix.mapRect (rectF) to measure the position of the rectangle after the above rotation compensation. Then compare the videoRectF position of the rectangle area of the screen occupied by the video TextureView (the rectangle area of the screen is in the case of full screen, and the following description is simplified to the description of the screen), then the displacement compensation can be calculated.

    Take transAnimX calculation as an example. The specific algorithm is as follows:

    1. When the width of the screen is less than the width of the screen: the screen is centered.
    2. When the left edge of the screen is inside the screen after transformation: the left edge of the screen is adsorbed to the left edge of the screen
    3. When the right edge of the screen is inside the screen after transformation: the right edge of the screen is adsorbed to the right edge of the screen
    if (currentLocationRectF.width() <= videoRectF.width()) { // Width < screen width: center
         transAnimX = videoRectF.right / 2 - (currentLocationRectF.right + currentLocationRectF.left) / 2;
     } else if (currentLocationRectF.left > videoRectF.left) { // The left side is in the screen: move the edge left
         transAnimX = videoRectF.left - currentLocationRectF.left;
     } else if (currentLocationRectF.right < videoRectF.right) {  // Right in the screen: move the edge right
         transAnimX = videoRectF.right - currentLocationRectF.right;
     }
    Copy the code

TransAnimY does the same thing.

3. Continuous springback dynamic effect treatment

The operation of dynamic effect takes time. If the last dynamic effect has not been completed and a new dynamic effect has been generated, how should we deal with it?

In the traditional way, we need to execute animtor.cancel(). The attribute animation ValueAnimotor. Cancel will only stay at the current position. The effect we want is that when the new animation is ready, the displacement and Angle compensation of the original animation must be completed, that is, immediately execute to the end position, so as not to affect the calculation of the new animation position.

We can do this by using ValueAnimotor. End directly, which calls back to the onAnimationUpdate end value and onAnimationEnd. OnAnimationStart, onAnimationUpdate, onAnimationUpdate, onAnimationUpdate, onAnimationUpdate… OnAnimationEnd.

After the animation is over, we need to update the rotation Angle of the original record VideoTouchRotateHandler#mRotateDegrees as the final Angle. VideoTouchRotateHandler# fixRotateEndAnim.

The following is the dynamic effect of the calculation and implementation of the source code

Springback dynamic effect parameter calculation: VideoTouchFixEndAnim


/** * springback dynamic effect parameter calculation, animation state control * <p> **@author yinxuming
 * @date2020/12/24 * /
public class VideoTouchFixEndAnim implements IVideoTouchEndAnim {

    private IVideoTouchAdapter mTouchAdapter;
    private ValueAnimator mAnimator;
    float mScale = 1.0 f;
    float mCurrentRotateDegrees;
    float mRotateEndFixDegrees;
    boolean isNeedFixAnim = false;

    public VideoTouchFixEndAnim(IVideoTouchAdapter touchAdapter) {
        mTouchAdapter = touchAdapter;
    }

    @Override
    public void setEndAnimScale(float scale) {
        mScale = scale;
        isNeedFixAnim = true;
    }

    @Override
    public void setEndAnimRotate(float currentRotate, float rotateEndFixDegrees) {
        mCurrentRotateDegrees = currentRotate;
        mRotateEndFixDegrees = rotateEndFixDegrees;
        isNeedFixAnim = true;
    }

    @Override
    public void startAnim(a) {
        // Note that the main thread calls animation related operations
        endPrevAnim();
        if(! isNeedFixAnim) {return;
        }
        mAnimator = makeFixEndAnimator();
        if (mAnimator == null) {
            return;
        }
        mAnimator.start();
    }


    @Override
    public void endPrevAnim(a) {
        if(mAnimator ! =null && (mAnimator.isRunning() || mAnimator.isStarted())) {
            mAnimator.end();
        }
        mAnimator = null;
    }

    /** * transAnimX, transAnimY, endAnimMatrix@return* /
    private ValueAnimator makeFixEndAnimator(a) {
        TextureView mTextureView = mTouchAdapter.getTextureView();
        // Animation start matrix: current screen transformation
        Matrix currentTransformMatrix = mTextureView.getTransform(null);
        Matrix endAnimMatrix = new Matrix();
        final float fixDegrees = mRotateEndFixDegrees;
        RectF videoRectF = new RectF(0.0, mTextureView.getWidth(), mTextureView.getHeight());
        PointF center = new PointF(videoRectF.right / 2, videoRectF.bottom / 2);
        endAnimMatrix.set(currentTransformMatrix);
        // Animation end matrix: simulate the calculation of the current screen after rotation compensation matrix
        endAnimMatrix.postRotate(fixDegrees, center.x, center.y);
        RectF currentLocationRectF = new RectF(0.0, mTextureView.getWidth(), mTextureView.getHeight());
        // Measure the final matrix transformation position of the screen
        endAnimMatrix.mapRect(currentLocationRectF);

        float transAnimX = 0f;
        float transAnimY = 0f;
        if (currentLocationRectF.left > videoRectF.left
                || currentLocationRectF.right < videoRectF.right
                || currentLocationRectF.top > videoRectF.top
                || currentLocationRectF.bottom < videoRectF.bottom) { // One side of the screen is zoomed in and automatically attached to the edge or center of the screen

            if (currentLocationRectF.width() <= videoRectF.width()) { // Width < screen width: center
                transAnimX = videoRectF.right / 2 - (currentLocationRectF.right + currentLocationRectF.left) / 2;
            } else if (currentLocationRectF.left > videoRectF.left) { // The left side is in the screen: move the edge left
                transAnimX = videoRectF.left - currentLocationRectF.left;
            } else if (currentLocationRectF.right < videoRectF.right) {  // Right in the screen: move the edge right
                transAnimX = videoRectF.right - currentLocationRectF.right;
            }

            if (currentLocationRectF.height() <= videoRectF.height()) { // Height < screen: center
                transAnimY = videoRectF.bottom / 2 - (currentLocationRectF.bottom + currentLocationRectF.top) / 2;
            } else if (currentLocationRectF.top > videoRectF.top) {  // Move the suction edge up
                transAnimY = videoRectF.top - currentLocationRectF.top;
            } else if (currentLocationRectF.bottom < videoRectF.bottom) { // Move the suction edge down
                transAnimY = videoRectF.bottom - currentLocationRectF.bottom;
            }
        }

        endAnimMatrix.postTranslate(transAnimX, transAnimY);

        // Do not use animation to transform directly
// mTouchAdapter.getTextureView().setTransform(endAnimMatrix);
// mTouchAdapter.getVideoRotateHandler().postRotateDegrees(fixDegrees, false);
        if (transAnimX == 0 && transAnimY == 0 && fixDegrees == 0) {
            return null;
        } else {
            ScaleRotateEndAnimator animator = new ScaleRotateEndAnimator() {
                @Override
                protected void updateMatrixToView(Matrix transMatrix) {
                    mTouchAdapter.getTextureView().setTransform(transMatrix);
                }

                @Override
                protected void onFixEndAnim(ValueAnimator animator, float fixEndDegrees) {
                    mTouchAdapter.getVideoRotateHandler().fixRotateEndAnim(fixEndDegrees);
                    if (animator == mAnimator) {
                        mAnimator = null; onAnimEndRelease(); }}}; animator.setScaleEndAnimParams(currentTransformMatrix, endAnimMatrix, fixDegrees);returnanimator; }}private void onAnimEndRelease(a) {
        isNeedFixAnim = false;

        mScale = 1.0 f;
        mCurrentRotateDegrees = 0;
        mRotateEndFixDegrees = 0; }}Copy the code

Springback dynamic effect: ScaleRotateEndAnimator

public abstract class ScaleRotateEndAnimator extends ValueAnimator implements ValueAnimator.AnimatorUpdateListener.Animator.AnimatorListener {
    private static final String TAG = "VideoScaleEndAnimator";

    /** * Picture zoom animation time */
    public static final int SCALE_ANIMATOR_DURATION = 1000;

    private Matrix mStartMatrix = new Matrix();
    private Matrix mEndMatrix = new Matrix();
    private Matrix mMatrix = new Matrix();
    private float[] mStartMatrixValue;
    private float[] mInterpolateMatrixValue;
    private float[] mEndMatrixValue;
    private float mRotateDegrees;


    public void setScaleEndAnimParams(Matrix startMatrix, Matrix endMatrix, float rotateFixDegree) {
        mStartMatrix = startMatrix;
        mEndMatrix = endMatrix;
        mRotateDegrees = rotateFixDegree;
        mMatrix.reset();
        if (mStartMatrix == null || mEndMatrix == null) {
            return;
        }
        mStartMatrixValue = new float[9];
        mStartMatrix.getValues(mStartMatrixValue);
        mEndMatrixValue = new float[9];
        mEndMatrix.getValues(mEndMatrixValue);
        mInterpolateMatrixValue = new float[9];

        setAnimConfig();
    }

    protected void setAnimConfig(a) {
        setFloatValues(0.1f);
        setDuration(SCALE_ANIMATOR_DURATION);
        addUpdateListener(this);
        addListener(this);
    }


    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        // Get the animation progress
        float value = (Float) animation.getAnimatedValue();
        onValueUpdate(value);
    }


    public void onValueUpdate(float value) {
        if (mStartMatrix == null
                || mEndMatrix == null) {
            return;
        }
        for (int i = 0; i < 9; i++) {
            mInterpolateMatrixValue[i] = mStartMatrixValue[i] + (mEndMatrixValue[i] - mStartMatrixValue[i]) * value;
        }
        mMatrix.setValues(mInterpolateMatrixValue);
        updateMatrixToView(mMatrix);
    }


    protected abstract void updateMatrixToView(Matrix transMatrix);

    protected abstract void onFixEndAnim(ValueAnimator animator, float fixEndDegrees);

    @Override
    public void onAnimationStart(Animator animation) {}@CallSuper
    @Override
    public void onAnimationEnd(Animator animation) {
        onFixEndAnim(this, mRotateDegrees);
    }

    @CallSuper
    @Override
    public void onAnimationCancel(Animator animation) {}@Override
    public void onAnimationRepeat(Animator animation) {}}Copy the code

Complete project code

Github complete source code

reference

  1. Android Matrix takes you to control thunder and Lightning
  2. Customize ImageView that can be rotated, panned, or scaled