自定义view触摸放大缩小

真心佩服那些一直专注于技术共享的大神们,正是因为他们无私的分享精神,我才能每天都有进步。近日又算是仔细学了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。此时它里面的代码如下:

empty

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

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

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

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

 package com.example.view;
   
   import android.annotation.SuppressLint;
   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.ViewTreeObserver.OnGlobalLayoutListener;
  import android.view.View.OnTouchListener;
  import android.widget.ImageView;
  
  public class ZoomImageView extends ImageView implements OnGlobalLayoutListener, 
  OnScaleGestureListener, OnTouchListener
  {
      private boolean mOnce = false;//是否执行了一次
      
      /**
       * 初始缩放的比例
       */
      private float initScale;
      /**
       * 缩放比例
       */
      private float midScale;
      /**
       * 可放大的最大比例
       */
      private float maxScale;
      /**
       * 缩放矩阵
       */
      private Matrix scaleMatrix;
      
      /**
       * 缩放的手势监控类
       */
      private ScaleGestureDetector mScaleGestureDetector;
  
      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);
          
          scaleMatrix = new Matrix();
          
          setScaleType(ScaleType.MATRIX);
          
          mScaleGestureDetector = new ScaleGestureDetector(context, this);
          //触摸回调
          setOnTouchListener(this);
          
      }
      
      /**
       * 该方法在view与window绑定时被调用,且只会被调用一次,其在view的onDraw方法之前调用
       */
      protected void onAttachedToWindow()
      {
          super.onAttachedToWindow();
          //注册监听器
          getViewTreeObserver().addOnGlobalLayoutListener(this);
      }
      
      /**
       * 该方法在view被销毁时被调用
       */
      @SuppressLint("NewApi") protected void onDetachedFromWindow() 
      {
          super.onDetachedFromWindow();
          //取消监听器
          getViewTreeObserver().removeOnGlobalLayoutListener(this);
      }
      
      /**
       * 当一个view的布局加载完成或者布局发生改变时,OnGlobalLayoutListener会监听到,调用该方法
       * 因此该方法可能会被多次调用,需要在合适的地方注册和取消监听器
       */
      public void onGlobalLayout() 
      {
          if(!mOnce)
          {
              //获得当前view的Drawable
              Drawable d = getDrawable();
              
              if(d == null)
              {
                 return;
             }
             
             //获得Drawable的宽和高
             int dw = d.getIntrinsicWidth();
             int dh = d.getIntrinsicHeight();
             
             //获取当前view的宽和高
             int width = getWidth();
             int height = getHeight();
             
             //缩放的比例,scale可能是缩小的比例也可能是放大的比例,看它的值是大于1还是小于1
             float scale = 1.0f;
             
             //如果仅仅是图片宽度比view宽度大,则应该将图片按宽度缩小
             if(dw>width&&dh
   
   
    
    width&&dh>height)
             {
                 scale = Math.min(width*1.0f/dw, height*1.0f/dh);
             }
             //如果图片宽度和高度都比view的要小,则应该按最小的比例放大图片
             if(dw
    
    
     
     
      
      height)
             {
                 scale = height*1.0f/dh;
             }
             
             //初始化缩放的比例
             initScale = scale;
             midScale = initScale*2;
             maxScale = initScale*4;
             
             //移动图片到达view的中心
             int dx = width/2 - dw/2;
             int dy = height/2 - dh/2;
             scaleMatrix.postTranslate(dx, dy);
             
             //缩放图片
             scaleMatrix.postScale(initScale, initScale, width/2, height/2);    
             
             setImageMatrix(scaleMatrix);
             mOnce = true;
         }
         
     }
     /**
      * 获取当前已经缩放的比例
      * @return  因为x方向和y方向比例相同,所以只返回x方向的缩放比例即可
      */
     private float getDrawableScale()
     {
         
         float[] values = new float[9];
         scaleMatrix.getValues(values);
         
         return values[Matrix.MSCALE_X];
         
     }
 
     /**
      * 缩放手势进行时调用该方法
      * 
      * 缩放范围:initScale~maxScale
      */
     public boolean onScale(ScaleGestureDetector detector)
     {
                  if(getDrawable() == null)
         {
             return true;//如果没有图片,下面的代码没有必要运行
         }
         
         float scale = getDrawableScale();
         //获取当前缩放因子
         float scaleFactor = detector.getScaleFactor();
         
         if((scale
      
      
       
       1.0f)||(scale>initScale&&scaleFactor<1.0f))
         {
             //如果缩小的范围比允许的最小范围还要小,就重置缩放因子为当前的状态的因子
             if(scale*scaleFactor
       
       
         <1.0f) { scaleFactor = initScale/scale; } //如果缩小的范围比允许的最小范围还要小,就重置缩放因子为当前的状态的因子 if(scale*scaleFactor>maxScale&&scaleFactor>1.0f) { scaleFactor = maxScale/scale; } // scaleMatrix.postScale(scaleFactor, scaleFactor, getWidth()/2, getHeight()/2); scaleMatrix.postScale(scaleFactor, scaleFactor,detector.getFocusX(), detector.getFocusY()); setImageMatrix(scaleMatrix);//千万不要忘记设置这个,我总是忘记 } return true; } /** * 缩放手势开始时调用该方法 */ public boolean onScaleBegin(ScaleGestureDetector detector) { //返回为true,则缩放手势事件往下进行,否则到此为止,即不会执行onScale和onScaleEnd方法 return true; } /** * 缩放手势完成后调用该方法 */ public void onScaleEnd(ScaleGestureDetector detector) { } /** * 监听触摸事件 */ public boolean onTouch(View v, MotionEvent event) { if(mScaleGestureDetector != null) { //将触摸事件传递给手势缩放这个类 mScaleGestureDetector.onTouchEvent(event); } return true; } } 
       
      
      
     
     
    
    
   
   
由于多点触控在模拟上没法演示,因此zheli仍旧是一个手机上录制的gif。在真机上,我是用两根手指进行缩放的。可以看到,缩放的的效果是实现了。但是问题也出现了。什么问题呢?  即缩放完成后,图片的位置不再居中了,与屏幕之间有了空隙。我们想要的显然不是这样子的效果。我们需要缩放完成后, 图片位置不移动,而且缩放过程中,不允许与屏幕有空白间隙。 那么怎么解决这个问题呢??限于篇幅,就放在下一篇文章中吧。如果你还接着往下做的话,请保存好代码,看下一篇文章《(二)弥补图片自由缩放出现的间隙》。

实现Android自定义放大缩小按钮,可以通过自定义View实现。下面是一个简单的实现方法: 1. 创建一个自定义View类,并继承Button类。 2. 在构造函数中初始化一些变量,如原始宽度、高度、放大后的宽度、高度等。 3. 重写onMeasure()方法,在该方法中设置View的宽度和高度。 4. 重写onDraw()方法,在该方法中实现绘制放大缩小的效果。 5. 重写onTouchEvent()方法,处理用户的触摸事件。 具体实现方法可以参考以下代码: ```java public class ZoomButton extends Button { private int mOriginalWidth; private int mOriginalHeight; private int mZoomWidth; private int mZoomHeight; public ZoomButton(Context context) { super(context); init(); } public ZoomButton(Context context, AttributeSet attrs) { super(context, attrs); init(); } public ZoomButton(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { // 初始化变量 mOriginalWidth = getWidth(); mOriginalHeight = getHeight(); mZoomWidth = (int) (mOriginalWidth * 1.2f); mZoomHeight = (int) (mOriginalHeight * 1.2f); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 设置View的宽度和高度 setMeasuredDimension(mOriginalWidth, mOriginalHeight); } @Override protected void onDraw(Canvas canvas) { // 绘制放大缩小的效果 if (isPressed()) { canvas.scale(1.2f, 1.2f, mOriginalWidth / 2, mOriginalHeight / 2); } super.onDraw(canvas); } @Override public boolean onTouchEvent(MotionEvent event) { // 处理用户的触摸事件 switch (event.getAction()) { case MotionEvent.ACTION_DOWN: setWidth(mZoomWidth); setHeight(mZoomHeight); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: setWidth(mOriginalWidth); setHeight(mOriginalHeight); break; } return super.onTouchEvent(event); } } ``` 这样就实现了一个简单的自定义放大缩小按钮。 关于Android自定义View实现可展开、会呼吸的按钮,可以参考以下步骤: 1. 创建一个自定义View类,并继承Button类。 2. 在构造函数中初始化一些变量,如默认状态的宽度、高度、展开后的宽度、高度等。 3. 重写onMeasure()方法,在该方法中设置View的宽度和高度。 4. 重写onDraw()方法,在该方法中实现绘制展开呼吸的效果。 5. 重写onTouchEvent()方法,处理用户的触摸事件。 具体实现方法可以参考以下代码: ```java public class ExpandButton extends Button { private int mDefaultWidth; private int mDefaultHeight; private int mExpandWidth; private int mExpandHeight; private int mState = STATE_DEFAULT; private static final int STATE_DEFAULT = 0; private static final int STATE_EXPAND = 1; public ExpandButton(Context context) { super(context); init(); } public ExpandButton(Context context, AttributeSet attrs) { super(context, attrs); init(); } public ExpandButton(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { // 初始化变量 mDefaultWidth = getWidth(); mDefaultHeight = getHeight(); mExpandWidth = (int) (mDefaultWidth * 1.2f); mExpandHeight = (int) (mDefaultHeight * 1.2f); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 设置View的宽度和高度 setMeasuredDimension(mDefaultWidth, mDefaultHeight); } @Override protected void onDraw(Canvas canvas) { // 绘制展开呼吸的效果 if (mState == STATE_EXPAND) { float scale = 1.0f + 0.05f * (float) Math.sin(System.currentTimeMillis() / 200.0f); canvas.scale(scale, scale, mDefaultWidth / 2, mDefaultHeight / 2); } super.onDraw(canvas); } @Override public boolean onTouchEvent(MotionEvent event) { // 处理用户的触摸事件 switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (mState == STATE_DEFAULT) { setWidth(mExpandWidth); setHeight(mExpandHeight); mState = STATE_EXPAND; } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (mState == STATE_EXPAND) { setWidth(mDefaultWidth); setHeight(mDefaultHeight); mState = STATE_DEFAULT; } break; } return super.onTouchEvent(event); } } ``` 这样就实现了一个简单的自定义展开呼吸的按钮。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值