• (一)自定义ImageView,初步实现多点触控、自由缩放


          真心佩服那些一直专注于技术共享的大神们,正是因为他们无私的分享精神,我才能每天都有进步。近日又算是仔细学了android的自定义控件技术,跟着大神的脚步实现了一个自定义的ImageView。里面涉及到常用的多点触控技术。在此十分感谢那些默默奉献的大神们,同时向他们学习,也把自己的学习过程以及收获到的知识分享给大家。这个自定义的ImgaeView实现了图片的自由缩放,自由移动,并解决了与ViewPager的兼容性问题。开发完成后,可以直接代替普通的ImageView了。废话不多说了,现在就跟我一起进入到自定义ImageView实现多点触控的开发之旅中吧。

         本项目所涉及到的源码以及图片素材,可点击下面的链接下载:
    http://download.csdn.net/detail/fuyk12/9243417

    一、效果展示以及前言说明

          由于多点触控在模拟器上无法演示,因此我也用真机录制了一个gif,展示给大家看,但是录制的效果不是很流畅,不过足以说明问题了。效果展示如下,左图为真机效果,右图为模拟上的效果(无法演示自由缩放):

                        

           效果说明:  项目中展示的是一个ViewPager,里面放置了三张图片(如上图所示的三张图片)。而盛载图片的就是自定义的ImageView,由于在其中实现了多点触控技术,因此我们从上图中可以看到,每一张图片都可以自由缩放和移动。因为左图展示的是我在手机上的操作,所以你看不到我触控的地方,这很正常。小伙伴们做的时候,就可以在自己的手机上看到了。那么上图展示的都包括什么效果呢?其中包括:图片的自由缩放,图片的自由移动,图片的双击放大和缩小的效果。

         所用到的知识点: 基本的android知识不必多说。此外还需要用到android下的缩放手势监控类ScaleGestureDetector,其他多样的手势监控类(例如监控双击)GestureDetector,以及基本的OnTouchListener和OnGlobalLayoutListener。还有控制图片变换的Matrix。这些API如果大家不会用,可以网上学习一下。因为如果再讲这些基础的API,文章显得超级冗余。因此本系列文章的主旨不是讲解使用到的android基础知识,而是实战,主要分析实现这个自定义ImageView的各种逻辑以及碰到的困难。那些API,很容易就学会的,或者读者也可以一般跟着做这个项目的代码,一边学。

         限于篇幅, 这一篇文章将初步实现图片的自由缩放,在本篇文章的基础上,更多内容在后续的文章里面。

    二、进入项目实战

          下面就让开始写代码,一步一步来实现上面所展示的效果吧。

    (1)在控件上图片的显示控制

          新建项目,新建类ZoomImageView继承自ImageView。此时它里面的代码如下:

    package com.example.view;
    
    import android.widget.ImageView;
    
    public class ZoomImageView extends ImageView
    {
        public ZoomImageView(Context context)
        {
            this(context,null);
        }
        public ZoomImageView(Context context, AttributeSet attrs) 
        {
            this(context, attrs,0);
    
        }
        public ZoomImageView(Context context, AttributeSet attrs, int defStyle)
        {
            super(context, attrs, defStyle);
                 
        }
        
    }

        代码很简单,不再解释。 然后将需要的图片素材都拷贝进来(文章开头可以下载)。首先要考虑的是,有的图片很大,有的图片很小,我们需要将这些图片合适的显示在ZoomImageView上。在这里,我们让所有的图片都显示在屏幕中央,也就是说,大的图片让它缩小,小的图片让它放大。为了详细的说明这个问题。看下面的一张分析图:

          上面分析的是一张比较小的图片,因此缩放是放大,如果图片比较大,那么缩放就应该是缩小了。想想为什么无论是放大还是缩小,都要以小的缩放比例为标准?这是因为,比如宽度需要方法2倍才能与屏幕一样宽,高度需要放大3倍才能与屏幕一样高,如果我们选择放大3倍,那么宽度就超出了屏幕。按照这样的道理,想要达到图示B的那样的展示效果,无论放大还是缩小都应该以小的比例为标准。从上面图示的分析,我们就拿到了将图片显示在ZoomImageView上的标准,即:

    平移:
    x方向:屏幕宽度/2 - 图片原始宽度/2
    y方向:屏幕高度/2 - 图片原始高度/2
    缩放:通过图片原始宽高与屏幕宽高的比较,以小的缩放比例为标准进行缩放。

             好了,图片显示的原理已经分析完毕,下面将其用代码表达出来。修改ZoomImageView的代码如下:

      1 package com.example.view;
      2 
      3 import android.annotation.SuppressLint;
      4 import android.content.Context;
      5 import android.graphics.Matrix;
      6 import android.graphics.drawable.Drawable;
      7 import android.util.AttributeSet;
      8 import android.util.Log;
      9 import android.view.MotionEvent;
     10 import android.view.ScaleGestureDetector;
     11 import android.view.ScaleGestureDetector.OnScaleGestureListener;
     12 import android.view.View;
     13 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
     14 import android.view.View.OnTouchListener;
     15 import android.widget.ImageView;
     16 
     17 public class ZoomImageView extends ImageView implements OnGlobalLayoutListener 
     18 {
     19     private boolean mOnce = false;//是否执行了一次
     20     
     21     /**
     22      * 初始缩放的比例
     23      */
     24     private float initScale;
     25     /**
     26      * 缩放比例
     27      */
     28     private float midScale;
     29     /**
     30      * 可放大的最大比例
     31      */
     32     private float maxScale;
     33     /**
     34      * 缩放矩阵
     35      */
     36     private Matrix scaleMatrix;
     37     
     38     /**
     39      * 缩放的手势监控类
     40      */
     41     private ScaleGestureDetector mScaleGestureDetector;
     42 
     43     public ZoomImageView(Context context)
     44     {
     45         this(context,null);
     46     }
     47     public ZoomImageView(Context context, AttributeSet attrs) 
     48     {
     49         this(context, attrs,0);
     50 
     51     }
     52     public ZoomImageView(Context context, AttributeSet attrs, int defStyle)
     53     {
     54         super(context, attrs, defStyle);
     55         
     56         scaleMatrix = new Matrix();
     57         
     58         setScaleType(ScaleType.MATRIX);        
     59     }
     60     
     61     /**
     62      * 该方法在view与window绑定时被调用,且只会被调用一次,其在view的onDraw方法之前调用
     63      */
     64     protected void onAttachedToWindow()
     65     {
     66         super.onAttachedToWindow();
     67         //注册监听器
     68         getViewTreeObserver().addOnGlobalLayoutListener(this);
     69     }
     70     
     71     /**
     72      * 该方法在view被销毁时被调用
     73      */
     74     @SuppressLint("NewApi") protected void onDetachedFromWindow() 
     75     {
     76         super.onDetachedFromWindow();
     77         //取消监听器
     78         getViewTreeObserver().removeOnGlobalLayoutListener(this);
     79     }
     80     
     81     /**
     82      * 当一个view的布局加载完成或者布局发生改变时,OnGlobalLayoutListener会监听到,调用该方法
     83      * 因此该方法可能会被多次调用,需要在合适的地方注册和取消监听器
     84      */
     85     public void onGlobalLayout() 
     86     {
     87         if(!mOnce)
     88         {
     89             //获得当前view的Drawable
     90             Drawable d = getDrawable();
     91             
     92             if(d == null)
     93             {
     94                 return;
     95             }
     96             
     97             //获得Drawable的宽和高
     98             int dw = d.getIntrinsicWidth();
     99             int dh = d.getIntrinsicHeight();
    100             
    101             //获取当前view的宽和高
    102             int width = getWidth();
    103             int height = getHeight();
    104             
    105             //缩放的比例,scale可能是缩小的比例也可能是放大的比例,看它的值是大于1还是小于1
    106             float scale = 1.0f;
    107             
    108             //如果仅仅是图片宽度比view宽度大,则应该将图片按宽度缩小
    109             if(dw>width&&dh<height)
    110             {
    111                 scale = width*1.0f/dw;
    112             }
    113             //如果图片和高度都比view的大,则应该按最小的比例缩小图片
    114             if(dw>width&&dh>height)
    115             {
    116                 scale = Math.min(width*1.0f/dw, height*1.0f/dh);
    117             }
    118             //如果图片宽度和高度都比view的要小,则应该按最小的比例放大图片
    119             if(dw<width&&dh<height)
    120             {
    121                 scale = Math.min(width*1.0f/dw, height*1.0f/dh);
    122             }
    123             //如果仅仅是高度比view的大,则按照高度缩小图片即可
    124             if(dw<width&&dh>height)
    125             {
    126                 scale = height*1.0f/dh;
    127             }
    128             
    129             //初始化缩放的比例
    130             initScale = scale;
    131             midScale = initScale*2;
    132             maxScale = initScale*4;
    133             
    134             //移动图片到达view的中心
    135             int dx = width/2 - dw/2;
    136             int dy = height/2 - dh/2;
    137             scaleMatrix.postTranslate(dx, dy);
    138             
    139             //缩放图片
    140             scaleMatrix.postScale(initScale, initScale, width/2, height/2);    
    141             
    142             setImageMatrix(scaleMatrix);
    143             mOnce = true;
    144         }
    145         
    146     }
    147 
    148 }

          代码解释:多了些成员变量,大家先敲上即可,以后自会知道他们什么作用。在这里,使用OnGlobalLayoutListener来监听ZoomImageView的状态改变,,注意在ZoomImageView的onDraw方法执行之前,会调用onGlobalLayout方法。因此保证了在图片被画出来之前,就已经实现了对其显示位置的控制。因为OnGlobalLayoutListener监听的是view状态的改变,因此可能会被多出调用,所以需要一个布尔型变量mOnce来控制,onGlobalLayout中的代码只能执行一次,而且在onDetachedFromWindow的时候要取消监听。而onGrobalLayout中的代码,就是根据前面的分析所写出来的。注意在缩放的时候,有多种情况需要考虑。关于第129行到132行,是保存缩放比例。因为缩放图片不可能无限放大,应该有一个缩放的范围。这里,取的是initScale~maxScale。而midScale算是中间的一个临界点,后面会用到。代码的注释很详细了,其他的就不再多解释了。

          下面我们就来看一看图片的位置是否按照代码中预设的摆正了。修改activity_main.xml,如下:

     1 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     2     xmlns:tools="http://schemas.android.com/tools"
     3     android:layout_width="match_parent"
     4     android:layout_height="match_parent"
     5    >
     6    
     7     <com.example.view.ZoomImageView
     8         android:id="@+id/img_ziv"
     9         android:layout_width="match_parent"
    10         android:layout_height="match_parent"
    11         android:scaleType="matrix"
    12         android:src="@drawable/mingxing0403">"
    13         
    14     </com.example.view.ZoomImageView>
    15 
    16 
    17 </RelativeLayout>

           在布局中,放了一张图片在ZoomImageView中,MainActivity中默认已经加载 了这个布局,不必修改。现在运行下程序,效果如下:

         ok,图片的显示这部分已经完成了。下面开始编写自由缩放的代码。

    (2)图片的自由缩放

          自由缩放是通过ScaleGestureDetector这个类来实现的。需要将手指触摸的事件传递给它,然后对手指移动进行监控,获取缩放因子才能实现缩放功能。大体逻辑就是:首先获取到图片心当前的缩放比例,然后根据缩放因子,判断是否超出了我们允许的缩放范围,如果不在允许的缩放范围内,就禁止缩放,否则就允许缩放。先看代码,再详细的解释吧。修改ZoomImageView,如下:

      1 package com.example.view;
      2 
      3 import android.annotation.SuppressLint;
      4 import android.content.Context;
      5 import android.graphics.Matrix;
      6 import android.graphics.drawable.Drawable;
      7 import android.util.AttributeSet;
      8 import android.util.Log;
      9 import android.view.MotionEvent;
     10 import android.view.ScaleGestureDetector;
     11 import android.view.ScaleGestureDetector.OnScaleGestureListener;
     12 import android.view.View;
     13 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
     14 import android.view.View.OnTouchListener;
     15 import android.widget.ImageView;
     16 
     17 public class ZoomImageView extends ImageView implements OnGlobalLayoutListener, 
     18 OnScaleGestureListener, OnTouchListener
     19 {
     20     private boolean mOnce = false;//是否执行了一次
     21     
     22     /**
     23      * 初始缩放的比例
     24      */
     25     private float initScale;
     26     /**
     27      * 缩放比例
     28      */
     29     private float midScale;
     30     /**
     31      * 可放大的最大比例
     32      */
     33     private float maxScale;
     34     /**
     35      * 缩放矩阵
     36      */
     37     private Matrix scaleMatrix;
     38     
     39     /**
     40      * 缩放的手势监控类
     41      */
     42     private ScaleGestureDetector mScaleGestureDetector;
     43 
     44     public ZoomImageView(Context context)
     45     {
     46         this(context,null);
     47     }
     48     public ZoomImageView(Context context, AttributeSet attrs) 
     49     {
     50         this(context, attrs,0);
     51 
     52     }
     53     public ZoomImageView(Context context, AttributeSet attrs, int defStyle)
     54     {
     55         super(context, attrs, defStyle);
     56         
     57         scaleMatrix = new Matrix();
     58         
     59         setScaleType(ScaleType.MATRIX);
     60         
     61         mScaleGestureDetector = new ScaleGestureDetector(context, this);
     62         //触摸回调
     63         setOnTouchListener(this);
     64         
     65     }
     66     
     67     /**
     68      * 该方法在view与window绑定时被调用,且只会被调用一次,其在view的onDraw方法之前调用
     69      */
     70     protected void onAttachedToWindow()
     71     {
     72         super.onAttachedToWindow();
     73         //注册监听器
     74         getViewTreeObserver().addOnGlobalLayoutListener(this);
     75     }
     76     
     77     /**
     78      * 该方法在view被销毁时被调用
     79      */
     80     @SuppressLint("NewApi") protected void onDetachedFromWindow() 
     81     {
     82         super.onDetachedFromWindow();
     83         //取消监听器
     84         getViewTreeObserver().removeOnGlobalLayoutListener(this);
     85     }
     86     
     87     /**
     88      * 当一个view的布局加载完成或者布局发生改变时,OnGlobalLayoutListener会监听到,调用该方法
     89      * 因此该方法可能会被多次调用,需要在合适的地方注册和取消监听器
     90      */
     91     public void onGlobalLayout() 
     92     {
     93         if(!mOnce)
     94         {
     95             //获得当前view的Drawable
     96             Drawable d = getDrawable();
     97             
     98             if(d == null)
     99             {
    100                 return;
    101             }
    102             
    103             //获得Drawable的宽和高
    104             int dw = d.getIntrinsicWidth();
    105             int dh = d.getIntrinsicHeight();
    106             
    107             //获取当前view的宽和高
    108             int width = getWidth();
    109             int height = getHeight();
    110             
    111             //缩放的比例,scale可能是缩小的比例也可能是放大的比例,看它的值是大于1还是小于1
    112             float scale = 1.0f;
    113             
    114             //如果仅仅是图片宽度比view宽度大,则应该将图片按宽度缩小
    115             if(dw>width&&dh<height)
    116             {
    117                 scale = width*1.0f/dw;
    118             }
    119             //如果图片和高度都比view的大,则应该按最小的比例缩小图片
    120             if(dw>width&&dh>height)
    121             {
    122                 scale = Math.min(width*1.0f/dw, height*1.0f/dh);
    123             }
    124             //如果图片宽度和高度都比view的要小,则应该按最小的比例放大图片
    125             if(dw<width&&dh<height)
    126             {
    127                 scale = Math.min(width*1.0f/dw, height*1.0f/dh);
    128             }
    129             //如果仅仅是高度比view的大,则按照高度缩小图片即可
    130             if(dw<width&&dh>height)
    131             {
    132                 scale = height*1.0f/dh;
    133             }
    134             
    135             //初始化缩放的比例
    136             initScale = scale;
    137             midScale = initScale*2;
    138             maxScale = initScale*4;
    139             
    140             //移动图片到达view的中心
    141             int dx = width/2 - dw/2;
    142             int dy = height/2 - dh/2;
    143             scaleMatrix.postTranslate(dx, dy);
    144             
    145             //缩放图片
    146             scaleMatrix.postScale(initScale, initScale, width/2, height/2);    
    147             
    148             setImageMatrix(scaleMatrix);
    149             mOnce = true;
    150         }
    151         
    152     }
    153     /**
    154      * 获取当前已经缩放的比例
    155      * @return  因为x方向和y方向比例相同,所以只返回x方向的缩放比例即可
    156      */
    157     private float getDrawableScale()
    158     {
    159         
    160         float[] values = new float[9];
    161         scaleMatrix.getValues(values);
    162         
    163         return values[Matrix.MSCALE_X];
    164         
    165     }
    166 
    167     /**
    168      * 缩放手势进行时调用该方法
    169      * 
    170      * 缩放范围:initScale~maxScale
    171      */
    172     public boolean onScale(ScaleGestureDetector detector)
    173     {
    174         
    175         if(getDrawable() == null)
    176         {
    177             return true;//如果没有图片,下面的代码没有必要运行
    178         }
    179         
    180         float scale = getDrawableScale();
    181         //获取当前缩放因子
    182         float scaleFactor = detector.getScaleFactor();
    183         
    184         if((scale<maxScale&&scaleFactor>1.0f)||(scale>initScale&&scaleFactor<1.0f))
    185         {
    186             //如果缩小的范围比允许的最小范围还要小,就重置缩放因子为当前的状态的因子
    187             if(scale*scaleFactor<initScale&&scaleFactor<1.0f)
    188             {
    189                 scaleFactor = initScale/scale;
    190             }
    191             //如果缩小的范围比允许的最小范围还要小,就重置缩放因子为当前的状态的因子
    192             if(scale*scaleFactor>maxScale&&scaleFactor>1.0f)
    193             {
    194                 scaleFactor = maxScale/scale;
    195             }
    196             
    197 //            scaleMatrix.postScale(scaleFactor, scaleFactor, getWidth()/2, getHeight()/2);
    198             scaleMatrix.postScale(scaleFactor, scaleFactor,detector.getFocusX(), 
    199                     detector.getFocusY());
    200             
    201             
    202             setImageMatrix(scaleMatrix);//千万不要忘记设置这个,我总是忘记
    203         }
    204         
    205         
    206     
    207         return true;
    208     }
    209     /**
    210      * 缩放手势开始时调用该方法
    211      */
    212     public boolean onScaleBegin(ScaleGestureDetector detector) 
    213     {    
    214         //返回为true,则缩放手势事件往下进行,否则到此为止,即不会执行onScale和onScaleEnd方法
    215         return true;
    216     }
    217     /**
    218      * 缩放手势完成后调用该方法
    219      */
    220     public void onScaleEnd(ScaleGestureDetector detector)
    221     {
    222         
    223         
    224     }
    225 
    226     /**
    227      * 监听触摸事件
    228      */
    229     public boolean onTouch(View v, MotionEvent event)
    230     {
    231     
    232         if(mScaleGestureDetector != null)
    233         {
    234             //将触摸事件传递给手势缩放这个类
    235             mScaleGestureDetector.onTouchEvent(event);
    236         }
    237         return true;
    238     }
    239 
    240 }

           红色部分是我们增加的代码。从代码中可以看到,ZoomImageView实现了OnScaleGestureListener和OnTouchListener接口。在第229行onTouch方法中,将触摸事件传递给了mScaleGestureDetector 。在mScaleGestureDetector 的onScale方法中,对缩放逻辑进行了编写。首先使用getDrawableScale获取到当前缩放比例,然后再获取到缩放因子。然后进行判断:如果当前缩放比例比最大比例小,且缩放因子大于1,说明想放大,这是被允许的,因为还还可以再放大。如果当前缩放比例比最小比例大,且缩放因子小于1,说明想缩小,这也是被允许的。利用Matrix的postScale方法设置缩放即可。其中的逻辑还是比较简单的。其他就没什么好解释的了,注释很清晰。那么效果如何呢?达到预期了吗?运行程序,效果如下:

          由于多点触控在模拟上没法演示,因此zheli仍旧是一个手机上录制的gif。在真机上,我是用两根手指进行缩放的。可以看到,缩放的的效果是实现了。但是问题也出现了。什么问题呢?即缩放完成后,图片的位置不再居中了,与屏幕之间有了空隙。我们想要的显然不是这样子的效果。我们需要缩放完成后,图片位置不移动,而且缩放过程中,不允许与屏幕有空白间隙。那么怎么解决这个问题呢??限于篇幅,就放在下一篇文章中吧。如果你还接着往下做的话,请保存好代码,看下一篇文章《(二)弥补图片自由缩放出现的间隙》。点击下面的链接即可:
    http://www.cnblogs.com/fuly550871915/p/4939954.html

  • 相关阅读:
    RESTful Web服务的操作
    Nginx学习之如何搭建文件防盗链服务
    PostgreSQL10.5安装详细步骤(Win10)
    前端安全系列(一):如何防止XSS攻击?
    【原码笔记】-- protobuf.js 与 Long.js
    【微信开发】-- 发送模板消息
    能编程与会编程
    vue2入坑随记(二) -- 自定义动态组件
    微信上传图片
    vue2入坑随记(一)-- 初始全家桶
  • 原文地址:https://www.cnblogs.com/fuly550871915/p/4939629.html
Copyright © 2020-2023  润新知