图片查看器 PhotoView

一.使图片能够全部显示在自定义 View 中

1.自定义 View

采用自定义View继承AppCompatImageView,重写构造方法:

public class PhotoView extends AppCompatImageView {
    public PhotoView(Context context) {
        this(context, null);
    }

    public PhotoView(Context context, AttributeSet attrs) {
        super(context, attrs);
        if (isInEditMode()) {
            return;
        }
        init(context);
    }
}
复制代码

2.获取图片的宽高

首先需要将图片在我们的控件中完全显示。因为图片的尺寸不固定,这里需要获取图片的尺寸与我们控件的宽高做比较,然后对图片进行缩放。要获取控件的宽高,我们需要在控制回掉完 onLayout 方法之后在对应的方法中获取控件的宽高,这里采用让控件监听 View.OnLayoutChangeListener 接口:

public class PhotoView extends AppCompatImageView implements View.OnLayoutChangeListener{}
复制代码

在 onLayoutChange 方法中监听布局的改变:

@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
    //判断布局是否发生变化
    if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) {
        updateBaseMatrix(getDrawable());//更新ImageView设置的drawable的矩阵
    }
}
复制代码

在构造方法中开启监听器:

private void init(Context context) {
    addOnLayoutChangeListener(this);
}
复制代码

这样子就可以拿到控件对应的宽高。

3.根据 View 的宽高将图片进行缩放

因为需要将图片在我们的控件中完全显示,在控件的宽高和图片不匹配时,需要对图片进行缩放:

//更新drawable对应的矩阵
private void updateBaseMatrix(Drawable drawable) {
    if (drawable == null) {
        return;
    }
    //获取ImageView和设置的drawable的宽高
    float viewWidth = getImageViewWidth();
    float viewHeight = getImageViewHeight();
    int drawableWidth = drawable.getIntrinsicWidth();
    int drawableHeight = drawable.getIntrinsicHeight();

    mBaseMatrix.reset();//重置显示图片矩阵

    //获取将drawable缩放的宽高
    float widthScale = viewWidth / drawableWidth;
    float heightScale = viewHeight / drawableHeight;

    //根据ImageView设置的ScalyType设置drawable的缩放类型
    switch (mScaleType) {
        case CENTER: {
            //将drawable的中心移动到ImageView的中心
            mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2.0f, (viewHeight - drawableHeight) / 2.0f);
            break;
        }
        case CENTER_CROP: {
            //将缩放后的drawable放大至填充满整个ImageView,然后将缩放后的drawable的中心移动到ImageView的中心
            float scale = Math.max(widthScale, heightScale);
            mBaseMatrix.postScale(scale, scale);
            mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2.0f, (viewHeight - drawableHeight * scale) / 2.0f);
            break;
        }
        case CENTER_INSIDE: {
            //将原本大小的drawable设置给ImageView,如果drawable大于ImageView,则缩小,然后将缩放后的drawable的中心移动到ImageView的中心
            float scale = Math.min(1.0f, Math.min(widthScale, heightScale));
            mBaseMatrix.postScale(scale, scale);
            mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2.0f, (viewHeight - drawableHeight * scale) / 2.0f);
            break;
        }
        default:
            break;
    }

    RectF tempDst = new RectF(0, 0, viewWidth, viewHeight);
    RectF tempSrc = new RectF(0, 0, drawableWidth, drawableHeight);

    switch (mScaleType) {
        case FIT_CENTER: {
            mBaseMatrix.setRectToRect(tempSrc, tempDst, Matrix.ScaleToFit.CENTER);
            break;
        }
        case FIT_START: {
            mBaseMatrix.setRectToRect(tempSrc, tempDst, Matrix.ScaleToFit.START);
            break;
        }
        case FIT_END: {
            mBaseMatrix.setRectToRect(tempSrc, tempDst, Matrix.ScaleToFit.END);
            break;
        }
        case FIT_XY: {
            mBaseMatrix.setRectToRect(tempSrc, tempDst, Matrix.ScaleToFit.FILL);
            break;
        }
    }

    resetMatrix();
}

private void resetMatrix() {
    mSuppMatrix.reset();//重置我们操作图片过程中实用的矩阵
    setImageMatrix(getDrawMatrix());//设置图片的矩阵
    checkMatrixBounds();//检查经过我们操作后图片是否超出边界
}

