PhotoView is a control for processing picture gestures, its source design is very good, high cohesion and low coupling, worth our in-depth study.

1 Basic Structure

The PhotoView class code is very simple, just look at the construction.

public PhotoView(Context context, AttributeSet attr, int defStyle) {
    super(context, attr, defStyle);
    init();
}

private void init(a) {
    attacher = new PhotoViewAttacher(this);
    //We always pose as a Matrix scale type, though we can change to another scale type
    //via the attacher
    super.setScaleType(ScaleType.MATRIX);
    //apply the previously applied scale type
    if(pendingScaleType ! =null) {
        setScaleType(pendingScaleType);
        pendingScaleType = null; }}Copy the code

Initialize a PhotoViewAttacher class and set ScaleType to scaleType. MATRIX because PhotoView gestures are implemented by setting MATRIX. The core code of PhotoView is in PhotoViewAttacher. PhotoViewAttacther can be seen as a proxy of PhotoView. Let’s start with the construction of the photoview wattacher.

public PhotoViewAttacher(ImageView imageView) {
    mImageView = imageView;
    imageView.setOnTouchListener(this);
    imageView.addOnLayoutChangeListener(this);
    if (imageView.isInEditMode()) {
        return;
    }
    mBaseRotation = 0.0 f;
    // Create Gesture Detectors...
    mScaleDragDetector = new CustomGestureDetector(imageView.getContext(), onGestureListener);
    mGestureDetector = new GestureDetector(imageView.getContext(), new GestureDetector.SimpleOnGestureListener() {

        // forward long click listener
        @Override
        public void onLongPress(MotionEvent e) {}@Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {}@Override
        public boolean onSingleTapConfirmed(MotionEvent e) {}@Override
        public boolean onDoubleTap(MotionEvent ev) {}@Override
        public boolean onDoubleTapEvent(MotionEvent e) {}}); }Copy the code

The constructor takes an ImageView argument, which in this class is used to get the ImageView boundary, get the drawable, update the ImageView matrix, and so on. Then set **OnTouchListener**, the touch processing is done in its callback Boolean onTouch(View V, MotionEvent ev) method, OnLayoutChangeListener is mainly used to update the image’s default matrix when the external layout changes. Here’s the isInEditMode() method, which is used to preview the Android Studio layout editor, In the preview environment to get the context is com. The android.. Layoutlib bridge. Android. BridgeContext, here the method to get to some of the object is and the android system environment is not the same. Also in RecyclerView source code we can see a few lines of code:

private void createLayoutManager(Context context, String className, AttributeSet attrs,
            int defStyleAttr, int defStyleRes) {
    ClassLoader classLoader;
    if (isInEditMode()) {
        // Stupid layoutlib cannot handle simple class loaders.
        classLoader = this.getClass().getClassLoader();
    } else{ classLoader = context.getClassLoader(); }}Copy the code

Stupid layoutlib cannot handle simple class loaders. Stupid Layoutlib cannot handle simple class loaders. We can also use this method in onDraw to distinguish between the preview environment and the real environment. Going back to the photoview Wattacher code, it just returns isInEditMode, probably because the preview environment doesn’t need to listen for touch events, so it doesn’t go to the relevant method. We then initialize the CustomGestureDetector class, passing in OnGestureListener, which is a gesture listening callback interface.

interface OnGestureListener {

    void onDrag(float dx, float dy);

    void onFling(float startX, float startY, float velocityX,
                 float velocityY);

    void onScale(float scaleFactor, float focusX, float focusY);

    void onScale(float scaleFactor, float focusX, float focusY, float dx, float dy);
}
Copy the code

The GestureDetector is then initialized to listen for click, double click, long press, and fling callbacks. Let’s focus on the Boolean onTouch(View V, MotionEvent EV) method.

