一起来写一个具备手势放大缩小功能的自定义ImageView

我之前的个人项目里, 用到了图片浏览, 大图浏览时, 大多都采用第三方的方案, 但是作为一个开发者,我们也需要知道人家到底是怎么做的. 目前大家普遍用的是Github上 PinchImageView ,或者PhotoView ,我们自己也可以实现一个简单版本的.

以下是我的版本


EnhancedImageView

一个增强的自定义ImageView,具备手势放大缩小等功能,主要原理的是 Matrix + ScaleGestureDetector + GestureDetector 进行对图片进行移动与裁剪

效果图

目前功能有

  • 单指滑动 (onTouch)
  • 多指滑动 (onTouch)
  • 双击放大(GestureDetector onDoubleTap)
  • 放大状态双击恢复
  • 自由手势放大 (ScaleGestureDetector.OnScaleGestureListener)
  • 解决与ViewPager滑动冲突

    冲突原因:ViewPager屏蔽了子View的左右移动事件

    解决:在放大状态下: getParent().requestDisallowInterceptTouchEvent(true);

只是做了上面那些功能, 但是比如滑动时的惯性效果, 以及缩小到比初始状态还小时的动画恢复等, 都没有做. 正常情况下使用的是Github上体验比较棒的 PinchImageView

难点

  1. 在拖动时,边界控制,需要每次触摸后都要判断 RectF里面的参数情况
  2. 放大缩小时的中心点处理

细节很多,需要考虑的东西很多.做出来简单, 做好了很难.

Code

具体工程见: https://github.com/Jerey-Jobs/EnhancedImageView

package com.jerey.imageview;

import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewTreeObserver;
import android.widget.ImageView;

/**
 * OnGlobalLayoutListener 获取控件宽高
 */
public class EnhancedImageView extends ImageView
        implements ViewTreeObserver.OnGlobalLayoutListener
        , ScaleGestureDetector.OnScaleGestureListener
        , View.OnTouchListener {
    private static final String TAG = "EnhancedImageView";
    private static final boolean DEBUG = true;

    //初始化标志
    private boolean mInitOnce = false;
    //初始化缩放值
    private float mInitScale;
    private float mMinScale;
    //双击放大的值
    private float mMidScale;
    //放大最大值
    private float mMaxScale;
    //负责图片的平移缩放
    private Matrix mScaleMatrix;
    //为缩放而生的类,捕获缩放比例
    private ScaleGestureDetector mScaleGestureDetector;

    /****************************自由移动***************/
    //记录手指数量
    private int mLastPointerCount;
    //记录上次手指触摸位置
    private float mLastX;
    private float mLastY;
    //触摸移动距离
    private int mTouchSlop;
    //是否可以拖动
    private boolean isCanDrag;
    //边界检查时用
    private boolean isCheckLeftAndRight;
    private boolean isCheckTopAndBottom;

    /*****************双击放大********************************/
    private GestureDetector mGestureDetector;

    public EnhancedImageView(Context context) {
        this(context, null);
    }

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

    public EnhancedImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mScaleMatrix = new Matrix();
        //覆盖用户设置
        super.setScaleType(ScaleType.MATRIX);
        mScaleGestureDetector = new ScaleGestureDetector(context, this);
        setOnTouchListener(this);
        //获取系统默认缩放
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onDoubleTap(MotionEvent e) {
                float x = e.getX();
                float y = e.getY();
                log("当前scale " + getScale() + "mid: " + mMidScale);
                if (getScale() < mMidScale) {
                    mScaleMatrix.postScale(mMidScale/getScale(), mMidScale/getScale(),getWidth()/2,getHeight()/2);
                    setImageMatrix(mScaleMatrix);
                } else {
                    log("恢复初始化");
                    //计算将图片移动至中间距离
                    int dx = getWidth() / 2 - getDrawable().getIntrinsicWidth() / 2;
                    int dy = getHeight() / 2 - getDrawable().getIntrinsicHeight() / 2;
                    mScaleMatrix.reset();
                    mScaleMatrix.postTranslate(dx, dy);
                    mScaleMatrix.postScale(mInitScale, mInitScale,getWidth()/2,getHeight()/2);
                    setImageMatrix(mScaleMatrix);
                }

                return true;
            }
        });
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        getViewTreeObserver().addOnGlobalLayoutListener(this);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        getViewTreeObserver().removeGlobalOnLayoutListener(this);
    }

    /**
     * 全局布局完成后会调用
     */
    @Override
    public void onGlobalLayout() {
        if (!mInitOnce) {
            //得到控件的宽和高
            int width = getWidth();
            int height = getHeight();

            //拿到图片的宽高
            Drawable drawable = getDrawable();
            if (drawable == null) {
                return;
            }
            int drawableWidth = drawable.getIntrinsicWidth();
            int drawableHeight = drawable.getIntrinsicHeight();

            float scale = 1.0f;
            //若图片宽度大于控件宽度 高度小于空间高度
            if (drawableWidth > width && drawableHeight < height) {
                log("若图片宽度大于控件宽度 高度小于空间高度");
                scale = width * 1.0f / drawableWidth;
                //图片的高度大于控件高度 宽度小于控件宽度
            } else if (drawableHeight > height && drawableWidth < width) {
                log("图片的高度大于控件高度 宽度小于控件宽度");
                scale = height * 1.0f / drawableHeight;
            } else if (drawableWidth > width && drawableHeight > height) {
                log("都大于");
                scale = Math.min(width * 1.0f / drawableWidth, height * 1.0f / drawableHeight);
            } else if (drawableWidth < width && drawableHeight < height) {
                log("都小于");
                scale = Math.min(width * 1.0f / drawableWidth, height * 1.0f / drawableHeight);
            }
            mInitScale = scale;
            mMinScale = scale;
            mMidScale = scale * 2;
            mMaxScale = scale * 5;

            //计算将图片移动至中间距离
            int dx = getWidth() / 2 - drawableWidth / 2;
            int dy = getHeight() / 2 - drawableHeight / 2;

            mScaleMatrix.postTranslate(dx, dy);
            //xy方向不变形,必须传一样的
            mScaleMatrix.postScale(mInitScale, mInitScale, width / 2, height / 2);
            setImageMatrix(mScaleMatrix);

            mInitOnce = true;
        }
    }

    /**
     * 获取当前图片的缩放值
     *
     * @return
     */
    public float getScale() {
        float[] values = new float[9];
        mScaleMatrix.getValues(values);
        return values[Matrix.MSCALE_X];
    }


    /**
     * 为缩放而生的类:ScaleGestureDetector
     *
     * @param detector
     * @return
     */
    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        float scale = getScale();
        float scaleFactor = detector.getScaleFactor();
        log("多点触控时候的缩放值: " + scaleFactor);
        if (getDrawable() == null) {
            return true;
        }

        //缩放范围的控制, 放大时需要小于最大,缩小时需要大于最小
        if ((scale < mMaxScale && scaleFactor > 1.0f) || (scale > mMinScale && scaleFactor < 1.0f)) {
            if (scale * scaleFactor < mMinScale) {
                scaleFactor = mMinScale / scale;
            }

            if (scale * scaleFactor > mMaxScale) {
                scaleFactor = mMaxScale / scale;
            }
            log("设置最终缩放值 " + scaleFactor);
            mScaleMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());

            checkBorderForScale();

            setImageMatrix(mScaleMatrix);
        }

        return true;
    }

    /**
     * 获得图片放大缩小以后的宽和高
     *
     * @return
     */
    private RectF getMatrixRectF() {
        Matrix matrix = mScaleMatrix;
        RectF rectF = new RectF();

        Drawable drawable = getDrawable();

        if (drawable != null) {
            rectF.set(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
            //用matrix进行map一下
            matrix.mapRect(rectF);
        }

        return rectF;
    }

    /**
     * 缩放时候进行边界控制等
     */
    private void checkBorderForScale() {
        RectF rect = getMatrixRectF();

        float deltaX = 0;
        float deltaY = 0;

        int width = getWidth();
        int height = getHeight();

        if (rect.width() >= width) {
            if (rect.left > 0) { //和屏幕左边有空隙
                deltaX = -rect.left; //左边移动
            }
            // 和屏幕as
            if (rect.right < width) {
                deltaX = width - rect.right;
            }
        }

        if (rect.height() >= height) {
            if (rect.top > 0) {
                deltaY = -rect.top;
            }

            if (rect.bottom < height) {
                deltaY = height - rect.bottom;
            }
        }

        //如果宽度或者高度小于控件的宽和高 居中处理
        if (rect.width() < width) {
            deltaX = getWidth() / 2 - rect.right + rect.width() / 2;
        }

        if (rect.height() < height) {
            deltaY = getHeight() / 2 - rect.bottom + rect.height() / 2;
        }
        mScaleMatrix.postTranslate(deltaX, deltaY);
    }

    /**
     * 当移动时,进行边界检查.
     */
    private void checkBorderForTraslate() {
        RectF rectF = getMatrixRectF();

        float deltaX = 0;
        float deltaY = 0;
        int width = getWidth();
        int height = getHeight();

        //上边有空白,往上移动
        if (rectF.top > 0 && isCheckTopAndBottom) {
            deltaY = -rectF.top;
        }

        if (rectF.bottom < height && isCheckTopAndBottom) {
            deltaY = height - rectF.bottom;
        }
        //左边和空白往左边移动
        if (rectF.left > 0 && isCheckLeftAndRight) {
            deltaX = -rectF.left;
        }

        if (rectF.right < width && isCheckLeftAndRight) {
            deltaX = width - rectF.right;
        }
        mScaleMatrix.postTranslate(deltaX, deltaY);
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {

        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {

    }

    private void log(String str) {
        if (DEBUG) {
            Log.i(TAG, str);
        }
    }

    /**
     * 为了让mScaleGestureDetector拿到手势
     *
     * @param v
     * @param event
     * @return
     */
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (mGestureDetector.onTouchEvent(event)) {
            return true;
        }
        mScaleGestureDetector.onTouchEvent(event);
        /**
         * 计算多指触控中心点
         */
        float currentX = 0;
        float currentY = 0;
        int pointCount = event.getPointerCount();

        for (int i = 0; i < pointCount; i++) {
            currentX += event.getX(i);
            currentY += event.getY(i);
        }
        currentX /= pointCount;
        currentY /= pointCount;

        if (mLastPointerCount != pointCount) {
            isCanDrag = false;
            mLastX = currentX;
            mLastY = currentY;
        }

        mLastPointerCount = pointCount;
        RectF rectF = getMatrixRectF();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //请求不被拦截
                if(rectF.width() > getWidth() || rectF.height() > getHeight()){
                    getParent().requestDisallowInterceptTouchEvent(true);
                }

                break;

            case MotionEvent.ACTION_MOVE:
                if(rectF.width() > (getWidth() + 0.01) || rectF.height() > (getHeight() + 0.01)){
                    getParent().requestDisallowInterceptTouchEvent(true);
                }

                float dx = currentX - mLastX;
                float dy = currentY - mLastY;

                if (!isCanDrag) {
                    isCanDrag = isMoveAction(dx, dy);
                }

                if (isCanDrag) {
                    RectF rectf = getMatrixRectF();
                    if (getDrawable() != null) {
                        isCheckLeftAndRight = isCheckTopAndBottom = true;
                        //如果宽度小于控件宽度,不允许横向移动
                        if (rectf.width() < getWidth()) {
                            dx = 0;
                            isCheckLeftAndRight = false;
                        }
                        //若高度小于控件高度,不允许纵向移动
                        if (rectf.height() < getHeight()) {
                            dy = 0;
                            isCheckTopAndBottom = false;
                        }
                        mScaleMatrix.postTranslate(dx, dy);
                        checkBorderForTraslate();
                        setImageMatrix(mScaleMatrix);
                    }
                }
                mLastX = currentX;
                mLastY = currentY;
                break;
            //结束时,将手指数量置0
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mLastPointerCount = 0;
                break;
            default:
                break;

        }


        return true;
    }


    /**
     * 判断当前移动距离是否大于系统默认最小移动距离
     *
     * @param dx
     * @param dy
     * @return
     */
    private boolean isMoveAction(float dx, float dy) {
        return Math.sqrt(dx * dx + dy * dy) > mTouchSlop;
    }

    private void resetToInit(){
        //得到控件的宽和高
        int width = getWidth();
        int height = getHeight();

        //拿到图片的宽高
        Drawable drawable = getDrawable();
        if (drawable == null) {
            return;
        }
        int drawableWidth = drawable.getIntrinsicWidth();
        int drawableHeight = drawable.getIntrinsicHeight();

        float scale = 1.0f;
        //若图片宽度大于控件宽度 高度小于空间高度
        if (drawableWidth > width && drawableHeight < height) {
            log("若图片宽度大于控件宽度 高度小于空间高度");
            scale = width * 1.0f / drawableWidth;
            //图片的高度大于控件高度 宽度小于控件宽度
        } else if (drawableHeight > height && drawableWidth < width) {
            log("图片的高度大于控件高度 宽度小于控件宽度");
            scale = height * 1.0f / drawableHeight;
        } else if (drawableWidth > width && drawableHeight > height) {
            log("都大于");
            scale = Math.min(width * 1.0f / drawableWidth, height * 1.0f / drawableHeight);
        } else if (drawableWidth < width && drawableHeight < height) {
            log("都小于");
            scale = Math.min(width * 1.0f / drawableWidth, height * 1.0f / drawableHeight);
        }
        mInitScale = scale;
        mMidScale = scale * 2;
        mMaxScale = scale * 5;

        //计算将图片移动至中间距离
        int dx = getWidth() / 2 - drawableWidth / 2;
        int dy = getHeight() / 2 - drawableHeight / 2;

        mScaleMatrix.postTranslate(dx, dy);

        ValueAnimator valueAnimator = new ValueAnimator();
    }

}

封面图.png

欢迎star, follow https://github.com/Jerey-Jobs/EnhancedImageView


本文作者:Anderson/Jerey_Jobs

博客地址 : 夏敏的博客/Anderson大码渣/Jerey_Jobs

简书地址 : Anderson大码渣

github地址 : Jerey_Jobs

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 太长了,还是转载吧...今天在看博客的时候,无意中发现了@Trinea在GitHub上的一个项目Android开源...
    庞哈哈哈12138阅读 19,965评论 3 283
  • 说到底,他无非和自己过不去,从大漠一路逃出来,嘴角出血皮肤干裂,脚底的水泡磨破又起最终流脓结痂,他背包里的水一口未...
    我曾路过他的国阅读 107评论 0 0
  • 这,是个什么怪字! 不管我用什么输入法都打不出来! 我把它写在纸上, 盯着它看了良久, 希望能看出些许端倪; 结果...
    aishe阅读 323评论 2 2
  • 《西藏生死书》 作者 索甲仁波切 这是一本将藏传佛法智慧运用于当代世俗生活之中,为饱受生死执念与俗世纷扰的世人传授...
    Heve七阅读 896评论 0 1