• Android实现支持缩放平移图片


    本文主要用到了以下知识点

    1. Matrix
    2. GestureDetector 能够捕捉到长按、双击
    3. ScaleGestureDetector 用于检测缩放的手势

    自由的缩放

    需求:当图片加载时,将图片在屏幕中居中;图片宽或高大于屏幕的,缩小至屏幕大小;自由对图片进行方法或缩小;

    package com.zhy.view;  
    
    import android.content.Context;  
    import android.graphics.Matrix;  
    import android.graphics.drawable.Drawable;  
    import android.util.AttributeSet;  
    import android.util.Log;  
    import android.view.MotionEvent;  
    import android.view.ScaleGestureDetector;  
    import android.view.ScaleGestureDetector.OnScaleGestureListener;  
    import android.view.View;  
    import android.view.View.OnTouchListener;  
    import android.view.ViewTreeObserver;  
    import android.widget.ImageView;  
    
    public class ZoomImageView extends ImageView implements OnScaleGestureListener,  
            OnTouchListener, ViewTreeObserver.OnGlobalLayoutListener  
    
    {  
        private static final String TAG = ZoomImageView.class.getSimpleName();  
    
        public static final float SCALE_MAX = 4.0f;  
        /** 
         * 初始化时的缩放比例,如果图片宽或高大于屏幕,此值将小于0 
         */  
        private float initScale = 1.0f;  
    
        /** 
         * 用于存放矩阵的9个值 
         */  
        private final float[] matrixValues = new float[9];  
    
        private boolean once = true;  
    
        /** 
         * 缩放的手势检测 
         */  
        private ScaleGestureDetector mScaleGestureDetector = null;  
    
        private final Matrix mScaleMatrix = new Matrix();  
    
        public ZoomImageView(Context context)  
        {  
            this(context, null);  
        }  
    
        public ZoomImageView(Context context, AttributeSet attrs)  
        {  
            super(context, attrs);  
            super.setScaleType(ScaleType.MATRIX);  
            mScaleGestureDetector = new ScaleGestureDetector(context, this);  
            this.setOnTouchListener(this);  
        }  
    
        @Override  
        public boolean onScale(ScaleGestureDetector detector)  
        {  
            float scale = getScale();  
            float scaleFactor = detector.getScaleFactor();  
    
            if (getDrawable() == null)  
                return true;  
    
            /** 
             * 缩放的范围控制 
             */  
            if ((scale < SCALE_MAX && scaleFactor > 1.0f)  
                    || (scale > initScale && scaleFactor < 1.0f))  
            {  
                /** 
                 * 最大值最小值判断 
                 */  
                if (scaleFactor * scale < initScale)  
                {  
                    scaleFactor = initScale / scale;  
                }  
                if (scaleFactor * scale > SCALE_MAX)  
                {  
                    scaleFactor = SCALE_MAX / scale;  
                }  
                /** 
                 * 设置缩放比例 
                 */  
                mScaleMatrix.postScale(scaleFactor, scaleFactor, getWidth() / 2,  
                        getHeight() / 2);  
                setImageMatrix(mScaleMatrix);  
            }  
            return true;  
    
        }  
    
        @Override  
        public boolean onScaleBegin(ScaleGestureDetector detector)  
        {  
            return true;  
        }  
    
        @Override  
        public void onScaleEnd(ScaleGestureDetector detector)  
        {  
        }  
    
        @Override  
        public boolean onTouch(View v, MotionEvent event)  
        {  
            return mScaleGestureDetector.onTouchEvent(event);  
    
        }  
    
    
        /** 
         * 获得当前的缩放比例 
         *  
         * @return 
         */  
        public final float getScale()  
        {  
            mScaleMatrix.getValues(matrixValues);  
            return matrixValues[Matrix.MSCALE_X];  
        }  
    
        @Override  
        protected void onAttachedToWindow()  
        {  
            super.onAttachedToWindow();  
            getViewTreeObserver().addOnGlobalLayoutListener(this);  
        }  
    
        @SuppressWarnings("deprecation")  
        @Override  
        protected void onDetachedFromWindow()  
        {  
            super.onDetachedFromWindow();  
            getViewTreeObserver().removeGlobalOnLayoutListener(this);  
        }  
    
        @Override  
        public void onGlobalLayout()  
        {  
            if (once)  
            {  
                Drawable d = getDrawable();  
                if (d == null)  
                    return;  
                Log.e(TAG, d.getIntrinsicWidth() + " , " + d.getIntrinsicHeight());  
                int width = getWidth();  
                int height = getHeight();  
                // 拿到图片的宽和高  
                int dw = d.getIntrinsicWidth();  
                int dh = d.getIntrinsicHeight();  
                float scale = 1.0f;  
                // 如果图片的宽或者高大于屏幕,则缩放至屏幕的宽或者高  
                if (dw > width && dh <= height)  
                {  
                    scale = width * 1.0f / dw;  
                }  
                if (dh > height && dw <= width)  
                {  
                    scale = height * 1.0f / dh;  
                }  
                // 如果宽和高都大于屏幕,则让其按按比例适应屏幕大小  
                if (dw > width && dh > height)  
                {  
                    scale = Math.min(dw * 1.0f / width, dh * 1.0f / height);  
                }  
                initScale = scale;  
                // 图片移动至屏幕中心  
                            mScaleMatrix.postTranslate((width - dw) / 2, (height - dh) / 2);  
                mScaleMatrix  
                        .postScale(scale, scale, getWidth() / 2, getHeight() / 2);  
                setImageMatrix(mScaleMatrix);  
                once = false;  
            }  
    
        }  
    
    }  

    1、我们在onGlobalLayout的回调中,根据图片的宽和高以及屏幕的宽和高,对图片进行缩放以及移动至屏幕的中心。如果图片很小,那就正常显示,不放大了~
    2、我们让OnTouchListener的MotionEvent交给ScaleGestureDetector进行处理

    @Override  
        public boolean onTouch(View v, MotionEvent event)  
        {  
            return mScaleGestureDetector.onTouchEvent(event);  
    
        }  

    3、在onScale的回调中对图片进行缩放的控制,首先进行缩放范围的判断,然后设置mScaleMatrix的scale值
    现在的效果:

    存在问题:

    1、缩放的中心点,我们设置是固定的,屏幕中间
    2、放大后,无法移动~

    设置缩放中心

    1、单纯的设置缩放中心
    仅仅是设置中心很简单,直接修改下中心点 :

    /** 
                 * 设置缩放比例 
                 */  
                mScaleMatrix.postScale(scaleFactor, scaleFactor,  
                        detector.getFocusX(), detector.getFocusX());  
                setImageMatrix(mScaleMatrix);  

    但是,随意的中心点放大、缩小,会导致图片的位置的变化,最终导致,图片宽高大于屏幕时,图片与屏幕间出现白边;图片小于屏幕,但是不居中。

    2、控制缩放时图片显示的范围
    所以我们在缩放的时候需要手动控制下范围:

    /** 
         * 在缩放时,进行图片显示范围的控制 
         */  
        private void checkBorderAndCenterWhenScale()  
        {  
    
            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;  
                }  
                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 = width * 0.5f - rect.right + 0.5f * rect.width();  
            }  
            if (rect.height() < height)  
            {  
                deltaY = height * 0.5f - rect.bottom + 0.5f * rect.height();  
            }  
            Log.e(TAG, "deltaX = " + deltaX + " , deltaY = " + deltaY);  
    
            mScaleMatrix.postTranslate(deltaX, deltaY);  
    
        }  
    
        /** 
         * 根据当前图片的Matrix获得图片的范围 
         *  
         * @return 
         */  
        private RectF getMatrixRectF()  
        {  
            Matrix matrix = mScaleMatrix;  
            RectF rect = new RectF();  
            Drawable d = getDrawable();  
            if (null != d)  
            {  
                rect.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());  
                matrix.mapRect(rect);  
            }  
            return rect;  
        }  

    自由的进行移动

    @Override  
        public boolean onTouch(View v, MotionEvent event)  
        {  
            mScaleGestureDetector.onTouchEvent(event);  
    
            float x = 0, y = 0;  
            // 拿到触摸点的个数  
            final int pointerCount = event.getPointerCount();  
            // 得到多个触摸点的x与y均值  
            for (int i = 0; i < pointerCount; i++)  
            {  
                x += event.getX(i);  
                y += event.getY(i);  
            }  
            x = x / pointerCount;  
            y = y / pointerCount;  
    
            /** 
             * 每当触摸点发生变化时,重置mLasX , mLastY  
             */  
            if (pointerCount != lastPointerCount)  
            {  
                isCanDrag = false;  
                mLastX = x;  
                mLastY = y;  
            }  
    
    
            lastPointerCount = pointerCount;  
    
            switch (event.getAction())  
            {  
            case MotionEvent.ACTION_MOVE:  
                Log.e(TAG, "ACTION_MOVE");  
                float dx = x - mLastX;  
                float dy = y - mLastY;  
    
                if (!isCanDrag)  
                {  
                    isCanDrag = isCanDrag(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);  
                        checkMatrixBounds();  
                        setImageMatrix(mScaleMatrix);  
                    }  
                }  
                mLastX = x;  
                mLastY = y;  
                break;  
    
            case MotionEvent.ACTION_UP:  
            case MotionEvent.ACTION_CANCEL:  
                Log.e(TAG, "ACTION_UP");  
                lastPointerCount = 0;  
                break;  
            }  
    
            return true;  
        }  

    首先我们拿到触摸点的数量,然后求出多个触摸点的平均值,设置给我们的mLastX , mLastY , 然后在移动的时候,得到dx ,dy 进行范围检查以后,调用mScaleMatrix.postTranslate进行设置偏移量,当然了,设置完成以后,还需要再次校验一下,不能把图片移动的与屏幕边界出现白边,校验完成后,调用setImageMatrix.

    这里:需要注意一下,我们没有复写ACTION_DOWM,是因为,ACTION_DOWN在多点触控的情况下,只要有一个手指按下状态,其他手指按下不会再次触发ACTION_DOWN,但是多个手指以后,触摸点的平均值会发生很大变化,所以我们没有用到ACTION_DOWN。每当触摸点的数量变化,我们就会跟新当前的mLastX,mLastY.
    下面是上面用到的两个私有方法,一个用于检查边界,一个用于判断是否是拖动的操作:

    /** 
         * 移动时,进行边界判断,主要判断宽或高大于屏幕的 
         */  
        private void checkMatrixBounds()  
        {  
            RectF rect = getMatrixRectF();  
    
            float deltaX = 0, deltaY = 0;  
            final float viewWidth = getWidth();  
            final float viewHeight = getHeight();  
            // 判断移动或缩放后,图片显示是否超出屏幕边界  
            if (rect.top > 0 && isCheckTopAndBottom)  
            {  
                deltaY = -rect.top;  
            }  
            if (rect.bottom < viewHeight && isCheckTopAndBottom)  
            {  
                deltaY = viewHeight - rect.bottom;  
            }  
            if (rect.left > 0 && isCheckLeftAndRight)  
            {  
                deltaX = -rect.left;  
            }  
            if (rect.right < viewWidth && isCheckLeftAndRight)  
            {  
                deltaX = viewWidth - rect.right;  
            }  
            mScaleMatrix.postTranslate(deltaX, deltaY);  
        }  
    
        /** 
         * 是否是推动行为 
         *  
         * @param dx 
         * @param dy 
         * @return 
         */  
        private boolean isCanDrag(float dx, float dy)  
        {  
            return Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop;  
        }  

    这样,我们就可以快乐的放大、缩小加移动了~~~

    双击放大与缩小

    谈到双击事件,我们的GestureDetector终于要登场了,这哥们可以捕获双击事件~~
    1、GestureDetector的使用
    因为GestureDetector设置监听器的话,方法一大串,而我们只需要onDoubleTap这个回调,所以我们准备使用它的一个内部类SimpleOnGestureListener,对接口的其他方法实现了空实现。
    不过还有几个问题需要讨论下,才能开始我们的代码:

    1、我们双击尺寸如何变化?
    我是这样的,根据当前的缩放值,如果是小于2的,我们双击直接到变为原图的2倍;如果是2,4之间的,我们双击直接为原图的4倍;其他状态也就是4倍,双击后还原到最初的尺寸。
    如果你觉得这样不合适,可以根据自己的爱好调整。

    2、我们双击变化,需要一个动画~~比如我们上例的演示图,图片很大,全屏显示的时候initScale=0.5左后,如果双击后变为2,也就是瞬间大了四倍,没有一个过渡的效果的话,给用户的感觉会特别差。所以,我们准备使用postDelay执行一个Runnable,Runnable中再次根据的当然的缩放值继续执行。
    首先我们在构造方法中,完成对GestureDetector的初始化,以及设置onDoubleTap监听

    public ZoomImageView(Context context, AttributeSet attrs)  
        {  
            super(context, attrs);  
            mScaleGestureDetector = new ScaleGestureDetector(context, this);  
            mGestureDetector = new GestureDetector(context,  
                    new SimpleOnGestureListener()  
                    {  
                        @Override  
                        public boolean onDoubleTap(MotionEvent e)  
                        {  
                            if (isAutoScale == true)  
                                return true;  
    
                            float x = e.getX();  
                            float y = e.getY();  
                            Log.e("DoubleTap", getScale() + " , " + initScale);  
                            if (getScale() < SCALE_MID)  
                            {  
                                ZoomImageView.this.postDelayed(  
                                        new AutoScaleRunnable(SCALE_MID, x, y), 16);  
                                isAutoScale = true;  
                            } else if (getScale() >= SCALE_MID  
                                    && getScale() < SCALE_MAX)  
                            {  
                                ZoomImageView.this.postDelayed(  
                                        new AutoScaleRunnable(SCALE_MAX, x, y), 16);  
                                isAutoScale = true;  
                            } else  
                            {  
                                ZoomImageView.this.postDelayed(  
                                        new AutoScaleRunnable(initScale, x, y), 16);  
                                isAutoScale = true;  
                            }  
    
                            return true;  
                        }  
                    });  
            mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();  
            super.setScaleType(ScaleType.MATRIX);  
            this.setOnTouchListener(this);  
        }  

    1、当双击的时候,首先判断是否正在自动缩放,如果在,直接retrun ;
    2、然后就进入了我们的if,如果当然是scale小于2,则通过view.发送一个Runnable进行执行;其他类似;
    下面看我们的Runnable的代码:

    /** 
         * 自动缩放的任务 
         *  
         * @author zhy 
         *  
         */  
        private class AutoScaleRunnable implements Runnable  
        {  
            static final float BIGGER = 1.07f;  
            static final float SMALLER = 0.93f;  
            private float mTargetScale;  
            private float tmpScale;  
    
            /** 
             * 缩放的中心 
             */  
            private float x;  
            private float y;  
    
            /** 
             * 传入目标缩放值,根据目标值与当前值,判断应该放大还是缩小 
             *  
             * @param targetScale 
             */  
            public AutoScaleRunnable(float targetScale, float x, float y)  
            {  
                this.mTargetScale = targetScale;  
                this.x = x;  
                this.y = y;  
                if (getScale() < mTargetScale)  
                {  
                    tmpScale = BIGGER;  
                } else  
                {  
                    tmpScale = SMALLER;  
                }  
    
            }  
    
            @Override  
            public void run()  
            {  
                // 进行缩放  
                mScaleMatrix.postScale(tmpScale, tmpScale, x, y);  
                checkBorderAndCenterWhenScale();  
                setImageMatrix(mScaleMatrix);  
    
                final float currentScale = getScale();  
                //如果值在合法范围内,继续缩放  
                if (((tmpScale > 1f) && (currentScale < mTargetScale))  
                        || ((tmpScale < 1f) && (mTargetScale < currentScale)))  
                {  
                    ZoomImageView.this.postDelayed(this, 16);  
                } else//设置为目标的缩放比例  
                {  
                    final float deltaScale = mTargetScale / currentScale;  
                    mScaleMatrix.postScale(deltaScale, deltaScale, x, y);  
                    checkBorderAndCenterWhenScale();  
                    setImageMatrix(mScaleMatrix);  
                    isAutoScale = false;  
                }  
    
            }  
        }  

    代码写完了,我们依然需要把我们的event传给它,依然是在onTouch方法:

    @Override  
        public boolean onTouch(View v, MotionEvent event)  
        {  
            if (mGestureDetector.onTouchEvent(event))  
                return true;  

    与ViewPager一起使用时的事件冲突问题

    现在我们迅速的想一想,记得之前学习过事件分发机制,我们的ZoomImageView在ViewPager中,如果我们不想被拦截,那么如何做呢?
    首先不想被拦截的条件是:我们的宽或高大于屏幕宽或高时,因为此时可以移动,我们不想被拦截。接下来,不想被拦截:
    getParent().requestDisallowInterceptTouchEvent(true);
    一行代码足以

    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() || rectF.height() > getHeight())  
                {  
                    getParent().requestDisallowInterceptTouchEvent(true);  
                }  

    requestDisallowInterceptTouchEvent

    ViewPager来实现左右滑动切换tab,如果tab的某一项中嵌入了水平可滑动的View就会让你有些不爽,比如想滑动tab项中的可水平滑动的控件,却导致tab切换。

    因为Android事件机制是从父View传向子View的,可以去检测你当前子View是不是在有可滑动控件等,决定事件是否拦截,但是这个麻烦,而且并不能解决所有的问题(必须检测触摸点是否在这个控件上面),其实有比较简单的方法,在你嵌套的控件中注入ViewPager实例(调用控件的getParent()方法),然后在onTouchEvent,onInterceptTouchEvent,dispatchTouchEvent里面告诉父View,也就是ViewPager不要拦截该控件上的触摸事件。

    参考链接

    requestDisallowInterceptTouchEvent - 喜糖 - 博客园

    postDelayed方法

    这是一种可以创建多线程消息的函数  
    使用方法:  
    1,首先创建一个Handler对象  
    Handler handler=new Handler();  
    2,然后创建一个Runnable对象  
    Runnable runnable=new Runnable(){  
       @Override  
       public void run() {  
        // TODO Auto-generated method stub  
        //要做的事情,这里再次调用此Runnable对象,以实现每两秒实现一次的定时器操作  
        handler.postDelayed(this, 2000);  
       }   
    };  
    3,使用PostDelayed方法,两秒后调用此Runnable对象  
    handler.postDelayed(runnable, 2000);  
    实际上也就实现了一个2s的一个定时器  
    4,如果想要关闭此定时器,可以这样操作  
    handler.removeCallbacks(runnable);  
    
    当然,你也可以做一个闹钟提醒延时的函数试试,比如,先用MediaPlayer播放闹钟声音,  
    如果不想起,被停止播放之后,下次就5分钟后再播放,再被停止的话,下次就4分钟后播放,  
    ………………  
    只要更改延时的时间就可以实现了,用一个static对象的话会比较容易操作。  
    
    
    全手打原创哦,百度能告诉你的我就不告诉你了。 

    参考链接

    关于 android 中 postDelayed方法的讲解 - - ITeye技术网站

    源代码

    放大功能

    单图版源码点击下载

    ViewPager版源码下载

    本文主要参考

    Android 手势检测实战 打造支持缩放平移的图片预览效果(上) - Hongyang - 博客频道 - CSDN.NET

    Android 手势检测实战 打造支持缩放平移的图片预览效果(下) - Hongyang - 博客频道 - CSDN.NET

  • 相关阅读:
    学生党 应该去 研究研究 Socket(套接字) 实现原理
    收录一些 硬件 相关的 文章
    谈谈 软件 开源项目
    我决定 开启 一个 人工智能 机器学习 技术 的 普及项目 Let it Learn
    我邀请 民科吧 网友 和 三江老师 来 反相吧 辩论 调和级数
    共量子论 丢番图方程组 数值求解 最小分子解
    刚 看了一下 一元三次方程 的 解法
    研究发展 C 语言
    对 量子病态定理 提出的 代数方程 不成立 的 证明
    数学 改革
  • 原文地址:https://www.cnblogs.com/jjx2013/p/6223655.html
Copyright © 2020-2023  润新知