@Override
public boolean onTouch(View v, MotionEvent ev) {
    boolean handled = false;
    if (mZoomEnabled && Util.hasDrawable((ImageView) v)) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                ViewParent parent = v.getParent();
                // First, disable the Parent from intercepting the touch
                // event
                if(parent ! =null) {
                    parent.requestDisallowInterceptTouchEvent(true);
                }
                // If we're flinging, and the user presses down, cancel
                // fling
                cancelFling();
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                // If the user has zoomed less than min scale, zoom back
                // to min scale
                if (getScale() < mMinScale) {
                    RectF rect = getDisplayRect();
                    if(rect ! =null) {
                        v.post(new AnimatedZoomRunnable(getScale(), mMinScale,
                            rect.centerX(), rect.centerY()));
                        handled = true; }}else if (getScale() > mMaxScale) {
                    RectF rect = getDisplayRect();
                    if(rect ! =null) {
                        v.post(new AnimatedZoomRunnable(getScale(), mMaxScale,
                            rect.centerX(), rect.centerY()));
                        handled = true; }}break;
        }
        // Try the Scale/Drag detector
        if(mScaleDragDetector ! =null) {
            boolean wasScaling = mScaleDragDetector.isScaling();
            boolean wasDragging = mScaleDragDetector.isDragging();
            handled = mScaleDragDetector.onTouchEvent(ev);
            booleandidntScale = ! wasScaling && ! mScaleDragDetector.isScaling();booleandidntDrag = ! wasDragging && ! mScaleDragDetector.isDragging(); mBlockParentIntercept = didntScale && didntDrag; }// Check to see if the user double tapped
        if(mGestureDetector ! =null && mGestureDetector.onTouchEvent(ev)) {
            handled = true; }}return handled;
}
Copy the code

MZoomEnabled is an externally settable property that handles gestures only if zooming is allowed and the ImageView has a drawable. Cancel the fling when ACTION_DOWN, and prevent the parent View to intercept touch events, here in the parent. RequestDisallowInterceptTouchEvent (true); , requestDisallowInterceptTouchEvent (Boolean disallowIntercept) method in a custom View of the scene or use of a lot of. Correct scaling in ACTION_CANCEL and ACTION_UP, and pull overly scaled operations back to the specified range through the Animation. Now pass the event to the CustomGestureDetector and GestureDetector.

2 Gesture Monitoring

Let’s look at the specific gesture listening part. Gestures generally include: The PhotoView uses a native GestureDetector for single click, double click, long press, two-finger zoom, and fling. PhotoView uses a GestureDetector for single click, double click, long press, and fling. A CustomGestureDetector is defined to handle the fling event. Note that the CustomGestureDetector also handles the Fling event. Let’s focus on the CustomGestureDetector class, which handles zooming and dragging. Detection of scaling is handled using a native ScaleGestureDetector. The constructor of ScaleGestureDetector needs to pass in an OnScaleGestureListener to call back scaling related values. Take a look at the integration of ScaleGestureDetector. First define the callback in the constructor.

