Android图片处理二:PhotoView源码解析

PhotoView 是一个用于处理图片手势的控件,其源码设计很不错,高内聚低耦合,值得我们深入学习下。

1 基本结构


PhotoView 类代码很简单,看下构造就行了。

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

private void init() {
    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;
    }
}

初始化了一个 PhotoViewAttacher 类,把 ScaleType 设置为 ScaleType.MATRIX ,因为 PhotoView 的手势操作都是通过设置 matrix 生效的。


PhotoView 的核心代码都在 PhotoViewAttacher 中,PhotoViewAttacther 可以看做是 PhotoView 的一个代理。先从 PhotoViewAttacher 的构造看起。

public PhotoViewAttacher(ImageView imageView) {
    mImageView = imageView;
    imageView.setOnTouchListener(this);
    imageView.addOnLayoutChangeListener(this);
    if (imageView.isInEditMode()) {
        return;
    }
    mBaseRotation = 0.0f;
    // 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) {
        }
    });
}

构造函数的参数是一个 ImageView ,在这个类里主要用途是获取 ImageView 的边界,获取 drawable ,更新 ImageView 的矩阵,作为回调参数等。接着设置 **OnTouchListener** ,触摸处理都是在它的回调 boolean onTouch(View v, MotionEvent ev) 方法中做的, OnLayoutChangeListener 主要用于在外面布局发生变化的时候更新图片默认的矩阵。


这里说下 isInEditMode() 方法,这个方法是用于 Android Studio 布局编辑器预览的,在预览环境拿到的 contextcom.android.layoutlib.bridge.android.BridgeContext ,这里面的方法获取到的一些对象是和 Android 系统环境不太一样的。还有在 RecyclerView 的源码中我们可以看到这么几行代码:

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();
    }
}


这里可以看到一句注释:Stupid layoutlib cannot handle simple class loaders. ,里面的 layoutlib 应该说的就是 BridgeContext 所在的包。
我们也可以在 onDraw 的时候根据这个方法来在预览环境和真实环境区别绘制。


回到 PhotoViewAttacher 的代码,这里判断 isInEditMode 后就直接返回了,可能是因为预览环境不需要监听触摸事件,也就不会走到相关的方法了。接着初始化了 CustomGestureDetector 类,这里传入了 OnGestureListenerOnGestureListener 是个手势监听回调接口。

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);
}

接着初始化了 GestureDetector ,监听单击、双击、长按、fling 的回调。


我们重点看下 boolean onTouch(View v, MotionEvent ev) 方法。

@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);
            boolean didntScale = !wasScaling && !mScaleDragDetector.isScaling();
            boolean didntDrag = !wasDragging && !mScaleDragDetector.isDragging();
            mBlockParentIntercept = didntScale && didntDrag;
        }
        // Check to see if the user double tapped
        if (mGestureDetector != null && mGestureDetector.onTouchEvent(ev)) {
            handled = true;
        }

    }
    return handled;
}


mZoomEnabled 是外部可设置的属性,只有允许缩放并且 ImageViewdrawable 的情况下才会处理手势操作。在 ACTION_DOWN 时取消 fling ,并且阻止父 View 拦截触摸事件,这里用了 parent.requestDisallowInterceptTouchEvent(true);requestDisallowInterceptTouchEvent(boolean disallowIntercept) 方法在自定义 View 的场景里还是用的挺多的。在 ACTION_CANCELACTION_UP 时校正缩放,把过度缩放的操作通过 Animation 拉回指定范围。


下面就把事件传递给 CustomGestureDetectorGestureDetector 了。

2 手势监听


我们看下具体的手势监听部分。手势一般包括:双击、单击、长按、双指缩放、拖曳、惯性滚动(fling),对于单击、双击、长按、fling,PhotoView 使用了原生的 GestureDetector
来检测,而对于双指缩放、拖曳,则定义了一个 CustomGestureDetector 来处理,注意 CustomGestureDetector 也会处理 fling 事件。


我们主要看下 CustomGestureDetector ,这个类主要处理缩放和拖曳。缩放的检测使用了原生的 ScaleGestureDetector 来处理。ScaleGestureDetector 的构造方法需要传入一个 OnScaleGestureListener 用于回调缩放相关的值。


先看下 ScaleGestureDetector 的集成。首先在构造方法中定义好回调。

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);

这里逻辑很简单,定义了两个成员变量分别记录x轴和y轴的中心点,两次回调的中心点差值就是中心点移动的距离。scaleFactor 是缩放因子,相对于当前大小的缩放比例。


然后在 onTouchEvent 中,把 event 传给 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;
    }
}


这里有个 processTouchEvent ,拖曳就是在里面处理的。在看代码之前,我先讲下多点触控的基本知识。


