当前位置: 首页 > 工具软件 > PhotoView > 使用案例 >

自定义PhotoView

李开宇
2023-12-01

自定义PhotoView

支持单指滑动、双指缩放/旋转、边界超出判定、缩放过度判定、旋转自动回正,下面直接贴完整代码
目前无法做到放大过度回弹时,图片的位置保持在两指之间的中心位置,如果有好的想法,欢迎提出。

Android 自定义PhotoView演示视频

package com.example.myapplication.photoView;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.ScaleGestureDetector.OnScaleGestureListener;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.example.myapplication.LogUtil;

public class MyPhotoView2 extends androidx.appcompat.widget.AppCompatImageView {

    private final GestureDetector mGestureDetector;
    private final ScaleGestureDetector mScaleGestureDetector;
    private final RotateGestureDetector mRotateGestureDetector;

    private Matrix mMatrix;

    private float mPreScaleFactor = 1.0f;
    private float mBaseScale;
    private float mMaxScale;

    private float viewCenterX;
    private float viewCenterY;

    private float frameViewLeft;
    private float frameViewTop;
    private float frameViewRight;
    private float frameViewBottom;

    private static final int MAX_SCROLL_FACTOR = 3;
    private static final float DAMP_FACTOR = 9.0f;  // 阻尼因子

    private boolean isFirstLayout = true;

    private int mState;
    private static final int STATE_NONE = 0;
    private static final int STATE_DRAG = 1;
    private static final int STATE_ZOOM = 2;

    private float currentImageRotate = 0f; // 图片当前的旋转角度


    public MyPhotoView2(@NonNull Context context) {
        this(context, null);
    }

    public MyPhotoView2(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyPhotoView2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setScaleType(ScaleType.MATRIX);
        mMatrix = new Matrix();
        mGestureDetector = new GestureDetector(context, mSimpleOnGestureListener);
        mScaleGestureDetector = new ScaleGestureDetector(context, mOnScaleGestureListener);
        mRotateGestureDetector = new RotateGestureDetector(context, mSimpleRotateGestureListener);

    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (!isFirstLayout) return ;
        isFirstLayout = false;
        viewCenterX = (float) getWidth() / 2;
        viewCenterY = (float) getHeight() / 2;
        autoFillView();
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                if (mState == STATE_DRAG) {
                    checkBound();
                }
                break;
        }