ScaleGestureDetector.OnScaleGestureListener mScaleListener = new ScaleGestureDetector.OnScaleGestureListener() {
    private float lastFocusX, lastFocusY = 0;

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        float scaleFactor = detector.getScaleFactor();

        if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor))
            return false;

        if (scaleFactor >= 0) {
            mListener.onScale(scaleFactor,
                    detector.getFocusX(),
                    detector.getFocusY(),
                    detector.getFocusX() - lastFocusX,
                    detector.getFocusY() - lastFocusY
            );
            lastFocusX = detector.getFocusX();
            lastFocusY = detector.getFocusY();
        }
        return true;
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        lastFocusX = detector.getFocusX();
        lastFocusY = detector.getFocusY();
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {
        // NO-OP}}; mDetector =new ScaleGestureDetector(context, mScaleListener);
Copy the code

The logic here is simple. Two member variables are defined to record the center points of the x and y axes respectively. The difference between the center points of the two callbacks is how far the center points have moved. ScaleFactor is the scaling factor, the scale relative to the current size. Then in onTouchEvent, pass the event to the ScaleGestureDetector.

public boolean onTouchEvent(MotionEvent ev) {
    try {
        mDetector.onTouchEvent(ev);
        return processTouchEvent(ev);
    } catch (IllegalArgumentException e) {
        // Fix for support lib bug, happening when onDestroy is called
        return true; }}Copy the code

There’s a processTouchEvent, and that’s where the drag is handled. Before I look at the code, I’ll cover the basics of multi-touch. Touch events mainly involve the MotionEvent class, which stores the finger movement state and mainly includes:

  • ACTION_DOWN The first finger is pressed
  • ACTION_POINTER_DOWN After the first finger is pressed, the other fingers are pressed
  • ACTION_POINTER_UP Lift one of the multiple fingers at a long time and note that there are still fingers on the screen after loosening
  • ACTION_UP lifts the last finger
  • ACTION_MOVE Finger movement
  • The parent ACTION_CANCEL View receives an ACTION_DOWN and sends the event to the child View. If subsequent ACTION_MOVE and ACTION_UP events are blocked by the parent View, the child View receives an ACTION_CANCEL event

You can get an action by using getAction(), where the return value, for a single finger, is the state of the action, with the same meaning as the above constants, but for a multi-finger press or lift, the return value contains the index of the action, while the return value for a multi-finger slide does not contain the index and remains the state. The state and index of an action can be retrieved separately, with getActionMasked() fetching only the state and getActionIndex() fetching only the index. For multi-finger operations, there are two properties to focus on, PointerId and PointerIndex, which can be obtained by getActionIndex(), It can also be obtained by findPointerIndex(int pointerId). The touchpoint ID can be obtained by the getPointerId(int pointerIndex) method, which requires passing in the touchpoint index. Note the values of PointerId and PointerIndex. _

  • PointerId is generated when the finger is pressed and recycled when the finger is lifted. Note that when multi-touch, any finger is lifted and other fingers are liftedPointerIdThe same,PointerIdAssignment does not change.
  • PointerIndex is generated when a finger is pressed, counting from 0, multi-touch when one of the fingers is lifted, behind the fingerPointerIndexThe value ranges from 0 to number of touch points -1.

_ Now let’s look at the code, some code of processTouchEvent feels redundant, the following code is modified (if you don’t like it, don’t print ~).

private boolean processTouchEvent(MotionEvent ev) {
    switch (ev.getActionMasked()) {
        case MotionEvent.ACTION_DOWN:
            break;
        case MotionEvent.ACTION_MOVE:
            break;
        case MotionEvent.ACTION_CANCEL:
            break;
        case MotionEvent.ACTION_UP:
            break;
        case MotionEvent.ACTION_POINTER_UP:
            break;
    }
    return true;
}
Copy the code

The above methods and constants have been described, here is mainly to talk about these constants under the case of common operations.

  • ACTION_DOWN typically records the position of the touch points and initializes some variables.
  • ACTION_MOVE generally obtains the current position of the touch point, takes the difference of the last recorded position, and performs operations such as zooming and dragging.
  • ACTION_CANCEL Event interrupts, resets the status.
  • ACTION_UP resets the state. If it is in the dragging state, it will judge the sliding speed. If the speed exceeds a certain value, inertial sliding will be triggered.
  • One of the ACTION_POINTER_UP fingers is lifted and the position of the reference touch point needs to be updated.

To take a closer look at the code, let’s look at ACTION_DOWN processing:

mVelocityTracker = VelocityTracker.obtain();
if (null! = mVelocityTracker) { mVelocityTracker.addMovement(ev); } mLastTouchX = ev.getX(); mLastTouchY = ev.getY(); mIsDragging =false;
Copy the code

First, initialize the VelocityTracker class. The SynchronizedPool obtain() method obtains the VelocityTracker instance from the pool first. AddMovement tracks movement events and is invoked in ACTION_DOWN, ACTION_MOVE, and ACTION_UP. Float getX() (int pointerIndex); float getX(int pointerIndex) (int pointerIndex); So here’s the first finger press, and I think it’s enough to go with no arguments. Moving on to the ACTION_MOVE event.

final float x = ev.getX();
final float y = ev.getY();
final float dx = x - mLastTouchX, dy = y - mLastTouchY;

if(! mIsDragging) {// Use Pythagoras to see if drag length is larger than
    // touch slop
    mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop;
}

if (mIsDragging) {
    mListener.onDrag(dx, dy);
    mLastTouchX = x;
    mLastTouchY = y;

    if (null != mVelocityTracker) {
        mVelocityTracker.addMovement(ev);
    }
}
Copy the code

First, the movement distances dx and dy, which are used for dragging gestures, mIsDragging has been initialized to false in the ACTION_DOWN event, the judgment condition of dragging is that the sliding distance is greater than the minimum sliding distance, which has been assigned in the constructor:

final ViewConfiguration configuration = ViewConfiguration.get(context);
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
mTouchSlop = configuration.getScaledTouchSlop();
Copy the code

GetScaledTouchSlop is according to according to the device density (density) to obtain the minimum sliding distance, the default is 8 dp (< dimen name = “config_viewConfigurationTouchSlop” > 8 dp < / dimen >). If you can currently drag, the drag callback is triggered and the current X and Y coordinates are recorded, adding events to the VelocityTracker.

Note that the onFling callback has a negative sign for the speed because the callback is for the OverScroller, whose coordinate system (positive left up) is opposite to the normal coordinate system (positive right down).

Moving on to ACTION_UP.

if (mIsDragging) {
    if (null! = mVelocityTracker) { mLastTouchX = ev.getX(); mLastTouchY = ev.getY();// Compute velocity within the last 1000ms
        mVelocityTracker.addMovement(ev);
        mVelocityTracker.computeCurrentVelocity(1000);

        final float vX = mVelocityTracker.getXVelocity(), vY = mVelocityTracker.getYVelocity();

        // If the velocity is greater than minVelocity, call
        // listener
        if(Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) { mListener.onFling(mLastTouchX, mLastTouchY, -vX, -vY); }}}// Recycle Velocity Tracker
if (null! = mVelocityTracker) { mVelocityTracker.recycle(); mVelocityTracker =null;
}
Copy the code

Here, we mainly deal with the inertial sliding after releasing the hand and release the VelocityTracker. To determine whether to inertial sliding, we need to look at the speed of x axis and y axis. Before VelocityTracker can obtain the speed, computeCurrentVelocity(int units) shall be called to calculate the speed. The parameter of computeCurrentVelocity(int units) method is unit, 1 represents 1ms. The unit of getXVelocity() and getYVelocity() is px/s. If either of the x or y axes is greater than the minimum velocity, inertial sliding will be triggered. This minimum speed is similar to TouchSlop above and is also obtained from ViewConfiguration:

mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
<dimen name="config_viewMinFlingVelocity">50dp</dimen>
Copy the code

The default value is 50dp. At the end of the ACTION_UP event, release the VelocityTracker. Moving on to the ACTION_POINTER_UP event.

final int pointerIndex = ev.getActionIndex();
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastTouchX = ev.getX(newPointerIndex);
mLastTouchY = ev.getY(newPointerIndex);
Copy the code

Multi-finger touch lifts one of the fingers, because mLastTouchX has been storing the coordinates of the first finger before, so we just need to determine if the first finger is lifted, and if the first finger is lifted, we update it to the coordinates of the next finger. So far the core touch event capture has been done, mainly dealing with dragging and inertial sliding.

2 Gesture processing

Before talking about specific processing, let’s look at the three basic variables, mBaseMatrix, mSuppMatrix and mDrawMatrix.

  • MBaseMatrix base matrix, record is the picture according toScaleTypeZoom move to fitImageViewDoes not record gesture operations
  • MSuppMatrix Additional matrix, which records gesture operations
  • MDrawMatrix is actually set toImageViewThe matrix of theta is given bymBaseMatrixmSuppMatrixMultiply by

The mDrawMatrix is used when setting the matrix for the ImageView and getting the boundaries.

2.1 zoom

Zoom is divided into double – click zoom and multi – finger zoom. Let’s take a look at the callback handling of double click scaling. PhotoView defines three default zoom levels: 1.0F, 1.75F, and 3.0F, which correspond to mMinScale, mMidScale, and mMaxScale respectively.

@Override
public boolean onDoubleTap(MotionEvent ev) {
    try {
        float scale = getScale();
        float x = ev.getX();
        float y = ev.getY();
        if (scale < getMediumScale()) {
            setScale(getMediumScale(), x, y, true);
        } else if (scale >= getMediumScale() && scale < getMaximumScale()) {
            setScale(getMaximumScale(), x, y, true);
        } else {
            setScale(getMinimumScale(), x, y, true); }}catch (ArrayIndexOutOfBoundsException e) {
        // Can sometimes happen when getX() and getY() is called
    }
    return true;
}

public float getScale(a) {
    return (float) Math.sqrt((float) Math.pow(getValue(mSuppMatrix, Matrix.MSCALE_X), 2) + (float) Math.pow
        (getValue(mSuppMatrix, Matrix.MSKEW_Y), 2));
}

private float getValue(Matrix matrix, int whichValue) {
    matrix.getValues(mMatrixValues);
    returnmMatrixValues[whichValue]; } # Matrix classpublic void getValues(float[] values) {
    if (values.length < 9) {
        throw new ArrayIndexOutOfBoundsException();
    }
    nGetValues(native_instance, values);
}
Copy the code