触摸事件主要涉及到 MotionEvent 类,这个类存储了手指的移动状态,主要包含:

  • ACTION_DOWN 第一个手指按下
  • ACTION_POINTER_DOWN 第一个手指按下后其他手指按下
  • ACTION_POINTER_UP 多个手指长按时抬起其中一个手指,注意松开后还有手指在屏幕上
  • ACTION_UP 最后一个手指抬起
  • ACTION_MOVE 手指移动
  • ACTION_CANCEL 父View收到ACTION_DOWN后会把事件传给子View,如果后续的ACTION_MOVE和ACTION_UP等事件被父View拦截掉,那子View就会收到ACTION_CANCEL事件


可以通过 getAction() 方法获取到一个动作,这里的返回值,对于单指而言,就是动作的状态,含义跟上面这些常量一样,但是如果是多指按下或者抬起,返回值是包含动作的索引的,多指的滑动返回值不包含索引,还是状态。
动作的状态和索引可以分开获取,getActionMasked() 可以只获取状态,getActionIndex() 可以只获取索引。

对于多指操作,要关注两个属性,触摸点id(PointerId)和索引(PointerIndex),触摸点索引可以通过刚刚说的 getActionIndex() 获取到,也可以通过 findPointerIndex(int pointerId) 获取到,触摸点id可以通过 getPointerId(int pointerIndex) 方法来获取,这个方法需要传入触摸点索引。值得注意的是 PointerIdPointerIndex 的取值。
_

  • PointerId 手指按下时生成,手指抬起时回收,注意多点触摸时,抬起任何一个手指,其他手指的 PointerId 不变,PointerId 赋值后不会变更。
  • PointerIndex 手指按下时生成,从0开始计数,多点触摸抬起其中一个手指时,后面的手指 PointerIndex 会更新,取值范围是0~触摸点个数-1。

_
现在来看下代码,这里的 processTouchEvent 有些代码感觉多余了,下面代码是我改过的(不喜勿喷~)


先看下整体结构:

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;
}

这里面的方法和常量上面都讲过了,这里主要讲下这些常量case下的常用操作。

  • ACTION_DOWN 一般会记录触摸点位置,初始化一些变量。
  • ACTION_MOVE 一般会获取当前触摸点位置,跟上次记录的位置取差值,进行缩放、拖动等操作。
  • ACTION_CANCEL 事件中断,重置状态。
  • ACTION_UP 重置状态,如果这时处于拖动的状态,会判断滑动的速度,速度超过一定的值会触发惯性滑动。
  • ACTION_POINTER_UP 多指中的一个手指抬起,需要更新参考触摸点的位置。


再完整地看下代码,先看 ACTION_DOWN 的处理:

mVelocityTracker = VelocityTracker.obtain();
if (null != mVelocityTracker) {
    mVelocityTracker.addMovement(ev);
}

mLastTouchX = ev.getX();
mLastTouchY = ev.getY();
mIsDragging = false;

首先,初始化 VelocityTrackerVelocityTracker 是一个速度检测类,内部存了个 SynchronizedPoolobtain() 方法会优先从池子里取 VelocityTracker 的实例,取不到再创建。addMovement 用于跟踪移动事件,一般会在 ACTION_DOWNACTION_MOVEACTION_UP 中调用。然后记录事件的x、y坐标,获取事件坐标有两种方法,一种是无参数的 float getX() ,这个方法获取的是索引为0的点的坐标,一种是带参数的 float getX(int pointerIndex) ,这个需要传入索引值,用于多指操作,这里是第一个手指按下,我觉得使用无参数的就够了。


继续看 ACTION_MOVE 事件。

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);
    }
}

首先得出移动距离dx、dy,这个距离用于拖动手势,刚刚 ACTION_DOWN 事件中把 mIsDragging 初始化为false,这里是否拖动的判断条件是滑动距离大于最小滑动距离,这里的最小滑动距离在构造函数中已经赋值了:

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

getScaledTouchSlop 是按根据设备密度(density)来获取的最小滑动距离,默认是 8dp<dimen name="config_viewConfigurationTouchSlop">8dp</dimen>) 。


如果当前可以拖动,则会触发拖动回调,并且记录当前x、y坐标,给 VelocityTracker 添加事件。

注意回调 onFling 方法时速度加了负号,因为这个回调是给 OverScroller 用的,OverScroller 的坐标系(向左向上为正)跟正常的坐标系(向右向下为正)是反的。


继续看 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;
}


这里主要处理松开手后的惯性滑动以及释放 VelocityTracker ,判断是否要惯性滑动,要看 x 轴和 y 轴的速度,VelocityTracker 在获取速度前要先调用 computeCurrentVelocity(int units) 计算速度,computeCurrentVelocity(int units) 方法的参数是单位,1表示1ms,1000表示1s,这个值决定了下面 getXVelocity()getYVelocity() 的单位,如果传入的是1000,那速度单位就是 px/s ,只要 x 轴或者 y 轴有任何一个大于最小速度的,就会触发惯性滑动。这个最小速度跟上面的 TouchSlop 类似,也是从 ViewConfiguration 中获取的:

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

默认值是50dp。


ACTION_UP 事件的最后,释放 VelocityTracker