        if (mGestureDetector.onTouchEvent(event)) {
            return true;
        }
        mScaleGestureDetector.onTouchEvent(event);
        mRotateGestureDetector.onTouchEvent(event);
        return true;
    }

    private void autoFillView() {

        Drawable drawable = getDrawable();
        if (drawable == null) return ;

        int viewWidth = getWidth();
        int viewHeight = getHeight();
        int drawableWidth = drawable.getIntrinsicWidth();
        int drawableHeight = drawable.getIntrinsicHeight();

        float scale = Math.min((float) viewWidth / drawableWidth, (float) viewHeight / drawableHeight);
        mBaseScale = scale;
        mMaxScale = mBaseScale + 1f;

        float dy = (viewHeight - drawableHeight * scale) / 2;

        Matrix newMatrix = new Matrix();
        newMatrix.setScale(scale, scale);
        newMatrix.postTranslate(0, dy);
        setImageMatrix(newMatrix);
        mMatrix = newMatrix;

        setFrameView();
    }

    private final GestureDetector.SimpleOnGestureListener mSimpleOnGestureListener = new GestureDetector.SimpleOnGestureListener() {
        @Override
        public boolean onDown(MotionEvent e) {
            return true;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            if (e1.getPointerCount() == e2.getPointerCount() && e1.getPointerCount() == 1) {

                mState = STATE_DRAG;

                RectF rectF = getMatrixRectF();
                float leftEdgeDistanceLeft = rectF.left - frameViewLeft;
                float topEdgeDistanceTop = rectF.top - frameViewTop;
                float rightEdgeDistanceRight = rectF.right - frameViewRight;
                float bottomEdgeDistanceBottom = rectF.bottom - frameViewBottom;

                int maxOffsetX = getWidth() / MAX_SCROLL_FACTOR;
                int maxOffsetY = getHeight() / MAX_SCROLL_FACTOR;

                if (leftEdgeDistanceLeft > 0) {
                    if (distanceX < 0) {
                        if (leftEdgeDistanceLeft < maxOffsetX) {
                            int ration= (int)  (DAMP_FACTOR / maxOffsetX * leftEdgeDistanceLeft) + 1;
                            distanceX /= ration;
                        } else {
                            distanceX = 0;
                        }
                    }
                } else if (rightEdgeDistanceRight < 0) {
                    if (distanceX > 0) {
                        if (rightEdgeDistanceRight > -maxOffsetX) {
                            int ration = (int) (DAMP_FACTOR / maxOffsetX * -rightEdgeDistanceRight) + 1;
                            distanceX /= ration;
                        } else {
                            distanceX = 0;
                        }
                    }
                }
                if (topEdgeDistanceTop > 0) {
                    if (distanceY < 0) {
                        int ration = (int) (DAMP_FACTOR / maxOffsetY * topEdgeDistanceTop) + 1;
                        distanceY /= ration;
                    } else {
                        distanceY = 0;
                    }
                } else if (bottomEdgeDistanceBottom < 0) {
                    if (distanceY > 0) {
                        int ration = (int) (DAMP_FACTOR / maxOffsetY * -bottomEdgeDistanceBottom) + 1;
                        distanceY /= ration;
                    } else {
                        distanceY = 0;
                    }
                }

                mMatrix.postTranslate(-distanceX, -distanceY);
                setImageMatrix(mMatrix);
                return true;
            }
            mState = STATE_NONE;
            return super.onScroll(e1, e2, distanceX, distanceY);
        }

        @Override
        public boolean onDoubleTap(MotionEvent e) {
            return super.onDoubleTap(e);
        }
    };

    private final OnScaleGestureListener mOnScaleGestureListener = new ScaleGestureDetector.SimpleOnScaleGestureListener() {
        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            LogUtil.e("缩放开始: " + System.currentTimeMillis());
            return true;
        }

        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            mState = STATE_NONE;
            if (getDrawable() == null || mMatrix == null) return true;

            mState = STATE_ZOOM;
            float scaleFactor = detector.getScaleFactor();
            float deltaFactor = scaleFactor - mPreScaleFactor;
            if (scaleFactor != 1.0f && deltaFactor != 0f) {
                mMatrix.postScale(deltaFactor + 1.0f, deltaFactor + 1.0f, detector.getFocusX(), detector.getFocusY());
                setImageMatrix(mMatrix);
            }
            mPreScaleFactor = scaleFactor;
            LogUtil.e("原始缩放比: " + getMatrixScale());
            return false;
        }

        @Override
        public void onScaleEnd(ScaleGestureDetector detector) {
            super.onScaleEnd(detector);
            LogUtil.e("缩放结束: " + System.currentTimeMillis());
            setFrameView();
        }
    };

    private final RotateGestureDetector.SimpleRotateGestureListener mSimpleRotateGestureListener = new RotateGestureDetector.SimpleRotateGestureListener() {

        float totalRotate = 0f;

        @Override
        public boolean onRotateBegin(RotateGestureDetector detector) {
            LogUtil.e("旋转开始: " + System.currentTimeMillis());
            return true;
        }

        @Override
        public boolean onRotate(RotateGestureDetector detector) {
            totalRotate += detector.getDegree();
            mMatrix.postRotate(detector.getDegree(), viewCenterX, viewCenterY);
            setImageMatrix(mMatrix);
            return true;
        }

        @Override
        public void onRotateEnd(RotateGestureDetector detector) {
            LogUtil.e("旋转结束: " + System.currentTimeMillis());
            fixRotate(totalRotate);
            fixScale();
            totalRotate = 0f;
         }
    };

    private void setFrameView() {
        RectF rectF = getMatrixRectF();
        frameViewLeft = Math.max(rectF.left, 0f);
        frameViewTop = Math.max(rectF.top, 0f);
        frameViewRight = Math.min(rectF.right, getWidth());
        frameViewBottom = Math.min(rectF.bottom, getHeight());
    }

    private void checkBound() {

        RectF rectF = getMatrixRectF();

        float leftEdgeDistanceLeft = rectF.left - frameViewLeft;
        float topEdgeDistanceTop = rectF.top - frameViewTop;
        float rightEdgeDistanceRight = rectF.right - frameViewRight;
        float bottomEdgeDistanceBottom = rectF.bottom - frameViewBottom;

        float[] values = new float[9];
        mMatrix.getValues(values);
        if (leftEdgeDistanceLeft > 0) {
            values[Matrix.MTRANS_X] -= leftEdgeDistanceLeft;
        } else if (rightEdgeDistanceRight < 0) {
            values[Matrix.MTRANS_X] -= rightEdgeDistanceRight;
        }
        if (topEdgeDistanceTop > 0) {
            values[Matrix.MTRANS_Y] -= topEdgeDistanceTop;
        } else if (bottomEdgeDistanceBottom < 0) {
            values[Matrix.MTRANS_Y] -= bottomEdgeDistanceBottom;
        }
        mMatrix.setValues(values);
        setImageMatrix(mMatrix);
    }

    private void fixRotate(float totalRotate) {
        // 右旋为正, 左旋为负
        LogUtil.e("旋转角度:  " + totalRotate);
        totalRotate %= 360;
        float fixDegree = 0f;
        if (totalRotate >= 0) {
            if (totalRotate < 45) {
                fixDegree = 0 - totalRotate;
            } else if (totalRotate >= 45 && totalRotate < 135) {
                fixDegree = 90 - totalRotate;
            } else if (totalRotate >= 135 && totalRotate < 225) {
                fixDegree = 180 - totalRotate;
            } else if (totalRotate >= 225 && totalRotate < 315) {
                fixDegree = 270 - totalRotate;
            } else if (totalRotate >= 315 && totalRotate < 360) {
                fixDegree = 360 - totalRotate;
            }
        } else {
            if (totalRotate > -45) {
                fixDegree = 0 -totalRotate;
            } else if (totalRotate <= -45 && totalRotate > -135) {
                fixDegree = -90 - totalRotate;
            } else if (totalRotate <= -135 && totalRotate > -225) {
                fixDegree = -180 - totalRotate;
            } else if (totalRotate <= -225 && totalRotate > -315) {
                fixDegree = -270 - totalRotate;
            } else if (totalRotate <= -315 && totalRotate > -360) {
                fixDegree = -360 - totalRotate;
            }
        }
        mMatrix.postRotate(fixDegree, viewCenterX, viewCenterY);
        setImageMatrix(mMatrix);

        currentImageRotate += (fixDegree + totalRotate);

        LogUtil.e("修正后角度: " + (fixDegree + totalRotate));
        LogUtil.e("当前角度: " + currentImageRotate);

    }


    /**
     * event执行顺序
     * 旋转开始->缩放开始->缩放结束->旋转结束
     * 所以选择在旋转结束时,去处理缩放过度问题
     */
    private void fixScale() {

        int viewWidth = getWidth();
        int viewHeight = getHeight();
        int drawableWidth = getDrawable().getIntrinsicWidth();
        int drawableHeight = getDrawable().getIntrinsicHeight();

        // 手动计算缩放比
        // 不能使用Matrix.MSCALE_X,它无法得出图片偏转时的准确缩放比
        RectF rectF = getMatrixRectF();
        LogUtil.e("图片相对宽度: " + rectF.width());
        LogUtil.e("图片相对高度: " + rectF.height());
        float currentImageScale;
        float widthScale, heightScale;
        if (currentImageRotate % 180 != 0) {
            // 图片竖直时的缩放比
            widthScale = rectF.width() / drawableWidth;
            heightScale = rectF.height() / drawableHeight;
        } else {
            // 图片横置时的缩放比
            widthScale = rectF.width() / drawableHeight;
            heightScale = rectF.height() / drawableWidth;
        }
        currentImageScale = (widthScale + heightScale) / 2;  // 使用平均值,会更加精确
        LogUtil.e("计算缩放比: " + currentImageScale);

        if (currentImageScale < mBaseScale) {
            // 图片小于最小比例,修正至屏幕中心位置
            float scale;
            if (currentImageRotate % 180 != 0) {
                scale = (float) viewWidth / drawableHeight;
            } else {
                scale = (float) viewWidth / drawableWidth;
            }

            float dy = (viewHeight - drawableHeight * scale) / 2;

            Matrix newMatrix = new Matrix();
            newMatrix.setScale(scale, scale);
            newMatrix.postTranslate(0, dy);
            newMatrix.postRotate(currentImageRotate, viewCenterX, viewCenterY);
            setImageMatrix(newMatrix);
            mMatrix = newMatrix;


        } else if (currentImageScale > mMaxScale) {

            // 太拉了,放大过度修正中心位置
            float dx = (viewWidth - drawableWidth * mMaxScale) / 2;
            float dy = (viewHeight - drawableHeight * mMaxScale) / 2;
            LogUtil.e("水平距离: " + dx + "  垂直距离: " + dy);

            Matrix newMatrix = new Matrix();
            newMatrix.setScale(mMaxScale, mMaxScale);
            newMatrix.postTranslate(dx, dy);
            newMatrix.postRotate(currentImageRotate, viewCenterX, viewCenterY);
            setImageMatrix(newMatrix);
            mMatrix = newMatrix;

        } else {

            // 算了,摆烂了,缩放全部修正至屏幕中心位置
            float dx = (viewWidth - drawableWidth * currentImageScale) / 2;
            float dy = (viewHeight - drawableHeight * currentImageScale) / 2;

            Matrix newMatrix = new Matrix();
            newMatrix.setScale(currentImageScale, currentImageScale);
            newMatrix.postTranslate(dx, dy);
            newMatrix.postRotate(currentImageRotate, viewCenterX, viewCenterY);
            setImageMatrix(newMatrix);
            mMatrix = newMatrix;
        }

        setFrameView();

    }

    private RectF getMatrixRectF() {
        RectF rectF = new RectF();
        Drawable drawable = getDrawable();
        if (drawable != null) {
            rectF.set(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
            mMatrix.mapRect(rectF);
        }
        return rectF;
    }

    private float getMatrixScale() {
        float[] values = new float[9];
        mMatrix.getValues(values);
        return values[Matrix.MSCALE_X];
    }
}

相关资料

  • RotateGestureDetector旋转手势辅助类,参考自链接,对其作了一些修改。
if (mInProgress && configChange) {   // 取消两指距离判定
//        if (mInProgress && (span < mMinSpan || configChange)) {
<dimen name="config_minScalingSpan">27mm</dimen>
 类似资料: