因为种种原因,需要在iphone应用中实现图片查看功能,由于iphone屏幕支持多点触摸,于是是想到用“手势”来实现图片的实时缩放和移动。借鉴无所不在的internet网络资料之后,终于实现此一功能,过程如下。
一、
新建MoveScaleImageView类,继承uiview。用于加载一个UIImage。它有两个主要的成员,一个UIImage对象用于指定一个内存图片,一个UIImageView控件用于显示图片。
@interface
UIImage*
UIImageView*
}
-(void)setImage:(UIImage*)_image;
@end
@implementation
-(id)initWithFrame:(CGRect)frame{
if
imageView=[[UIImageView
[self
//
[imageView
[imageView
}
return
}
-(void)dealloc{
originImage=nil;
imageView=nil;
[super
}
-(void)setImage:(UIImage
originImage=[[UIImage
[imageView
[imageView
//
}
@end
最主要的就是setImage方法。
MoveScaleImageView的使用很简单。在ViewController中构造一个MoveScaleImageView,然后用一个加载了图片文件的UIImage对象设置其image成员:
UIImage* image=[UIImage
MoveScaleImageView*
[fileContent
由于在这里我们没有对图片进行任何的缩放处理,对于小图片会位于屏幕的左上角,并在其他地方留下空白;对于尺寸大于屏幕的图片,则图片不能完全显示:
一、
要想识别手势(gesture),必须响应4个手势的通知方法(参考“iphone3开发基础教程”第13章的内容):
touchesBegan,touchesMoved,touchesEnded和touchesCancelled。
首先,我们先来考虑单点触摸情况,这比较简单一些。在单点触摸情况下,移动手指,imageView中的图片可以被拖动,这样,对于比较大的图片,我们可以通过拖动来浏览图片的各个部分,当然,对于能一次显示下全部的图片就不需要拖动了。
修改类MoveScaleImageView,在.h中增加一些声明:
@interface
UIImage*
UIImageView*
CGPoint
CGFloat
CGFloat
}
-(void)setImage:(UIImage*)_image;
-(void)moveToX:(CGFloat)x ToY:(CGFloat)y;
@end
然后实现touchesBegan和touchesMoved方法。
touchesBegan方法比较简单,记录下手指第一次触摸的位置。因为任何一个拖动都必然有一个起点和终点。
-(void)touchesBegan:(NSSet
UITouch
gestureStartPoint=[touch
//
}
然后是手指移动后回调的touchesMoved方法:
-(void)touchesMoved:(NSSet
UITouch* touch=[touches
CGPoint
//分别计算x,和y方向上的移动
offsetX=curr_point.x-gestureStartPoint.x;
offsetY=curr_point.y-gestureStartPoint.y;
//只要在任一方向上移动的距离超过Min_offset,判定手势有效
if(fabsf(offsetX)>=
[self
gestureStartPoint.x=curr_point.x;
gestureStartPoint.y=curr_point.y;
}
}
在这里我们做了一个简单的判断,只有手指移动了超过一定像素(min_offset常量)后,才识别为拖动手势,并调用moveToX方法。在这个方法中,需要不断的更新手指移动的坐标,因为这是一个连续的过程。
-(void)moveToX:(CGFloat)x ToY:(CGFloat)y{
//计算移动后的矩形框,原点x,y坐标,矩形宽高
CGFloat
curr_X=destX=curr_X-x;
curr_Y=destY=curr_Y-y;
destW=self.frame.size.width;
destH=self.frame.size.height;
if
curr_X=destX=0;
}
if
curr_Y=destY=0;
}
if
curr_X=destX=originImage.size.width-destW;
}
if
curr_Y=destY=originImage.size.height-destH;
}
//创建矩形框为本fame
CGRect
}
在这个方法中,我们采用了一种特殊的处理方式:截取大图片的一部分,并将截取部分显示在imageView里。我这样做的理由,是因为这是最简单、最容易的实现方式。我参考过网上的几种实现方式,发现基本上都需要使用Quartz2D API,并且实现起来要复杂得多。最终从闭路电视监控系统中得到了启发(想象一下,安保人员通过移动鼠标控制镜头移动的场景)。
我们设计了一个矩形框,用它作为模拟的镜头:
CGRect
同时还设计了一个全局变量用于记录图片缩放过程中的缩放倍率:
CGFloat
当跟踪到手指移动时,让“镜头”做反向运动(为什么是反向运动?因为我们模拟的是“拖动”效果,而不是“跟踪”效果,二者是恰恰相反的)。并通过
这样,移动操作实际上转换成了计算矩形框的位置。当然,我们也要做好边界判断,否则当矩形框超出图片原来的范围时,会发生扭曲缩放的现象。
接下来看怎样识别多点触摸。识别单点触摸和多点触摸其实非常简单,判断touchesBegan的touches参数的count属性即可:
-(void)touchesBegan:(NSSet
if
NSArray* twoTouches=[touches
originSpace=[self
FromPoint:[[twoTouches
}else
UITouch
gestureStartPoint=[touch
}
}
在上面的方法中,我们根据touches的count判断是否是单点触摸并进行分别的处理。对于2点触摸,我们记录了两指间的距离并记录在全局的CGFloat变量originSpace中。spaceToPoint方法是一个简单函数,使用中学中学过的3角函数计算2点间距离:
-(CGFloat)spaceToPoint:(CGPoint)first FromPoint:(CGPoint)two{//计算两点之间的距离
float
float
return
}
在两点触摸中,需要识别2个手势:外向捏合、内向捏合。通常前者使图像放大,而后者可使图像缩小。
在touchesMoved方法中,这样处理:
if
NSArray* twoTouches=[touches
CGFloat
//如果先触摸一根手指,再触摸另一根手指,则触发touchesMoved方法而不是touchesBegan方法
//此时originSpace应该是0,我们要正确设置它的值为当前检测到的距离,否则可能导致0除错误
if
originSpace=currSpace;
}
if
CGFloat
[self
}
}else
⋯⋯
}
}
先简单判断了是否为有效捏合(我们为此定义了一个常量min_offset),如果是,则计算手指有效移动长度和手势开始时的两指间距的商,以此作为缩放比例。然后调用scaleTo方法:
-(void)scaleTo:(CGFloat)x{
scale*=x;
//缩放限制:>=0.1,<=10
scale=(scale<0.1)?0.1:scale;
scale=(scale>10)?10:scale;
//重设imageView的frame
[self
}
这里,为防止用户无限制的对图像进行“捏合”操作,我们限制了scale的值在0.1-10之间(当然你可以将这个阀值定义为常量)。然后调用了一个原地的移动操作,即前面的moveTo方法。然而为支持缩放下的图片移动,这个方法被我们更改了:
-(void)moveToX:(CGFloat)x ToY:(CGFloat)y{
CGPoint
//重设镜头
[self
[imageView
}
其中更多的代码被我们移到了另一个方法resetLens中:
-(void)resetLens:(CGPoint)point{//设置镜头大小和位置
CGFloat
//===========镜头初始大小=========
width=self.frame.size.width/scale;
height=self.frame.size.height/scale;
//===========调整镜大小不得超过图像实际大小==========
if(width>originImage.size.width){
width=originImage.size.width;
}
if
height=originImage.size.height;
}
//计算镜头移动的位置(等比缩放)
x=lensRect.origin.x-point.x/scale;
y=lensRect.origin.y-point.y/scale;
//左边界越界处理
x=(x<0)?0:x;
//上边界越界处理
y=(y<0)?0:y;
//右边界越界
x=(x+width>originImage.size.width)?originImage.size.width-width:x;
//下边界越界处理
y=(y+height>originImage.size.height)?originImage.size.height-height:y;
//镜头等比缩放
lensRect=CGRectMake(x, y, width, height);
}
这些代码跟原来moveToX方法中的代码有些许的不同,主要是增加了对scale变量的引入,因为在缩放模式下,镜头的移动都是被scale系数缩放过的。通代码中的注释,我们不难理解整个代码。
这样,大图片经过“捏合”操作可以在屏幕上完全显示出来(上面原来基本看不清楚的第2张图片现在是一台苹果电脑):
当然,把小图片“捏合”放大成大图片也是可以的。此外通过手指的移动,能查看图片的不同部分。
在这个方法中,我们采用了一种特殊的处理方式:截取大图片的一部分,并将截取部分显示在imageView里。我这样做的理由,是因为这是最简单、最容易的实现方式。我参考过网上的几种实现方式,发现基本上都需要使用Quartz2D API,并且实现起来要复杂得多。最终从闭路电视监控系统中得到了启发(想象一下,安保人员通过移动鼠标控制镜头移动的场景)。
我们设计了一个矩形框,用它作为模拟的镜头:
CGRect
同时还设计了一个全局变量用于记录图片缩放过程中的缩放倍率:
CGFloat
当跟踪到手指移动时,让“镜头”做反向运动(为什么是反向运动?因为我们模拟的是“拖动”效果,而不是“跟踪”效果,二者是恰恰相反的)。并通过
这样,移动操作实际上转换成了计算矩形框的位置。当然,我们也要做好边界判断,否则当矩形框超出图片原来的范围时,会发生扭曲缩放的现象。
接下来看怎样识别多点触摸。识别单点触摸和多点触摸其实非常简单,判断touchesBegan的touches参数的count属性即可:
-(void)touchesBegan:(NSSet
if
NSArray* twoTouches=[touches
originSpace=[self
FromPoint:[[twoTouches
}else
UITouch
gestureStartPoint=[touch
}
}
在上面的方法中,我们根据touches的count判断是否是单点触摸并进行分别的处理。对于2点触摸,我们记录了两指间的距离并记录在全局的CGFloat变量originSpace中。spaceToPoint方法是一个简单函数,使用中学中学过的3角函数计算2点间距离:
-(CGFloat)spaceToPoint:(CGPoint)first FromPoint:(CGPoint)two{//计算两点之间的距离
float
float
return
}
在两点触摸中,需要识别2个手势:外向捏合、内向捏合。通常前者使图像放大,而后者可使图像缩小。
在touchesMoved方法中,这样处理:
if
NSArray* twoTouches=[touches
CGFloat
//如果先触摸一根手指,再触摸另一根手指,则触发touchesMoved方法而不是touchesBegan方法
//此时originSpace应该是0,我们要正确设置它的值为当前检测到的距离,否则可能导致0除错误
if
originSpace=currSpace;
}
if
CGFloat
[self
}
}else
⋯⋯
}
}
先简单判断了是否为有效捏合(我们为此定义了一个常量min_offset),如果是,则计算手指有效移动长度和手势开始时的两指间距的商,以此作为缩放比例。然后调用scaleTo方法:
-(void)scaleTo:(CGFloat)x{
scale*=x;
//缩放限制:>=0.1,<=10
scale=(scale<0.1)?0.1:scale;
scale=(scale>10)?10:scale;
//重设imageView的frame
[self
}
这里,为防止用户无限制的对图像进行“捏合”操作,我们限制了scale的值在0.1-10之间(当然你可以将这个阀值定义为常量)。然后调用了一个原地的移动操作,即前面的moveTo方法。然而为支持缩放下的图片移动,这个方法被我们更改了:
-(void)moveToX:(CGFloat)x ToY:(CGFloat)y{
CGPoint
//重设镜头
[self
[imageView
}
其中更多的代码被我们移到了另一个方法resetLens中:
-(void)resetLens:(CGPoint)point{//设置镜头大小和位置
CGFloat
//===========镜头初始大小=========
width=self.frame.size.width/scale;
height=self.frame.size.height/scale;
//===========调整镜大小不得超过图像实际大小==========
if(width>originImage.size.width){
width=originImage.size.width;
}
if
height=originImage.size.height;
}
//计算镜头移动的位置(等比缩放)
x=lensRect.origin.x-point.x/scale;
y=lensRect.origin.y-point.y/scale;
//左边界越界处理
x=(x<0)?0:x;
//上边界越界处理
y=(y<0)?0:y;
//右边界越界
x=(x+width>originImage.size.width)?originImage.size.width-width:x;
//下边界越界处理
y=(y+height>originImage.size.height)?originImage.size.height-height:y;
//镜头等比缩放
lensRect=CGRectMake(x, y, width, height);
}
这些代码跟原来moveToX方法中的代码有些许的不同,主要是增加了对scale变量的引入,因为在缩放模式下,镜头的移动都是被scale系数缩放过的。通代码中的注释,我们不难理解整个代码。
这样,大图片经过“捏合”操作可以在屏幕上完全显示出来(上面原来基本看不清楚的第2张图片现在是一台苹果电脑):
在ios开发中,肯定会碰到需要截取部分图片的情况。
最终的效果类似这样:
先看最原始的示例,显示完整的图片
写了个最简单的读取图片并显示的代码,打算以此为开始,逐渐实现截取部分图片的功能。
代码主要是,在控制器代码中:
- (void)loadView {
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation: UIStatusBarAnimationSlid e];
UIImage *image=[UIImage imageNamed:@"1.jpg"];
UIImageView *contentView = [[UIImageView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
[contentView setImage:image];
self.view=[[UIView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
[self.view addSubview:contentView];
}
另外,应该有一个名为1.jpg的768×1024的图片(我这里是iPad)。
截取整个图片
可以认为截取整个图片是截取部分图片的一个特例。对ios不熟嘛,因此打算很谨慎的推进。截取整个图片可以减少中间的复杂性。
根据API,摸索着写了一个示例,效果出乎意料:
代码:
- (void)loadView {
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation: UIStatusBarAnimationSlid e];
UIImage *image=[UIImage imageNamed:@"1.jpg"];
UIImageView *contentView = [[UIImageView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
//[contentView setImage:image];
CGRect rect = CGRectMake(0, 0, 768, 1024);//创建矩形框
UIGraphicsBeginImageCont ext(rect.size);//根据size大小创建一个基于位图的图形上下文
CGContextRef currentContext = UIGraphicsGetCurrentCont ext();//获取当前quartz 2d绘图环境
CGContextClipToRect( currentContext, rect);//设置当前绘图环境到矩形框
CGContextDrawImage(currentContext, rect, image.CGImage);//绘图
UIImage *cropped = UIGraphicsGetImageFromCu rrentImageContext();//获得图片
UIGraphicsEndImageContex t();//从当前堆栈中删除quartz 2d绘图环境
contentView.image=cropped;
self.view=[[UIView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
[self.view addSubview:contentView];
[cropped release];
}
这个代码说明了两点:
- 好的方面:说明我的代码起作用了,确实截取了所需的图形
- 坏的方面:图形是颠倒的,而且是镜像的。
问题应该出在坐标系上。下面画了一个quartz 2d的坐标系,坐标原点在左下角:
因此以这个坐标系取图形,就会有转向180°的效果。
其实如果是对图片的缩放,而不是剪切部分图片内容,这样写就可以了:
- (void)loadView {
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation: UIStatusBarAnimationSlid e];
UIImage *image=[UIImage imageNamed:@"1.jpg"];
//[contentView setImage:image];
CGRect rect = CGRectMake(0, 0, 384, 512);//创建矩形框
UIGraphicsBeginImageCont ext(rect.size);//根据size大小创建一个基于位图的图形上下文
CGContextRef currentContext = UIGraphicsGetCurrentCont ext();//获取当前quartz 2d绘图环境
CGContextClipToRect(currentContext, rect);//设置当前绘图环境到矩形框
//CGContextRotateCTM(currentContext, 50);
//CGContextDrawImage(currentContext, rect, image.CGImage);//绘图
[image drawInRect:rect];
UIImage *cropped = UIGraphicsGetImageFromCu rrentImageContext();//获得图片
UIGraphicsEndImageContex t();//从当前堆栈中删除quartz 2d绘图环境
UIImageView *contentView = [[UIImageView alloc] initWithFrame:rect];
contentView.image=cropped;
self.view=[[UIView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
[self.view addSubview:contentView];
[cropped release];
}
效果类似这样:
这个方法可以帮助我们在后续开发中实现缩略图。但是不符合现在的需求。
于是想了下面的基本思路:
这样,需要一个能旋转和向下移动的API。ios提供了C++界面的函数调用:
- CGContextRotateCTM,实现角度的转换
- CGContextTranslateCTM,可以重新设置坐标系原点,平移坐标系和移动图片是等效的
代码:
- (void)loadView {
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation: UIStatusBarAnimationSlid e];
UIImage *image=[UIImage imageNamed:@"1.jpg"];
//[contentView setImage:image];
CGRect rect = CGRectMake(0, 0, 384, 512);//创建矩形框
UIGraphicsBeginImageCont ext(rect.size);//根据size大小创建一个基于位图的图形上下文
CGContextRef currentContext = UIGraphicsGetCurrentCont ext();//获取当前quartz 2d绘图环境
CGContextClipToRect(currentContext, rect);//设置当前绘图环境到矩形框
CGContextRotateCTM(currentContext, M_PI);
CGContextTranslateCTM(currentContext, -rect.size.width, -rect.size.height);
CGContextDrawImage(currentContext, rect, image.CGImage);//绘图
//[image drawInRect:rect];
UIImage *cropped = UIGraphicsGetImageFromCu rrentImageContext();//获得图片
UIGraphicsEndImageContex t();//从当前堆栈中删除quartz 2d绘图环境
UIImageView *contentView = [[UIImageView alloc] initWithFrame:rect];
contentView.image=cropped;
self.view=[[UIView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
[self.view addSubview:contentView];
[cropped release];
}
这个结果还有缺陷,可以看到图片是正立的了,但是图片反转了,是个镜像。
解决办法也有,不过不是操作图片了,而是操作图片所在的视图。思路是把视图看作一个位图的矩阵,对它做矩阵变换运算,使视图做镜像反转。写法很简单:
- (void)loadView {
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation: UIStatusBarAnimationSlid e];
UIImage *image=[UIImage imageNamed:@"1.jpg"];
//[contentView setImage:image];
CGRect rect = CGRectMake(0, 0, 384, 512);//创建矩形框
UIGraphicsBeginImageCont ext(rect.size);//根据size大小创建一个基于位图的图形上下文
CGContextRef currentContext = UIGraphicsGetCurrentCont ext();//获取当前quartz 2d绘图环境
CGContextClipToRect(currentContext, rect);//设置当前绘图环境到矩形框
CGContextRotateCTM(currentContext, M_PI);
CGContextTranslateCTM(currentContext, -rect.size.width, -rect.size.height);
//CGContextTranslateCTM(currentContext,0.0,200.0);
CGContextDrawImage(currentContext, rect, image.CGImage);//绘图
//[image drawInRect:rect];
UIImage *cropped = UIGraphicsGetImageFromCu rrentImageContext();//获得图片
UIGraphicsEndImageContex t();//从当前堆栈中删除quartz 2d绘图环境
UIImageView *contentView = [[UIImageView alloc] initWithFrame:rect];
contentView.image=cropped;
contentView.transform = CGAffineTransformIdentit y;
contentView.transform = CGAffineTransformMakeSca le(-1.0, 1.0);
self.view=[[UIView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
[self.view addSubview:contentView];
[cropped release];
}
这里的转换因子,一个是针对x轴的,一个是针对y轴的。终于可以产生这样的效果了:
这里参考了这个文档:
虽然是很古老的文章了,但是说的很清楚。另外,方法名称已经发生变化,需要注意。
截取部分图片
截取部分图片,比如:
截取左边人像部分。
实现后的代码,效果是这样的:
如何实现的呢,这时候才发现,其实根本不需要上面那些转换,如果不使用quartz 2d的话,截取部分图片这么简单:
- (void)loadView {
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation: UIStatusBarAnimationSlid e];
UIImage *image=[UIImage imageNamed:@"1.jpg"];
CGRect rect = CGRectMake(60, 80, 331, 353);//创建矩形框
UIImageView *contentView = [[UIImageView alloc] initWithFrame:rect];
contentView.image=[UIImage imageWithCGImage:CGImageCreateWithImageIn Rect([image CGImage], rect)];
self.view=[[UIView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
[self.view addSubview:contentView];
[image release];
}
虽然编写代码的过程是曲折的,但是摸到很多有用的东西,都是以后要用到的。
对图片进行处理(得到某张图片的一部分)可一用以下代码:
UIImage *image = [UIImage imageNamed:filename];
CGImageRef imageRef = image.CGImage;
CGRect rect = CGRectMake(origin.x, origin.y ,size.width, size.height);
CGImageRef imageRefRect = CGImageCreateWithImageIn
UIImage *imageRect = [[UIImage alloc] initWithCGImage:imageRefRect];
把当前的视图作为照片保存到相册中去:
#import <QuartzCore/QuartzCore.h>
UIGraphicsBeginImageCont
[currentView.layer renderInContext:UIGraphicsGetCurrentCont
UIImage *viewImage = UIGraphicsGetImageFromCu
UIGraphicsEndImageContex
UIImageWriteToSavedPhoto
使UIimageView的图像旋转:
float rotateAngle = M_PI;
CGAffineTransform transform =CGAffineTransformMakeRot
imageView.transform = transform;