继续看 ACTION_POINTER_UP 事件。

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

多指触摸抬起其中一个手指,因为 mLastTouchX 在之前一直存的是第一个手指的坐标,所以这里只要判断是不是第一个手指抬起,如果是第一个手指抬起,就更新到下一个手指的坐标。


至此核心的触摸事件捕获看完了,主要处理了拖动和惯性滑动。

2 手势处理


在讲具体处理之前,先看下三个基本变量,mBaseMatrixmSuppMatrixmDrawMatrix

  • mBaseMatrix 基础矩阵,记录的是图片根据 ScaleType 缩放移动到适应 ImageView 的变化,不记录手势操作
  • mSuppMatrix 额外矩阵,记录的是手势操作
  • mDrawMatrix 实际设置给 ImageView 的矩阵,由 mBaseMatrixmSuppMatrix 相乘得到


在给 ImageView 设置矩阵和获取边界时,是要用 mDrawMatrix 的。

2.1 缩放


缩放分为双击缩放和多指缩放。先看下双击缩放的回调处理。


PhotoView 定义了三档默认缩放大小,1.0f、1.75f、3.0f,分别对应 mMinScalemMidScalemMaxScale ,下面看下是怎么切换的。

@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() {
    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);
    return mMatrixValues[whichValue];
}

# Matrix类
public void getValues(float[] values) {
    if (values.length < 9) {
        throw new ArrayIndexOutOfBoundsException();
    }
    nGetValues(native_instance, values);
}


首先去拿了存在 mSuppMatrix 里的缩放比例,这里源码里的 getScale() 方法有个 bug ,Matrix.MSKEW_Y 应该换成 Matrix.MSCALE_Y ,注意 Matrix.getValues 方法可能会抛出 ArrayIndexOutOfBoundsException 异常,所以 onDoubleTapcatch 了一下。继续看 setScale 方法。

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();
    }
}

这里缩放中心点取的是 ImageView 的中点,双击缩放走的是 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() {
        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() {
        float t = 1f * (System.currentTimeMillis() - mStartTime) / mZoomDuration;
        t = Math.min(1f, t);
        t = mInterpolator.getInterpolation(t);
        return t;
    }
}


这个缩放动画主要用到了一个 AccelerateDecelerateInterpolator ,加减速插值器,根据当前动画执行时间占比去拿到当前的插值(0-1),再据此拿到对应的缩放比例,走 OnGestureListener 回调执行缩放。

@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();
    }
}


矩阵的相关知识在之前的文章讲过了,不懂的可以去复习下:Android图片处理一:Matrix与手势链接
这里重点看 checkAndDisplayMatrix()

private void checkAndDisplayMatrix() {
    if (checkMatrixBounds()) {
        setImageViewMatrix(getDrawMatrix());
    }
}

private boolean checkMatrixBounds() {
    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;
}

checkMatrixBounds 算是这个类的一个核心方法了,用于矫正偏差。getDisplayRect 会对当前的 drawable 边界执行变换,变换矩阵就是前文说的 mDrawMatrix ,拿到了显示区域后会跟 ImageView 区域对比,把超出边界的部分拉回去,拉回去的位置会参考 ScaleType ,这里只有位移变换,mHorizontalScrollEdgemVerticalScrollEdge 主要记录当前的手势操作 matrix 需要往哪个边界矫正。


多指缩放最后回调的也是 onScale 方法,处理跟上面一样。

2.2 拖动
@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);
        }
    }
}

前面主要是移动,矫正边界, checkAndDisplayMatrix 走完会对 mHorizontalScrollEdgemVerticalScrollEdge 赋值,如果矫正边界了并且位移是往超出边界的方向就会触发请求父布局拦截事件,不再传递下来。

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);
}

跟双击缩放类似,这里定义了一个 FlingRunnableAnimatedZoomRunnable 的插值依赖了一个加减速插值器, FlingRunnable 则依赖了一个 OverScroller 类,滚动时其内部位置的更新其实也是借助了插值器实现,插值器是个内部类 ViscousFluidInterpolator ,变化图形我写了个demo演示。


image.png
这里 fling 没有用到这个插值器, FlingRunnablefling 方法其实是调用了 OverScrollerfling 方法。整个流程是:fling调用后,OverScroller 会记录一个当前时间,后面调用 computeScrollOffset 时,会计算出时间差,根据时间差计算出当前速度和滑动距离,记录当前位置。这里看下 run 方法。

@Override
public void run() {
    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);
    }
}

首先调用 computeScrollOffset() 更新当前的位置,后面就可以使用 getCurrX()getCurrY() 来获取位置了。然后更新到 mSuppMatrix 上并展示,不断触发这个 Runnable 直到 fling 停止。


mBaseMatrix 的更新在 updateBaseMatrix(Drawable drawable) 方法中,主要是根据 ScaleType 来调整 Drawable 的缩放和移动,有了前面的详细讲解,这里应该很容易看懂,就不细说了。


至此,PhotoView 的核心逻辑都分析完了。

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值