There is a bug in the getScale() method in the source code. Matrix.mskew_y should be replaced with matrix.mscale_y. Pay attention to the Matrix. The method getValues may throw ArrayIndexOutOfBoundsException abnormalities, therefore onDoubleTap catch it. Let’s move on to the setScale method.

public void setScale(float scale) {
    setScale(scale, false);
}

public void setScale(float scale, boolean animate) {
    setScale(scale,
        (mImageView.getRight()) / 2,
        (mImageView.getBottom()) / 2,
        animate
    );
}

public void setScale(float scale, float focalX, float focalY, boolean animate) {
    // Check to see if the scale is within bounds
    if (scale < mMinScale || scale > mMaxScale) {
        throw new IllegalArgumentException("Scale must be within the range of minScale and maxScale");
    }
    if (animate) {
        mImageView.post(new AnimatedZoomRunnable(getScale(), scale, focalX, focalY));
    } else{ mSuppMatrix.setScale(scale, scale, focalX, focalY); checkAndDisplayMatrix(); }}Copy the code

Here the zoom center is the ImageView midpoint, double click to zoom out the AnimatedZoomRunnable.

private class AnimatedZoomRunnable implements Runnable {

    private final float mFocalX, mFocalY;
    private final long mStartTime;
    private final float mZoomStart, mZoomEnd;