private Matrix getDrawMatrix() {
    mDrawMatrix.set(mBaseMatrix);//首先将图片的矩阵设置为源矩阵
    mDrawMatrix.postConcat(mSuppMatrix);//在源矩阵的基础上加上我们的计算矩阵
    return mDrawMatrix;
}

//检测当前 Drawable 的矩阵是否在正确的显示范围
private boolean checkMatrixBounds() {
    RectF rectF = getDisplayRect(getDrawMatrix());//将矩阵转换为矩形
    if (rectF == null) {
        return false;
    }
    float height = rectF.height();
    float width = rectF.width();
    float deltaX = 0;
    float deltaY = 0;

    //处理drawable的高
    int viewHeight = getImageViewHeight();
    if (height <= viewHeight) {//如果drawable的高度小于ImageView的高度
        switch (mScaleType) {
            case FIT_START: {
                deltaY = -rectF.top;//需要向上偏移
                break;
            }
            case FIT_END: {
                deltaY = viewHeight - height - rectF.top;//需要向下偏移
                break;
            }
            default: {
                deltaY = (viewHeight - height) / 2 - rectF.top;//居中处理
                break;
            }
        }
    } else if (rectF.top > 0) {//置顶
        deltaY = -rectF.top;
    } else if (rectF.bottom < viewHeight) {//置底
        deltaY = viewHeight - rectF.bottom;
    }

    //处理drawable的宽
    int viewWidth = getImageViewWidth();
    if (width <= viewWidth) {
        switch (mScaleType) {
            case FIT_START: {
                deltaX = -rectF.left;//向左偏移
                break;
            }
            case FIT_END: {
                deltaY = viewWidth - width - rectF.left;//向右偏移
                break;
            }
            default: {
                deltaX = (viewWidth - width) / 2 - rectF.left;//居中处理
                break;
            }
        }
        mScrollEdge = EDGE_BOTH;
    } else if (rectF.left > 0) {
        mScrollEdge = EDGE_LEFT;
        deltaX = -rectF.left;
    } else if (rectF.right < viewWidth) {
        mScrollEdge = EDGE_RIGHT;
        deltaX = viewWidth - rectF.right;
    } else {
        mScrollEdge = EDGE_NONE;
    }

    //将需要平移的x,y保存在矩阵中
    mSuppMatrix.postTranslate(deltaX, deltaY);
    return true;
}

private RectF getDisplayRect(Matrix matrix) {
    Drawable drawable = getDrawable();//获取图片
    if (drawable != null) {
        mDisplayRect.set(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());//构造矩形
        matrix.mapRect(mDisplayRect);//测量rect并将测量结果放入rect中
        return mDisplayRect;
    }
    return null;
}
复制代码

经过处理后,图片就可以按照我们设置的ScaleType显示在ImageView中。比如 ScaleType.FIT_CENTER 模式:

PhotoView photoView = new PhotoView(this);;
photoView.setImageResource(R.mipmap.pos0);
setContentView(photoView);
复制代码

双指放大缩小图片

首先需要让自定义 View 能够响应触摸事件,我们需要实现 View.OnTouchListener 接口:

public class PhotoView extends AppCompatImageView implements View.OnLayoutChangeListener, View.OnTouchListener {}
复制代码

并实现 onTouch 方法:

@Override
    public boolean onTouch(View v, MotionEvent event) {
        boolean handled = false;
        if (getDrawable() != null) {
            switch (event.getActionMasked()) {
                case MotionEvent.ACTION_DOWN: {
                    ViewParent parent = getParent();
                    if (parent != null) { parent.requestDisallowInterceptTouchEvent(true);//禁止父视图拦截接下来的事件,默认 DOWN 事件不能被拦截
                    }
                    break;
                }
            }
            //分发给 ScaleGestureDetector 识别缩放事件
            if (mScaleDragDetector != null) {
                handled = mScaleDragDetector.onTouchEvent(event);//传递识别缩放事件
            }
        }
        return handled;
    }
复制代码

我们需要获取到设备捕捉到的缩放比例,这里需要借用到系统提供的 ScaleGestureDetector 类。

ScaleGestureDetector mScaleDragDetector = new ScaleGestureDetector(context, mScaleGestureListener);
复制代码

构造函数中需要传入当前上下文 Context 和手势缩放监听器,这里采用 ScaleGestureDetector.OnScaleGestureListener 接口的实现类 ScaleGestureDetector.SimpleOnScaleGestureListener 并实现它的 onScale 方法:

