(一)自定义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的代码如下:
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
{
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);
} /**
* 该方法在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<height)
{
scale = width*1.0f/dw;
}
//如果图片和高度都比view的大,则应该按最小的比例缩小图片
if(dw>width&&dh>height)
{
scale = Math.min(width*1.0f/dw, height*1.0f/dh);
}
//如果图片宽度和高度都比view的要小,则应该按最小的比例放大图片
if(dw<width&&dh<height)
{
scale = Math.min(width*1.0f/dw, height*1.0f/dh);
}
//如果仅仅是高度比view的大,则按照高度缩小图片即可
if(dw<width&&dh>height)
{
scale = height*1.0f/dh;
} 129 //初始化缩放的比例
130 initScale = scale;
131 midScale = initScale*2;
132 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;
} } }
代码解释:多了些成员变量,大家先敲上即可,以后自会知道他们什么作用。在这里,使用OnGlobalLayoutListener来监听ZoomImageView的状态改变,,注意在ZoomImageView的onDraw方法执行之前,会调用onGlobalLayout方法。因此保证了在图片被画出来之前,就已经实现了对其显示位置的控制。因为OnGlobalLayoutListener监听的是view状态的改变,因此可能会被多出调用,所以需要一个布尔型变量mOnce来控制,onGlobalLayout中的代码只能执行一次,而且在onDetachedFromWindow的时候要取消监听。而onGrobalLayout中的代码,就是根据前面的分析所写出来的。注意在缩放的时候,有多种情况需要考虑。关于第129行到132行,是保存缩放比例。因为缩放图片不可能无限放大,应该有一个缩放的范围。这里,取的是initScale~maxScale。而midScale算是中间的一个临界点,后面会用到。代码的注释很详细了,其他的就不再多解释了。
下面我们就来看一看图片的位置是否按照代码中预设的摆正了。修改activity_main.xml,如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
> <com.example.view.ZoomImageView
android:id="@+id/img_ziv"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="matrix"
android:src="@drawable/mingxing0403">" </com.example.view.ZoomImageView> </RelativeLayout>
在布局中,放了一张图片在ZoomImageView中,MainActivity中默认已经加载 了这个布局,不必修改。现在运行下程序,效果如下:
ok,图片的显示这部分已经完成了。下面开始编写自由缩放的代码。
(2)图片的自由缩放
自由缩放是通过ScaleGestureDetector这个类来实现的。需要将手指触摸的事件传递给它,然后对手指移动进行监控,获取缩放因子才能实现缩放功能。大体逻辑就是:首先获取到图片心当前的缩放比例,然后根据缩放因子,判断是否超出了我们允许的缩放范围,如果不在允许的缩放范围内,就禁止缩放,否则就允许缩放。先看代码,再详细的解释吧。修改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,
18 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();
58
59 setScaleType(ScaleType.MATRIX);
60
61 mScaleGestureDetector = new ScaleGestureDetector(context, this);
62 //触摸回调
63 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<height)
{
scale = width*1.0f/dw;
}
//如果图片和高度都比view的大,则应该按最小的比例缩小图片
if(dw>width&&dh>height)
{
scale = Math.min(width*1.0f/dw, height*1.0f/dh);
}
//如果图片宽度和高度都比view的要小,则应该按最小的比例放大图片
if(dw<width&&dh<height)
{
scale = Math.min(width*1.0f/dw, height*1.0f/dh);
}
//如果仅仅是高度比view的大,则按照高度缩小图片即可
if(dw<width&&dh>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()
158 {
159
160 float[] values = new float[9];
161 scaleMatrix.getValues(values);
162
163 return values[Matrix.MSCALE_X];
164
165 } /**
* 缩放手势进行时调用该方法
*
* 缩放范围:initScale~maxScale
*/
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;
} }
红色部分是我们增加的代码。从代码中可以看到,ZoomImageView实现了OnScaleGestureListener和OnTouchListener接口。在第229行onTouch方法中,将触摸事件传递给了mScaleGestureDetector 。在mScaleGestureDetector 的onScale方法中,对缩放逻辑进行了编写。首先使用getDrawableScale获取到当前缩放比例,然后再获取到缩放因子。然后进行判断:如果当前缩放比例比最大比例小,且缩放因子大于1,说明想放大,这是被允许的,因为还还可以再放大。如果当前缩放比例比最小比例大,且缩放因子小于1,说明想缩小,这也是被允许的。利用Matrix的postScale方法设置缩放即可。其中的逻辑还是比较简单的。其他就没什么好解释的了,注释很清晰。那么效果如何呢?达到预期了吗?运行程序,效果如下:
由于多点触控在模拟上没法演示,因此zheli仍旧是一个手机上录制的gif。在真机上,我是用两根手指进行缩放的。可以看到,缩放的的效果是实现了。但是问题也出现了。什么问题呢?即缩放完成后,图片的位置不再居中了,与屏幕之间有了空隙。我们想要的显然不是这样子的效果。我们需要缩放完成后,图片位置不移动,而且缩放过程中,不允许与屏幕有空白间隙。那么怎么解决这个问题呢??限于篇幅,就放在下一篇文章中吧。如果你还接着往下做的话,请保存好代码,看下一篇文章《(二)弥补图片自由缩放出现的间隙》。点击下面的链接即可:
http://www.cnblogs.com/fuly550871915/p/4939954.html
(一)自定义ImageView,初步实现多点触控、自由缩放的更多相关文章
- cocos2d-x 多点触控实现缩放及相关问题的解决方法
首先,来看下代码: 声明文件: #ifndef __loading__MoreTouches__ #define __loading__MoreTouches__ #include <iostr ...
- android实现图片平铺效果&WebView多点触控实现缩放
1.图片平铺效果实现非常简单,只要在xml中添加一个 android:tileMode的属性就可以了.首先在drawable文件夹中添加自己的my.xml文件.代码: Java代码 <?xml ...
- android多点触控自由对图片缩放
在系统的相册中,观看相片就可以用多个手指进行缩放. 要实现这个功能,只需要这几步: 1.新建项目,在项目中新建一个ZoomImage.java public class ZoomImageView e ...
- (干货) Android实现ImageVIew多点触控及双击缩放
支持多点触控,放大自由移动,双击可以放大缩小.直接上代码: package com.cbt.view; import android.content.Context; import android.g ...
- 【朝花夕拾】Android自定义View篇之(八)多点触控(上)MotionEvent简介
前言 在前面的文章中,介绍了不少触摸相关的知识,但都是基于单点触控的,即一次只用一根手指.但是在实际使用App中,常常是多根手指同时操作,这就需要用到多点触控相关的知识了.多点触控是在Android2 ...
- 【朝花夕拾】Android自定义View篇之(九)多点触控(下)实践出真知
前言 在上一篇文章中,已经总结了MotionEvent以及多点触控相关的基础理论知识和常用的函数.本篇将通过实现单指拖动图片,多指拖动图片的实际案例来进行练习并实现一些效果,来理解前面的理论知识.要理 ...
- (五)多点触控之兼容ViewPager
在上一篇文章中,自定义的ZoomImageView已经实现了自由缩放,自由移动以及双击放大与缩小的功能.已经可以投入使用这个控件了.下面我们就在ViewPager中使用这个控件.如果你还没读过上一篇文 ...
- Android 多点触控与简单手势(一)
现在一般的Android手机都会使用电容触摸屏最少可以支持两点触摸,多的可能是七八个,所以基本上都会支持多点触控, android系统中应用程序可以使用多点触控的事件来完成各种手势和场景需求. And ...
- Android多点触控技术实战,自由地对图片进行缩放和移动
转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/11100327 在上一篇文章中我带着大家一起实现了Android瀑布流照片墙的效果, ...
随机推荐
- Python基础(6) - 基本语句
Python print(在Python 3.0中就变成了函数了) print语句是把对象用文本化的形式输出到标准的输出流上. Operation Interpretation print spam ...
- Nginx几个简单命令
• 启动nginx服务 sudo brew services start nginx 利用http://localhost:8080进行访问, 如果出现如下界面,说明启动成功. • 查看nginx版本 ...
- React.js 小书 Lesson20 - 更新阶段的组件生命周期
作者:胡子大哈 原文链接:http://huziketang.com/books/react/lesson20 转载请注明出处,保留原文链接和作者信息. 从之前的章节我们了解到,组件的挂载指的是将组件 ...
- ansible roles 目录规范
我的ansible roles项目的目录结构: (ansible_venv) [root@localhost ansible_home]# tree ansible_playbooks/ ansibl ...
- Azure 项目构建 – 部署高可用的 Python Web 应用
Python 以其优美,清晰,简单的特性在全世界广泛流行,成为最主流的编程语言之一.Azure 平台针对 Python 提供了非常完备的支持.本项目中,您将了解如何构造和部署基于 Azure Web ...
- 初学zookeeper--自定义事件监听
zk有四种节点类型: 持久节点,持久顺序节点,临时节点,临时顺序节点. 自定义监听事件时,在节点的创建,修改,删除的方法第一行都需要加入是否监听的一个方法: //开启监听的方法.第二个参数表示是否开启 ...
- UVA1339(字母映射)
memcmp(const void *buf1, const void *buf2, unsigned int count)可以比较两个串相等 http://baike.baidu.com/link? ...
- docker 容器启动并自启动redis
centos7.5 背景:每次开机后都要自动启动redis,也就是宿主机开机,启动容器,然后启动redis 按照网上的做法是:修改redis.conf ,修改redis的启动脚本(utils/...s ...
- webpack build后生成的app、vendor、manifest三者有何职能不同?
贴一下之前vue脚手架的webpack3配置: app.js是入口js,vendor则是通过提取公共模块插件来提取的代码块(webpack本身带的模块化代码部分),而manifest则是在vendor ...
- tp3.2开启允许跨域
在入口文件<?PHP下加上 header('Access-Control-Allow-Origin:*');header("Access-Control-Allow-Headers: ...