    public AnimatedZoomRunnable(final float currentZoom, final float targetZoom,
        final float focalX, final float focalY) {
        mFocalX = focalX;
        mFocalY = focalY;
        mStartTime = System.currentTimeMillis();
        mZoomStart = currentZoom;
        mZoomEnd = targetZoom;
    }

    @Override
    public void run(a) {
        float t = interpolate();
        float scale = mZoomStart + t * (mZoomEnd - mZoomStart);
        float deltaScale = scale / getScale();
        onGestureListener.onScale(deltaScale, mFocalX, mFocalY);
        // We haven't hit our target scale yet, so post ourselves again
        if (t < 1f) {
            Compat.postOnAnimation(mImageView, this); }}private float interpolate(a) {
        float t = 1f * (System.currentTimeMillis() - mStartTime) / mZoomDuration;
        t = Math.min(1f, t);
        t = mInterpolator.getInterpolation(t);
        returnt; }}Copy the code

The zoom animation mainly USES a AccelerateDecelerateInterpolator, deceleration interpolation, based on the current animation execution time ratio to get the current interpolation (0-1), and accordingly to get the corresponding scaling, Go OnGestureListener callback to perform scaling.

@Override
public void onScale(float scaleFactor, float focusX, float focusY) {
    onScale(scaleFactor, focusX, focusY, 0.0);
}