private final ScaleGestureDetector.OnScaleGestureListener mScaleGestureListener = new ScaleGestureDetector.SimpleOnScaleGestureListener() {

    @Override
    public boolean onScale(ScaleGestureDetector detector) {

        float scaleFactor = detector.getScaleFactor();
        // 将对象限制在指定范围内,不能太大也不能太小
        scaleFactor = Math.max(MIN_SCALE_FACTOR, Math.min(scaleFactor, MAX_SCALE_FACTOR));

        if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor)) {
            return false;
        }
        
        if ((getScale() < (mMaxScale + mOverScaleCoefficient) || scaleFactor < 1f)//当前缩放系数小于最大缩放值,或者当前想要缩小
        && (getScale() > (mMinScale - mOverScaleCoefficient) || scaleFactor > 1f)) {//当前缩放系数大于最小缩放值,或者当前想要放大
            mSuppMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());
            checkAndDisplayMatrix();
        }

        return true;
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        return super.onScaleBegin(detector);
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {
        super.onScaleEnd(detector);
    }
};

private float getScale() {//获取当前需要操作需要的缩放值
    float currentScale = (float) Math.sqrt(
            (float) Math.pow(getValue(mSuppMatrix, Matrix.MSCALE_X), 2) +
            (float) Math.pow(getValue(mSuppMatrix, Matrix.MSKEW_Y), 2)
    );
    return currentScale;
}

private float getValue(Matrix matrix, int whichValue) {//获取矩阵指定位置的值
    matrix.getValues(mMatrixValues);
    return mMatrixValues[whichValue];
}
复制代码

mMaxScale 和 mOverScaleCoefficient 是事先确定的缩放边界值:

private float mOverScaleCoefficient = 0.5f;//缩放系数

private float mMinScale = DEFAULT_MIN_SCALE;//最小缩放比例
private float mMidScale = DEFAULT_MID_SCALE;//中等缩放比例
private float mMaxScale = DEFAULT_MAX_SCALE;//最大缩放比例

private final static float DEFAULT_MAX_SCALE = 3.0f;
private final static float DEFAULT_MID_SCALE = 1.75f;
private final static float DEFAULT_MIN_SCALE = 1.0f;
复制代码

在给计算矩阵设置完需要缩放的值之后需要检查我们的操作是否超出了 ImageView 的范围:

checkAndDisplayMatrix();

private void checkAndDisplayMatrix() {
    if (checkMatrixBounds()) {
        setImageMatrix(getDrawMatrix());
    }
}
复制代码

然后给 ImageView 设置图片矩阵。

双击缩放图片

首先需要检测双击事件。这里需要用到系统提供的接口 GestureDetector.SimpleOnGestureListener。然后使用这个接口的实现类 GestureDetector.SimpleOnGestureListener。重写 onDoubleTap 方法:

private GestureDetector.SimpleOnGestureListener mOnGestureListener = new GestureDetector.SimpleOnGestureListener() {
    @Override
    public boolean onDoubleTap(MotionEvent e) {
        float scale = getScale();//获取当前图片的缩放值
        float x = e.getX();
        float y = e.getY();

        //根据缩放值极致设置新的缩放值
        if (scale < mMidScale) {
            setScale(mMidScale, x, y, true);
        } else if (scale < mMaxScale) {
            setScale(mMaxScale, x, y, true);
        } else {
            setScale(mMinScale, x, y, true);
        }
        return true;//消费该事件
    }
};

void setScale(float scale, float focalX, float focalY, boolean animate) {
    if (scale < mMinScale || scale > mMaxScale) {//缩放值超出范围
        throw new IllegalArgumentException("Scale must be within the range of minScale and maxScale");
    }

    mSuppMatrix.setScale(scale, scale, focalX, focalY);//将该事件设置给我们的计算矩阵
    checkAndDisplayMatrix();//检测矩阵的合法性
}
复制代码

然后在自定义 View 的 onTouch 方法中将 MotionEvent 对象传递给缩放器的 onTouchEvent 方法:

//分发给 GestureDetector 识别双击事件
if (mGestureDetector != null && mGestureDetector.onTouchEvent(event)) {
    handled = true;
}
复制代码

在图片放大情况下,超出 ImageView 时,可以进行滑动查看图片

