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

Posted Jadyli1

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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))android 图解 PhotoView,从‘百草园’到‘三味书屋’!

Android -- 开源库PhotoView 的基本使用

Android PhotoView基本功能实现

Android 大图片预览ViewPager

Android图片处理--缩放

Android图片处理的一些总结