@Override
public void onScale(float scaleFactor, float focusX, float focusY, float dx, float dy) {
    if (getScale() < mMaxScale || scaleFactor < 1f) {
        if(mScaleChangeListener ! =null) { mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY); } mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY); mSuppMatrix.postTranslate(dx, dy); checkAndDisplayMatrix(); }}Copy the code

CheckAndDisplayMatrix (); checkAndDisplayMatrix(); checkAndDisplayMatrix()

private void checkAndDisplayMatrix(a) {
    if(checkMatrixBounds()) { setImageViewMatrix(getDrawMatrix()); }}private boolean checkMatrixBounds(a) {
    final RectF rect = getDisplayRect(getDrawMatrix());
    if (rect == null) {
        return false;
    }
    final float height = rect.height(), width = rect.width();
    float deltaX = 0, deltaY = 0;
    final int viewHeight = getImageViewHeight(mImageView);
    if (height <= viewHeight) {
        switch (mScaleType) {
            case FIT_START:
                deltaY = -rect.top;
                break;
            case FIT_END:
                deltaY = viewHeight - height - rect.top;
                break;
            default:
                deltaY = (viewHeight - height) / 2 - rect.top;
                break;
        }
        mVerticalScrollEdge = VERTICAL_EDGE_BOTH;
    } else if (rect.top > 0) {
        mVerticalScrollEdge = VERTICAL_EDGE_TOP;
        deltaY = -rect.top;
    } else if (rect.bottom < viewHeight) {
        mVerticalScrollEdge = VERTICAL_EDGE_BOTTOM;
        deltaY = viewHeight - rect.bottom;
    } else {
        mVerticalScrollEdge = VERTICAL_EDGE_NONE;
    }
    final int viewWidth = getImageViewWidth(mImageView);
    if (width <= viewWidth) {
        switch (mScaleType) {
            case FIT_START:
                deltaX = -rect.left;
                break;
            case FIT_END:
                deltaX = viewWidth - width - rect.left;
                break;
            default:
                deltaX = (viewWidth - width) / 2 - rect.left;
                break;
        }
        mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH;
    } else if (rect.left > 0) {
        mHorizontalScrollEdge = HORIZONTAL_EDGE_LEFT;
        deltaX = -rect.left;
    } else if (rect.right < viewWidth) {
        deltaX = viewWidth - rect.right;
        mHorizontalScrollEdge = HORIZONTAL_EDGE_RIGHT;
    } else {
        mHorizontalScrollEdge = HORIZONTAL_EDGE_NONE;
    }
    // Finally actually translate the matrix
    mSuppMatrix.postTranslate(deltaX, deltaY);
    return true;
}

private RectF getDisplayRect(Matrix matrix) {
    Drawable d = mImageView.getDrawable();
    if(d ! =null) {
        mDisplayRect.set(0.0, d.getIntrinsicWidth(),
            d.getIntrinsicHeight());
        matrix.mapRect(mDisplayRect);
        return mDisplayRect;
    }
    return null;
}
Copy the code

CheckMatrixBounds is a core method of this class that is used to correct deviations. GetDisplayRect performs a transform on the current drawable boundary, and the transform matrix is the mDrawMatrix. After taking the display area, it compares it to the ImageView area and pulls back the part that is beyond the boundary. The location of the pull back will refer to the ScaleType. Here, there is only displacement transformation. MHorizontalScrollEdge and mVerticalScrollEdge mainly record which boundary the current gesture operation matrix needs to correct. The onScale method is the same as above.