需要进行滑动,我们需要监听 onTouch 方法,根据 MotionEvent 的不同状态进行处理:

switch (MotionEventCompat.getActionMasked(event)) {
    case MotionEvent.ACTION_DOWN:
        //记录点击时的宽高
        mLastTouchX = getActiveX(event);
        mLastTouchY = getActiveY(event);
        mIsDragging = false;
        break;
    case MotionEvent.ACTION_MOVE:
        final float x = getActiveX(event);
        final float y = getActiveY(event);
        //滑动的距离
        final float dx = x - mLastTouchX;
        final float dy = y - mLastTouchY;
        if (!mIsDragging) {
            //计算手指滑动的距离有没有超过临界值
            mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop;
        }
        if (mIsDragging) {
            mSuppMatrix.postTranslate(dx, dy);//将滑动状态设置给计算矩阵
            checkAndDisplayMatrix();//检查矩阵的合法性
            //更新坐标值
            mLastTouchX = x;
            mLastTouchY = y;
        }
        break;
}
复制代码

mTouchSlop 是设备默认承认的最小滑动值。

final ViewConfiguration configuration = ViewConfiguration.get(context);
mTouchSlop = configuration.getScaledTouchSlop();
复制代码

在手指抬起时进行fling操作

final ViewConfiguration configuration = ViewConfiguration.get(context);
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
...
switch (MotionEventCompat.getActionMasked(event)) {
    case MotionEvent.ACTION_UP:
        mActivePointerId = INVALID_POINTER_ID;
        if (mIsDragging) {
            if (mVelocityTracker != null) {
                mLastTouchX = getActiveX(event);
                mLastTouchY = getActiveY(event);
                //获取速度跟踪器
                mVelocityTracker = VelocityTracker.obtain();
                mVelocityTracker.addMovement(event);
                mVelocityTracker.computeCurrentVelocity(1000);
                //获取速度
                final float vX = mVelocityTracker.getXVelocity();
                final float vY = mVelocityTracker.getYVelocity();
        
                if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) {
                    //速度大于系统最小速度才进行fling操作
                    mCurrentFlingRunnable = new FlingRunnable(getContext());
                    mCurrentFlingRunnable.fling(getImageViewWidth(), getImageViewHeight()
                    , (int) velocityX, (int) velocityY);
                    PhotoView.this.post(mCurrentFlingRunnable);
                }
            }
        }
        
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
        
    break;
}

private class FlingRunnable implements Runnable {
    private OverScroller mScroller;
    private int mCurrentX, mCurrentY;

    public FlingRunnable(Context context) {
        mScroller = new OverScroller(context);
    }

    @Override
    public void run() {
        if (mScroller.isFinished()) {//判断滑动是否结束,结束了直接返回
            return;
        }
        if (mScroller.computeScrollOffset()) {//持续计算滑动过程,没结束返回true
            int newX = mScroller.getCurrX();
            int newY = mScroller.getCurrY();

            //将需要滑动的操作设置给用于计算的矩阵
            mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY);
            checkAndDisplayMatrix();

            mCurrentX = newX;
            mCurrentY = newY;
            PhotoView.this.postOnAnimation(this);
        }
    }

    void fling(int viewWidth, int viewHeight, int velocityX,
               int velocityY) {
        final RectF rectF = getDisplayRect();
        if (rectF == null) {
            return;
        }

        final int startX = Math.round(-rectF.left);
        final int minX, maxX, minY, maxY;

        if (viewWidth < rectF.width()) {
            minX = 0;
            maxX = Math.round(rectF.width() - viewWidth);
        } else {
            //视图的宽更大,不可拖动
            minX = maxX = startX;
        }

        final int startY = Math.round(-rectF.top);
        if (viewHeight < rectF.height()) {
            minY = 0;
            maxY = Math.round(rectF.height() - viewHeight);
        } else {
            //视图的高更大,不可拖动
            minY = maxY = startY;
        }

        mCurrentX = startX;
        mCurrentY = startY;

        if (startX != maxX || startY != maxY) {
            mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
        }
    }

    void cancelFling() {
        mScroller.forceFinished(true);
    }
}
复制代码

自此完成了简单的图片查看器 PhotoView。 https://github.com/jackzhengpinwen/kiwiViews/tree/master/app/src/main/java/com/zpw/views/exercise11

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值