自定义PhotoView

自定义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>
PhotoView PhotoView aims to help produce an easily usable implementation of a zooming Android ImageView. Branch Develop: Build Status Branch Master: Build Status PhotoView Features Out of the box zooming, using multi-touch and double-tap. Scrolling, with smooth scrolling fling. Works perfectly when used in a scrolling parent (such as ViewPager). Allows the application to be notified when the displayed Matrix has changed. Useful for when you need to update your UI based on the current zoom/scroll position. Allows the application to be notified when the user taps on the Photo. Sample Application The sample application (the source is in the repository) has been published onto Google Play for easy access: Get it on Google Play Gradle Dependency Add this in your root build.gradle file (not your module build.gradle file): allprojects { repositories { ... maven { url "https://jitpack.io" } } } Then, add the library to your project build.gradle dependencies { compile 'com.github.chrisbanes:PhotoView:1.2.6' } Sample Usage There is a sample provided which shows how to use the library in a more advanced way, but for completeness here is all that is required to get PhotoView working: ImageView mImageView; PhotoViewAttacher mAttacher; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Any implementation of ImageView can be used! mImageView = (ImageView) findViewById(R.id.iv_photo); // Set the Drawable displayed Drawable bitmap = getResources().getDrawable(R.drawable.wallpaper); mImageView.setImageDrawable(bitmap); // Attach a PhotoViewAttacher, which takes care of all of the zooming functionality. // (not needed unless you are going to change the drawable later) mAttacher = new PhotoViewAttacher(mImageView); } // If you later call mImageView.setImageDrawable/setImageBitmap/setImageResource/etc then you just need to call mAttacher.update(); Issues With ViewGroups There are some ViewGroups (ones that utilize onInterceptTouchEvent) that throw exceptions when a PhotoView is placed within them, most notably ViewPager and DrawerLayout. This is a framework issue that has not been resolved. In order to prevent this exception (which typically occurs when you zoom out), take a look at HackyDrawerLayout and you can see the solution is to simply catch the exception. Any ViewGroup which uses onInterceptTouchEvent will also need to be extended and exceptions caught. Use the HackyDrawerLayout as a template of how to do so. The basic implementation is: public class HackyProblematicViewGroup extends ProblematicViewGroup { public HackyProblematicViewGroup(Context context) { super(context); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { try { return super.onInterceptTouchEvent(ev); } catch (IllegalArgumentException e) { //uncomment if you really want to see these errors //e.printStackTrace(); return false; } } } Usage with Fresco Due to the complex nature of Fresco, this library does not currently support Fresco. See this project as an alternative solution. Subsampling Support This library aims to keep the zooming implementation simple. If you are looking for an implementation that supports subsampling, check out this project Pull Requests / Contribution Development happens in develop branch of this repository, and Pull Requests should be filled against that branch. Any Pull Request against master will be rejected License Copyright 2011, 2012 Chris Banes Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值