2.2 drag

@Override
public void onDrag(float dx, float dy) {
    if (mScaleDragDetector.isScaling()) {
        return; // Do not drag if we are already scaling
    }
    if(mOnViewDragListener ! =null) {
        mOnViewDragListener.onDrag(dx, dy);
    }
    mSuppMatrix.postTranslate(dx, dy);
    checkAndDisplayMatrix();

    /* * Here we decide whether to let the ImageView's parent to start taking * over the touch event. * * First we check whether this function is enabled. We never want the * parent to take over if we're scaling. We then check the edge we're  * on, and the direction of the scroll (i.e. if we're pulling against * the edge, aka 'overscrolling', let the parent take over). */
    ViewParent parent = mImageView.getParent();
    if(mAllowParentInterceptOnEdge && ! mScaleDragDetector.isScaling() && ! mBlockParentIntercept) {if (mHorizontalScrollEdge == HORIZONTAL_EDGE_BOTH
                || (mHorizontalScrollEdge == HORIZONTAL_EDGE_LEFT && dx >= 1f)
                || (mHorizontalScrollEdge == HORIZONTAL_EDGE_RIGHT && dx <= -1f)
                || (mVerticalScrollEdge == VERTICAL_EDGE_TOP && dy >= 1f)
                || (mVerticalScrollEdge == VERTICAL_EDGE_BOTTOM && dy <= -1f)) {
            if(parent ! =null) {
                parent.requestDisallowInterceptTouchEvent(false); }}}else {
        if(parent ! =null) {
            parent.requestDisallowInterceptTouchEvent(true); }}}Copy the code

The main front is to move, to correct the boundary, checkAndDisplayMatrix will be finished to mHorizontalScrollEdge, mVerticalScrollEdge, If the boundary is corrected and the shift is out of the boundary, the request parent layout interception event is triggered and is not passed down.

2.3 Fling

@Override
public void onFling(float startX, float startY, float velocityX, float velocityY) {
    mCurrentFlingRunnable = new FlingRunnable(mImageView.getContext());
    mCurrentFlingRunnable.fling(getImageViewWidth(mImageView),
        getImageViewHeight(mImageView), (int) velocityX, (int) velocityY);
    mImageView.post(mCurrentFlingRunnable);
}
Copy the code

Similar to double click to zoom, there’s one defined hereFlingRunnableAnimatedZoomRunnableIs dependent on a acceleration and deceleration interpolator,FlingRunnableIt depends on oneOverScrollerClass, the inner position of the scroll is actually updated with the help of an interpolator, which is an inner classViscousFluidInterpolatorI wrote a little demo.





hereflingWithout using this interpolator,FlingRunnableflingMethod is actually calledOverScrollerflingMethods. The entire process is: After the fling invocation,OverScrollerA current time is logged, called latercomputeScrollOffsetAccording to the time difference, the current speed and sliding distance are calculated and the current position is recorded. Here to seerunMethods.

@Override
public void run(a) {
    if (mScroller.isFinished()) {
        return; // remaining post that should not be handled
    }
    if (mScroller.computeScrollOffset()) {
        final int newX = mScroller.getCurrX();
        final int newY = mScroller.getCurrY();
        mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY);
        checkAndDisplayMatrix();
        mCurrentX = newX;
        mCurrentY = newY;
        // Post On animation
        Compat.postOnAnimation(mImageView, this); }}Copy the code

ComputeScrollOffset () is first called to update the current position, and then getCurrX() and getCurrY() can be used to get the position. Then update it to the mSuppMatrix and display it, firing the Runnable over and over until the fling stops. MBaseMatrix (Drawable Drawable) is an update to the updateBaseMatrix(Drawable Drawable) method, which is used to scale and move the Drawable based on the ScaleType. At this point, the core logic of PhotoView has been analyzed.