Core Audio

五月 17th, 2016 No comments 99 views

在音乐制作方面,苹果的系统公认是最好的,我们经常在各种片里看到拿个Mac的本本在那里进行音乐的处理,还有各种DJ,打碟的时候旁边也摆个Mac的本本。咱们大家应该都不是搞音乐的,所以苹果的系统比别人的好在哪里,我们也说不清,但我们能知道的是,苹果在声音以及音乐处理方面,底层的实现都是Core Audio,包括iOS。Core Audio是C的,所以能直接在ObjectiveC和C++中直接使用。另外那些可以调用C库的语言也能使用,比如Java,Ruby,Python这些。

Core Audio由于是基于C语言的,所以在调用它的API时,方式和我们平常iOS开发的OC,Swift不同,是典型的过程性语法。

Core Audio作为苹果的音乐处理引擎,主要分为以下几个大的组成部份:
1. Audio Units:这部分的API会直接处理音乐或声音数据,它会使用各种各样的buffer,比如从录音设备中获取的数据,经过处理后,讲结果丢给另一个audio unit。处理的方式是使用各种回调函数,用户的具体操作就在这个回调中实现。
2. Audio Queues:这部分算是在Audio Units之上的一层应用,这将是用户能够方便的实现录制,播放等功能,而不必自己去处理一些线程,IO,播放时序这些恼人的事情。
3. OpenAL:这算是定义了一个声音方面的工业标准,他能让用户实现声音的3D效果,比如环绕立体声这些。这部分也是在audio unit之上的一层应用,在游戏中会用到很多这方面的东东。

上面是Core Audio的数据处理引擎,在声音数据读取方面,Core Audio也提供了很多helper API,它也能分为几个大类
1. Audio File Services:看名字就能知道,它包含了基于本地文件的声音读取操作。其实在声音处理方面,有各种各样很多不同的音频格式,比如我们常见的mp3,wav,aiff这些,Audio File Services都能帮你处理,同时也能帮用户节省了很大的时间和精力。
2. Audio File Stream Services:这主要是用在网络环境下的,比如我们会听很多在线播放,网络广播这些,实际的音频数据都是以流(stream)的方式提供给用户的,这里就是用来处理这些的。
3. Audio Converter Services:在音频处理中,我们经常需要进行格式转换,比如在mp3和wav格式之间进行互转,这些操作都要用到它。
4. Extended Audio File Services:这部分的东西算是1和3的升级版,也就是能让你在通过文件方式读写音频数据的同时,实现音频格式的转换。
5. Core MIDI:音频录制时,出去我们常用的通过麦克风,其实很多也是来自于MIDI设备,比如MIDI钢琴,MIDI吉他等等,据说这是Mac才独有的功能,也许这就是很多音乐人钟情于Mac系统的原因吧。
6. Audio Session Services:这个是专门针对iOS的,主要用来处理iOS中音频资源的使用,它通过使用各种iOS中定义好的category,来实现用户的某些特定操作。比如当屏幕锁频时,你的App的声音是否继续播放;当你打开一个App时,突然有个电话拨进来,你的声音该如何处理等等。

下面我们来说一说计算机是如何处理音频信息的。

我们都知道,声音在空气中传播是,是一段连续的震荡波,已x,y坐标系来表示的话,x轴相当于时间,y轴就是某个时间点时音波相对于振幅最大值的比例。对计算机来说,要表这种曲线,我们可以通过在这段曲线的各个点进行数据采集,这种方法在计算机领域称之为pulse code modulation(脉冲编码调制),一般来说,都是按照x轴,也就是根据时间来对音频信息进行采集,如果是按照等分时间的方式,通常叫做linear pulse code modulation(LPCM),这个术语在音频处理中我们会经常碰到,所以在这里说明一下。这里的音频信息采集也有一个特定的术语,叫做采样(sampling)。

对于我们常见的CD音质的音频信息采样来说,使用的采样率是44.1KHz,也就是说,每秒钟会进行44100次的音频信息采样。也就是说,对于CD音质的音频信息,每23微秒就进行一次采样。在这23微秒的间隙,相应的音频信息则会丢失。
为什么采用44.1KHz的采样率,是因为根据香农采样定律,如果你要采集一段震荡波的信息,这段波的最大频率是B Hz,那么你使用的采样率要达到1/(2B),也就是1/(2B)秒就采样一次,才会尽可能的保持这段波的准确性。对于我们人耳来说,可以听到的频率范围是在20~20KHz之间,所以才用44.1KHz的采样率相对于人耳的听觉范围是比较合适的。

另外再说一下在Core Audio中经常会用的几个术语,sample,frame,channel和packet。

一个sample就是一个采样点的信息,对于x,y坐标来说,就是一个(x,y)的值。
channel就是声道的意思,一个channel在一个时间点上只有一个sample信息,如果包含多个声道,将有多个channel。在音频数据处理中,把某个时间点上的所有channel打包到一起,称之为一个frame,因为在播放时,多声道的音频数据需要同时进行处理,这样的大包能够让这些处理变得更容易。
再往上就是packet的概念,一个packet是把多个frame进行打包。
所以从大到小来说,分别是packet,frame,channel,sample。这些基本概念构成了整个计算机音频处理的基础,包含各个音频数据单元,音频格式信息等。

不过LPCM格式的音频没有用到packet,因为他是一种非压缩的音频数据,但对于一些音频压缩格式来说,会用到packet。

采样率对于所有的音频格式来说,并不一定是不变的。PCM的格式会使用恒定的采样率(constant bit rate, CBR),也就是说如果采样率为44.1KHz,那么这个采样率将一只是这个值。当然也会有变换的采样率(variable bit rate, VBR),通常在遇到VBR的情况是,Core Audio的API会处理VBR的格式,处理的方式一般来说为,给你一段数据,然后额外提供一个包含packet description的数组,告诉你这一段数据中每一个packet的数据情况,然后进行数据的处理。所以对于CBR的情况来说,是没有这些的。

Categories: Core Audio 标签:

renderInContext

五月 5th, 2016 No comments 116 views

在iOS的动画处理中,我们经常会碰到一些页面切换的效果,比如新页面代替旧的页面,为了实现某些特定的效果,我们需要把旧页面先保存下来,然后经过一定的动画处理,过渡到新的页面,这种情况下,renderInContext就派上用场了。

renderInContext可以通过把当前页面内容画到Core Graphics的context,来实现类似于截图的功能。

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.button = [UIButton buttonWithType:UIButtonTypeCustom];
    self.button.frame = CGRectMake(0, 0, 200, 40);
    self.button.center = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds));
    [self.button setTitle:@"Perform Transition" forState:UIControlStateNormal];
    [self.button setBackgroundColor:[UIColor blueColor]];
    self.button.layer.cornerRadius = 5.0f;
    self.button.layer.masksToBounds = YES;
    [self.button addTarget:self action:@selector(performTransition) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:self.button];
}

- (void)performTransition
{
    // Preserve the current view snapshot
    UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, YES, 0.0);
    [self.view.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *coverImage = UIGraphicsGetImageFromCurrentImageContext();

    // Insert snapshot view in front of this one
    UIView *coverView = [[UIImageView alloc] initWithImage:coverImage];
    coverView.frame = self.view.bounds;
    [self.view addSubview:coverView];

    // Update the view (well simply randomize the layer background color)
    CGFloat red = arc4random() / (CGFloat)INT_MAX;
    CGFloat green = arc4random() / (CGFloat)INT_MAX;
    CGFloat blue = arc4random() / (CGFloat)INT_MAX;
    self.view.backgroundColor = [UIColor colorWithRed:red
                                                green:green
                                                 blue:blue alpha:1.0];

    // Perform animatioin
    [UIView animateWithDuration:1.0 animations:^{
        // Scale, rotate and fade the view
        CGAffineTransform transform = CGAffineTransformMakeScale(0.01, 0.01);
        transform = CGAffineTransformRotate(transform, M_PI_2);
        coverView.transform = transform;
        coverView.alpha = 0.0;
    }completion:^(BOOL finished) {
        [coverView removeFromSuperview];
    }];
}

效果就是下面的样子

Categories: iOS 标签:

CAKeyFrameAnimation

五月 4th, 2016 No comments 104 views

KeyFrameAnimation来源于动画产业,最早做动画片的时候,会分为几个角色,牛点的人会根据剧情等因素把场景中的关键画面画出来,剩下的不那么牛的人则根据这些关键画面把一些过渡性的图片补足,假设一秒需要20张图片,关键图片可能有5张,剩下的15张则根据这5张的内容来补齐,这就是最早的key frame的来源。其实在视频编解码中也经常会碰到类似的属于,比如KeyFrame和I帧P帧这些名词,都是跟这些有关的。

在iOS中,如果你使用CAKeyFrameAnimation,则需要提供一些关键点,然后iOS在显示的过程中会根据这些信息自动补齐过渡性的内容。

我们可以用下面一个飞机飞行线路的例子来说明

- (void)drawSpaceCurve
{
    // Create a path
    UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
    [bezierPath moveToPoint:CGPointMake(0, 150)];
    [bezierPath addCurveToPoint:CGPointMake(300, 150)
                  controlPoint1:CGPointMake(75, 0)
                  controlPoint2:CGPointMake(225, 300)];

    // Draw the path using CAShapeLayer
    CAShapeLayer *pathLayer = [CAShapeLayer layer];
    pathLayer.path = bezierPath.CGPath;
    pathLayer.fillColor = [UIColor clearColor].CGColor;
    pathLayer.strokeColor = [UIColor redColor].CGColor;
    pathLayer.lineWidth = 3.0f;
    [self.view.layer addSublayer:pathLayer];

    // Add the ship
    CALayer *shipLayer = [CALayer layer];
    shipLayer.frame = CGRectMake(0, 0, 64, 64);
    shipLayer.position = CGPointMake(0, 150);
    shipLayer.contents = (__bridge id)[UIImage imageNamed:@"Ship"].CGImage;
    [self.view.layer addSublayer:shipLayer];

    // Create the keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 4.0f;
    animation.path = bezierPath.CGPath;
    [shipLayer addAnimation:animation forKey:nil];
}

在代码运行中,我们确实看到了飞机的运行轨迹和我们定义的bezierPath一致,但有个问题,就是飞机的机头方向没有在运行过程中做调整,这便使动画看起来怪怪的,不过好在Apple已经预见到这个需求,所以在CAKeyFrameAnimation中提供了一个名为rotationMode的属性,当我们把这个属性设置为kCAAnimationRotateAuto时,飞机在运行过程中机头始终超前,实现了我们的需求。

animation.rotationMode = kCAAnimationRotateAuto;

另外在实际的动画应用中,我们经常会使用到多个动画效果一同展示的情况,这种情况我们可以通过CAAnimationGroup来实现,它的用法和单独设置一个动画的效果没有太大的区别,但当你把多种效果组合到一起,在展示的时候,就可以轻松完成多个效果的同时展示。

我们还是用上面的轨迹来演示,不过把飞机换成了一个色块,动画是使色块不但按照我们规定好的路径运动,并且在运动中进行颜色的变换。

- (void)testAnimationGroup
{
    UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
    [bezierPath moveToPoint:CGPointMake(0, 150)];
    [bezierPath addCurveToPoint:CGPointMake(300, 150)
                  controlPoint1:CGPointMake(75, 0)
                  controlPoint2:CGPointMake(225, 300)];

    CAShapeLayer *pathLayer = [CAShapeLayer layer];
    pathLayer.path = bezierPath.CGPath;
    pathLayer.fillColor = [UIColor clearColor].CGColor;
    pathLayer.strokeColor = [UIColor redColor].CGColor;
    pathLayer.lineWidth = 3.0f;
    [self.view.layer addSublayer:pathLayer];

    CALayer *colorLayer = [CALayer layer];
    colorLayer.frame = CGRectMake(0, 0, 64, 64);
    colorLayer.position = CGPointMake(0, 150);
    colorLayer.backgroundColor = [UIColor greenColor].CGColor;
    [self.view.layer addSublayer:colorLayer];

    CAKeyframeAnimation *animation1 = [CAKeyframeAnimation animation];
    animation1.keyPath = @"position";
    animation1.path = bezierPath.CGPath;
    animation1.rotationMode = kCAAnimationRotateAuto;

    CABasicAnimation *animation2 = [CABasicAnimation animation];
    animation2.keyPath = @"backgroundColor";
    animation2.toValue = (__bridge id)[UIColor redColor].CGColor;

    CAAnimationGroup *group = [CAAnimationGroup animation];
    group.animations = @[animation1, animation2];
    group.duration = 4.0f;

    [colorLayer addAnimation:group forKey:nil];
}

Categories: iOS 标签:

AVPlayerLayer

四月 23rd, 2016 1 comment 202 views

AVPlayerLayer是一个CALayer的subclass,它是属于另一个framework,AVFoundation framework。它主要用来在iOS中播放视频内容,实际上,例如MPMoviePlayer里面的底层播放实现用的都是它。使用起来很简单,1、创建实例;2、播放操作

我们可以用下面这个简单的例子演示一下这个的用法

- (void)testPlayerLayer
{
    UIView *containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
    containerView.center = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds));
    [self.view addSubview:containerView];

    // get video url
    NSURL *url = [[NSBundle mainBundle] URLForResource:@"Ship" withExtension:@"mp4"];

    // Create player and player layer, need <AVFoundation/AVFoundation.h>
    AVPlayer *player = [AVPlayer playerWithURL:url];
    AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];

    // Set player frame and attach to our view
    playerLayer.frame = containerView.bounds;
    [containerView.layer addSublayer:playerLayer];

    [player play];
}

效果就如下图所示

使用方法就是这样,在这里要说的是,AVPlayerLayer既然是一个CALayer的subclass,那么我们就可以像对待一个普通的CALayer来对待它,使用CALayer的一些方法,比如transform,roundedCorner这些效果都可以加在它上面,还是用例子说话。

- (void)test3DPlayerLayer
{
    UIView *containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
    containerView.center = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds));
    [self.view addSubview:containerView];

    // get video url
    NSURL *url = [[NSBundle mainBundle] URLForResource:@"Ship" withExtension:@"mp4"];

    // Create player and player layer, need <AVFoundation/AVFoundation.h>
    AVPlayer *player = [AVPlayer playerWithURL:url];
    AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];

    // Set player frame and attach to our view
    playerLayer.frame = containerView.bounds;
    [containerView.layer addSublayer:playerLayer];

    // Transform layer
    CATransform3D transform = CATransform3DIdentity;
    transform.m34 = -1.0 / 500.0f;
    transform = CATransform3DRotate(transform, M_PI_4, 1, 1, 0);
    playerLayer.transform = transform;

    // Add rounded corners and border
    playerLayer.masksToBounds = YES;
    playerLayer.cornerRadius = 20.0f;
    playerLayer.borderColor = [UIColor redColor].CGColor;
    playerLayer.borderWidth = 5.0f;

    [player play];
}

经过3D变换,加圆角,加边框后的效果如下

代码不难,不过我要解释一下下面这几句关于3D变换的操作

    CATransform3D transform = CATransform3DIdentity;
    transform.m34 = -1.0 / 500.0f;
    transform = CATransform3DRotate(transform, M_PI_4, 1, 1, 0);
    playerLayer.transform = transform;

3D变换在iOS中是通过使用矩阵乘法运算来得出最终各个坐标点的,原理是下面的图

矩阵乘法是怎么运算的就不在这里讲了,网上很容易找到,另外就是在iOS设备上xyz各个轴的定义,以及如何围绕的轴来旋转

在iOS提供的transform的API中,不需要我们直接来进行矩阵运算,只需要我们指定旋转所需要的参数,其中有角度(单位是弧度),以及是按照哪个轴进行旋转。
常用的有下面两个函数

CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
transform = CATransform3DRotate(transform, M_PI_4, 1, 1, 0);

第一个函数是从基本面开始,进行的绝对值旋转,4个参数,分别代表了角度,x,y,z,这里是按照y轴旋转了PI/4, PI是180度,也就是相当于旋转了45度,x和z的值为0,也就是不旋转的意思。
第二个函数是在第一个transform的基础上继续进行旋转,旋转是既在x轴旋转,也在y轴旋转。

如果大家实验一下,发现其实旋转后的效果怪怪得,

比如只执行第一个函数的话,可能觉得图片仅仅变窄了(旋转前的原图是个正方形),看不出有旋转的感觉。这是因为没有使用Perspective Projection的原因,Perspective Projection在用户看来,应该是离我们近的,会大一些,离我们远得的看上去会比较小才正确。这个效果是否体现出来,其实是由矩阵中的m34值来决定的

也就是红圈圈里面的那个,这个值可以设置为 -1/d,d是相当于我们的眼睛与图片的距离,一般来说,对于iOS的应用,我们使用的d值的范围在500~1000之间,值越小,效果越明显,越大,效果越不明显,我们可以看一下m34的设置和使用方法

CATransform3D transform = CATransform3DIdentity;
//apply perspective
transform.m34 = - 1.0 / 500.0;
//rotate by 45 degrees along the Y axis
transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0);
//apply to layer
self.layerView.layer.transform = transform;


这回效果出来了吧!
CATransform3DIdentity,这个值在矩阵运算中,相当于什么都没干,也就是说,任意一个坐标跟CATransform3DIdentity做乘法运算时,计算后的结果不会变化。

有了这些说明,文章中第一个例子的各条语句在理解上应该都没有问题了。

Categories: iOS 标签:

CAShapeLayer

四月 19th, 2016 2 comments 216 views

在iOS中,一个UIView已经有了CALayer,可是又有一个CAShapeLayer的实现,这个类是CALayer的子类,重新实现了一个CAShapeLayer的原因有下面的这些原因
1. CAShapeLayer使用了硬件加速的方式来绘图,所以比Core Graphics的绘图速度更快
2. CAShapeLayer的内存消耗更少
3. CAShapeLayer中作图可以画到它的边界外面,CALayer中作图的话只会显示layer边界以内的内容
4. pixel和point的坐标系转化

CAShapeLayer可以很方便的使用CAPathRef类型,没必要像CALayer中那么复杂的需要使用类似于CAPathRect, CGContextSetLineWidth这些看似比较复杂的函数。

在CGContext下,可以创建一个Path,这个Path由用户自己定义图形的样子。在CALayer的常用方法中,有cornerRadius属性,定义了一个矩形的圆角模式,但是同时影响了4个交,在path中,我门当然可以画出一个不是四个角都是圆角的矩形,比如下面这个例子

    CGRect rect = CGRectMake(50, 50, 100, 100);
    CGSize radii = CGSizeMake(15, 5);
    UIRectCorner corners = UIRectCornerTopRight | UIRectCornerBottomRight | UIRectCornerBottomLeft;

    UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect
                                               byRoundingCorners:corners
                                                     cornerRadii:radii];

    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.strokeColor = [UIColor redColor].CGColor;
    shapeLayer.fillColor = [UIColor clearColor].CGColor;
    shapeLayer.lineWidth = 5;
    shapeLayer.lineJoin = kCALineJoinRound;
    shapeLayer.lineCap = kCALineCapRound;
    shapeLayer.path = path.CGPath;

    [self.view.layer addSublayer:shapeLayer];

这段代码,我们画了一个只有三个圆角的矩形,另外,定义strokeColor,fillColor,lineWidth这些操作都方便了很多,但有一点要注意,这些属性一旦定义,则在这个layer上的所以操作都将使用这个属性,如果你想使用不同的属性,比如线条颜色这些,那你只有使用一个新的CAShapeLayer了。

画出来的图形效果如下

当然,这个CAShapeLayer也可以当做maskLayer来使用,在文章http://shellcodes.sinaapp.com/?p=526中可以看到用法,这样就可以制作出符合自己风格的图片显示效果

Categories: 测试类 标签:

CALayer的masking显示

四月 12th, 2016 No comments 200 views

CALayer有一个BOOL属性值是masksToBounds,当这个值为TRUE时,他的superLayer中的内容会根据bounds值进行裁剪,也就是说在bounds内的内容会显示,否则不显示,配合着CALayer的cornerRadius则可以完成诸如圆角之类的效果。

但有时仅仅圆角的效果是不够用的,因为这些算事在rect效果上的一些小修改,如果有别的需求,比如一个五角星的相片框,只用上面两个值是无法实现的。

在这种情况下,CALayer的mask就有用了。CALayer的mask属性也是一个CALayer,如果我们给这个CALayer赋值为一张PNG的图片,则UIKit会计算出这个PNG图片的边缘区域,结果就是所有在这个区域内的内容会显示,否则不显示。

当我们使用如下代码时,会在屏幕中央显示一个黑色的图片

    UIImage *image = [UIImage imageNamed:@"Black"];

    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
    imageView.center = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds));
    imageView.image = image;

    [self.view addSubview:imageView];

如下

如果我们加上一个五角星的maskLayer,就完成了我们需要的那种五角星边框效果,修改后的代码如下

- (void)drawMaskedImage
{
    UIImage *image = [UIImage imageNamed:@"Black"];

    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
    imageView.center = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds));
    imageView.image = image;

    [self.view addSubview:imageView];

    CALayer *maskLayer = [[CALayer alloc] init];
    maskLayer.frame = imageView.bounds;
    UIImage *maskImage = [UIImage imageNamed:@"Star"];
    maskLayer.contents = (__bridge id _Nullable)(maskImage.CGImage);
    imageView.layer.mask = maskLayer;
}

效果呢,就是刚才所说的那种

Categories: iOS 标签:

CALayer中的anchorPoint

四月 8th, 2016 No comments 123 views

CALayer是封装在UIView中的,为什么要这样封装是为了使代码能够复用在OS X上,UIView上面的界面操作实际上都是在CALayer上完成的,只不过UIView中能够支持app中的各种事件,以及各种touch gesture的响应。在OS X编程中,类似的是NSView,支持一些在PC上才会有的特定的操作。但无论UIView还是NSView,底层的都是CALayer,这样Apple下很多界面的实现都可以统一起来。

在iOS中,UIView封装了很多方法可以让用户直接操作界面,是用户意识不到CALayer的存在,但有时需要用户直接访问UIView的CALayer,因为有些事情在UIView中是无法实现的,比如
1. Shadow,圆角,有色边框
2. 3D变换
3. 非矩形的形状
4. mask bounds
5. 非线性动画等

要描绘一个view的位置信息,有frame,bounds和center;
同样,要描绘一个layer(CALayer类型)的位置信息,用的是frame,bounds,position和anchorPoint

在这里,view的center和layer的position意义相同,但它们之间是有关联的,你如果改变了view的frame,相应的layer的frame,position等信息也会同步改变。

如果对一个图形进行旋转操作,那么这个图形的旋转点是按照layer的position来定位的。

现在说说anchorPoint,anchorPoint的默认值是和position相同的,但如果改变anchorPoint的值,图片的显示位置会根据anchorPoint的值重新放置,也就是图片根据你的anchorPoint进行了位移。

其实UIView中,如果用户直接修改frame或center值,也可以实现类似的效果,但为什么还会有一个anchorPoint呢,我们可以用下面的例子来说明anchorPoint的功能是frame或center无法替代的。

我们的例子是实现一个时钟,里面除了表盘还会有时针,分针和秒针,图片如下

我们先看看习惯的处理办法


- (void)viewDidLoad
{
    [super viewDidLoad];

    [self setupClockFace];

    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f
                                                  target:self
                                                selector:@selector(tick)
                                                userInfo:nil
                                                 repeats:YES];
}

- (void)setupClockFace
{
    self.clockFace = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"ClockFace"]];
    self.hourHand = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"HourHand"]];
    self.minuteHand = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"MinuteHand"]];
    self.secondHand = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"SecondHand"]];

    CGPoint center = CGPointMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds));

    self.clockFace.center = center;
    self.hourHand.center = center;
    self.minuteHand.center = center;
    self.secondHand.center = center;

    [self.view addSubview:self.clockFace];
    [self.view addSubview:self.hourHand];
    [self.view addSubview:self.minuteHand];
    [self.view addSubview:self.secondHand];

    self.view.backgroundColor = [UIColor lightGrayColor];
}

- (void)tick
{
    // convert time to hours, minutes and seconds
    NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];

    NSCalendarUnit units = NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond;

    NSDateComponents *components = [calendar components:units fromDate:[NSDate date]];

    // calculate hour hand angle
    CGFloat hoursAngle = (components.hour / 12.0) * M_PI * 2.0;

    CGFloat minsAngle = (components.minute / 60.0) * M_PI * 2.0;

    CGFloat secsAngle = (components.second / 60.0) * M_PI * 2.0;

    // rotate hands
    self.hourHand.transform = CGAffineTransformMakeRotation(hoursAngle);
    self.minuteHand.transform = CGAffineTransformMakeRotation(minsAngle);
    self.secondHand.transform = CGAffineTransformMakeRotation(secsAngle);
}

这样的代码下显示出来的效果如下

仔细看发现针的位置是在最中间,但这和我们实际生活中的习惯不是很相同,我们希望指针是按照针尾部进行旋转,我们当然可以把指针的图片做长,另外一部分使用透明的模式,但这样一是浪费存储空间,二是加载的图片更大,更浪费内存。我们为了实现这个效果,可以通过修改anchorPoint的值来实现


- (void)viewDidLoad
{
    [super viewDidLoad];

    [self setupClockFace];

    [self adjustClockHands];

    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f
                                                  target:self
                                                selector:@selector(tick)
                                                userInfo:nil
                                                 repeats:YES];
}

- (void)adjustClockHands
{
    self.hourHand.layer.anchorPoint = CGPointMake(0.5, 0.9);
    self.minuteHand.layer.anchorPoint = CGPointMake(0.5, 0.9);
    self.secondHand.layer.anchorPoint = CGPointMake(0.5, 0.9);
}

这样操作后,我们得到的效果就成这样了,应该就是我们需要的了

这里有亮点需要额外注意,第一个就是anchorPoint的单位,他不是point或者pixel值,而是一个unit值,也就是相当于width或者height的比例,所以可以看到CGPointMake(0.5, 0.9)这样的写法;另一个是anchorPoint虽然改变了,但layer的position值并不受影响,在指针transform的时候,仍然是按照position的值进行旋转的(默认position和anchorPoint是相同的)

Categories: iOS 标签:

iOS上最简单的动画处理

四月 7th, 2016 No comments 90 views

这里介绍一下iOS上最简单的两种类型,平移和旋转

这里使用UIKit中比较基本的API,由UIView的beginAnimations和commitAnimations来完成。

beginAnimations有两个参数,第一个参数是给这个动画设置一个名字,第二个参数可以是传递给animation的delegate使用的。

第一个例子,将一张图片从屏幕的右下角移动到左上角

- (void)startTopLeftImageViewAnimation
{
    self.xcodeImageView2.frame = CGRectMake(self.view.bounds.size.width - 120,
                                            self.view.bounds.size.height - 90,
                                            120, 90);
    self.xcodeImageView2.alpha = 1.0f;

    [UIView beginAnimations:kAnimationName2 context:(__bridge void * _Nullable)(self.xcodeImageView2)];

    [UIView setAnimationDuration:3.0f];

    [UIView setAnimationDelay:delay];

    [UIView setAnimationDelegate:self];

    [UIView setAnimationDidStopSelector:@selector(imageViewDidStop:finished:context:)];

    self.xcodeImageView2.frame = CGRectMake(0, 0, 120, 90);

    self.xcodeImageView2.alpha = 0.0f;

    [UIView commitAnimations];}

- (void)imageViewDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context
{
    NSLog(@"Animation finished");

    NSLog(@"Animation ID = %@", animationID);

    UIImageView *contextImageView = (__bridge UIImageView *)context;
    [contextImageView removeFromSuperview];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    [self startBottomRightViewAnimationAfterDeley:2.0f];
}

这段代码中,在beginAnimations之后,设置了动画的时间为3秒钟完成所有动作,设置了delay值,在2秒后开始动画;动画完成了两个效果,第一个是从右下角移到了左上角,另一个是渐渐消失,注意在代码中设置了delegate,然后设置了didStopSelector,在相应的函数中,我们获取了UIImageView(通过beginAnimations第二个参数传入),然后将它从页面中删除。

我们再来看图片的旋转,旋转用到了affine的方法,即仿射变换,说白了也是一种平移,只不过是按照中心点进行旋转而已,代码如下

- (void)rotateAnimation
{
    self.xcodeImageView.center = self.view.center;

    [UIView beginAnimations:@"clockwise" context:NULL];

    [UIView setAnimationDuration:5.0f];
    [UIView setAnimationDelegate:self];

    [UIView setAnimationDidStopSelector:@selector(clockwiseRotationStopped:finished:context:)];

    self.xcodeImageView.transform = CGAffineTransformMakeRotation((90.0f * M_PI) / 180.0f);

    [UIView commitAnimations];
}

- (void)clockwiseRotationStopped:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context
{
    [UIView beginAnimations:@"counterclockwise" context:NULL];

    [UIView setAnimationDuration:5.0f];

    self.xcodeImageView.transform = CGAffineTransformIdentity;

    [UIView commitAnimations];
}

代码的功能是先将图片顺时针旋转90度,结束后再回复到原状。当然在clockwiseRotationStopped中通过指定旋转角度来将图片回复到原状(transform使用CGAffineTransformIdentity),不过有个问题需要注意,transform的角度是个绝对值而不是相对值,所以在顺时针旋转后,要回复到原状,不能指定旋转角度为90,而是0才能让他回复到原状,像下面这样设置

- (void)clockwiseRotationStopped:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context
{
    [UIView beginAnimations:@"counterclockwise" context:NULL];

    [UIView setAnimationDuration:5.0f];

    self.xcodeImageView.transform = CGAffineTransformMakeRotation((0.0f * M_PI) / 180.0f);

    [UIView commitAnimations];
}

在此基础上可以有很多的应用,比如把平移和转动结合起来,用在一个圆形上面,就可以实现轮子滚动的效果.

affine的变换主要有三种:平移,旋转和等比例的缩放

连同前两篇文章的示例代码可以在这里下载。

Categories: iOS 标签:

iOS上一些基本的图形处理之二

四月 7th, 2016 No comments 92 views

这片文章说说iOS中关于gradient的处理。gradient这个词我还真不知道在中文里该怎么翻译,有没有专门的词来表达。在这里先不管他中文的名称,这里主要说说在直角坐标系中的使用。

gradient的具体操作就是将坐标系中一个点作为起点,另外选一个点作为终点,同时起点和终点都指定一种颜色,gradient来完成两个点间的颜色过渡。当然这些颜色不是只局限在一根直线上面,而是将颜色拓展到跟直线垂直的所有区域。可以这样理解,有了起点和终点,可以画一根线,在这根线上,随意画一根垂直的线,这根垂直线上的所有颜色都是相同的,等会可以通过例子来看到效果。

创建gradient可以用CGGradientCreateWithColorComponents,这个函数需要四个参数,第一个是colorSpace,这个可以通过简单调用CGColorSpaceCreateDeviceRGB来获得;第二个是color Components,我们拿RGBA颜色系来说,要表示一种颜色,需要4个值,这个等会在下面的例子可以看到;第三个是location of color,这个参数用来表明颜色过渡渐变的速度,这是一个数组,里面元素的个数必须要与第四个参数的值一致;第四个参数表明一共用了多少种颜色。

直接用代码来说明问题

- (void)drawGradient1
{
    /* 获得colorSpace */
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();

    /* 指定startColor,endColor并且拆分成元素数组 */
    UIColor *startColor = [UIColor blueColor];
    CGFloat *startColorComponents = (CGFloat *)CGColorGetComponents([startColor CGColor]);
    UIColor *endColor = [UIColor greenColor];
    CGFloat *endColorComponents = (CGFloat *)CGColorGetComponents([endColor CGColor]);

    CGFloat colorComponents[8] =
    {
        /* Four components of the blue color (RGBA) */
        startColorComponents[0],
        startColorComponents[1],
        startColorComponents[2],
        startColorComponents[3],

        /* Four components of the green color (RGBA) */
        endColorComponents[0],
        endColorComponents[1],
        endColorComponents[2],
        endColorComponents[3]
    };

    /* locations of color in color array */
    CGFloat colorIndices[2] =
    {
        0.0f, /* Color 0 in the colorComponents array */
        1.0f, /* Color 1 in the colorComponents array */
    };

    CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, (const CGFloat *)&colorComponents, (const CGFloat *)&colorIndices, 2);

    CGColorSpaceRelease(colorSpace);

    CGContextRef currentContext = UIGraphicsGetCurrentContext();

    CGRect screenBounds = [[UIScreen mainScreen] bounds];

    CGPoint startPoint, endPoint;

    startPoint = CGPointMake(0.0f, screenBounds.size.height / 2.0f);

    endPoint = CGPointMake(screenBounds.size.width, startPoint.y);

    CGContextDrawLinearGradient(currentContext,
                                gradient,
                                startPoint,
                                endPoint,
                                0);

    CGGradientRelease(gradient);
}

记得释放colorSpace和gradient的资源,最终效果如下

效果是这样的是因为我指定的start和end的点坐标分别为(0.0f, screenBounds.size.height / 2.0f)和(screenBounds.size.width, startPoint.y),他们连成的线位于屏幕的最中间,呈水平位置,如前面所说的,在这个直线上所有与他垂直的线都使用同一种颜色,水平线上每个点的颜色在blue和green之间渐变,所以就出现了上面的效果。

函数CGContextDrawLinearGradient的最后一个参数需要注意一下,它有几个可选值,kCGGradientDrawsBeforeStartLocation和kCGGradientDrawsAfterEndLocation,表明是否在startPoint之前填充颜色和是否在endPoint之后填充颜色,我们可以通过下面的例子来学习这个参数的意义

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    UIColor *startColor = [UIColor orangeColor];
    CGFloat *startColorComponents = (CGFloat *)CGColorGetComponents([startColor CGColor]);

    UIColor *endColor = [UIColor blueColor];
    CGFloat *endColorComponents = (CGFloat *)CGColorGetComponents([endColor CGColor]);

    CGFloat colorComponents[8] =
    {
        startColorComponents[0],
        startColorComponents[1],
        startColorComponents[2],
        startColorComponents[3],

        endColorComponents[0],
        endColorComponents[1],
        endColorComponents[2],
        endColorComponents[3]
    };

    CGFloat colorIndices[2] =
    {
        0.0f,
        1.0f
    };

    CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace,
                                                                 (const CGFloat *)&colorComponents,
                                                                 (const CGFloat *)&colorIndices,
                                                                 2);

    CGColorSpaceRelease(colorSpace);

    CGPoint startPoint, endPoint;

    startPoint = CGPointMake(120.0f, 260.0f);
    endPoint = CGPointMake(120.0f, 220.0f);

    CGContextRef currentContext = UIGraphicsGetCurrentContext();
    CGContextSaveGState(currentContext);

    CGContextDrawLinearGradient(currentContext,
                                gradient,
                                startPoint,
                                endPoint,
                                    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    UIColor *startColor = [UIColor orangeColor];
    CGFloat *startColorComponents = (CGFloat *)CGColorGetComponents([startColor CGColor]);

    UIColor *endColor = [UIColor blueColor];
    CGFloat *endColorComponents = (CGFloat *)CGColorGetComponents([endColor CGColor]);

    CGFloat colorComponents[8] =
    {
        startColorComponents[0],
        startColorComponents[1],
        startColorComponents[2],
        startColorComponents[3],

        endColorComponents[0],
        endColorComponents[1],
        endColorComponents[2],
        endColorComponents[3]
    };

    CGFloat colorIndices[2] =
    {
        0.0f,
        1.0f
    };

    CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace,
                                                                 (const CGFloat *)&colorComponents,
                                                                 (const CGFloat *)&colorIndices,
                                                                 2);

    CGColorSpaceRelease(colorSpace);

    CGPoint startPoint, endPoint;

    startPoint = CGPointMake(120.0f, 260.0f);
    endPoint = CGPointMake(120.0f, 220.0f);

    CGContextRef currentContext = UIGraphicsGetCurrentContext();
    CGContextSaveGState(currentContext);

    CGContextDrawLinearGradient(currentContext,
                                gradient,
                                startPoint,
                                endPoint,
                                kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);

    CGGradientRelease(gradient);

    CGContextRestoreGState(currentContext););

    CGGradientRelease(gradient);

    CGContextRestoreGState(currentContext);

这段代码和上面的基本相同,有两处区别,一是start和end坐标不同,这回两个点连线之后时候与屏幕垂直的一条短线,高度并没有达到整个屏幕;第二个是使用了参数kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation。
我们看看效果

现在我将CGContextDrawLinearGradient第四个参数分别使用kCGGradientDrawsBeforeStartLocation,kCGGradientDrawsAfterEndLocation和0,你们就能看出区别来了。注意start左边在end的下方。

简单吧:)

Categories: iOS 标签:

iOS上一些基本的图形处理

四月 7th, 2016 No comments 116 views

这片文章是通过使用UIKit中的基础API来实现一些iOS上最基本的图形处理,主要是一些线和图形。

UIKit是一层high-level的图形框架,对用户来说,比较方便使用,它能让用户来操作iOS上的基本元素,比如view,window,button等元素,UIKit也是通过一些底层的API交互来实现这些功能的,iOS中主要的图像引擎是Quartz 2D。

iOS中总有一些比较奇怪的术语,比如path,比如context。UIKit中基本的图形操作最终都要落在context上面,这个context就相当于canvas,一个画布。

首先从画线开始。

一条直线,需要两个点,在iOS中,第一个点需要用户通过CGContextMoveToPoint来指定,第二个点通过CGContextAddLineToPoint来指定,这样两个点有了之后,在指定一些其它的属性,比如线宽和颜色,线条就可以画出来了。

在开始代码之前,需要创建一个UIView的子类,一些的操作,都在这个类的-(void)drawRect:(CGRect)rect中执行

/* 获取context */
CGContextRef currentContext = UIGraphicsGetCurrentContext();
/* 设置线条宽度 */
CGContextSetLineWidth(currentContext, 5.0f);
/* 指定第一个点 */
CGContextMoveToPoint(currentContext, 50.0f, 10.0f);
/* 指定第二个点 */
CGContextAddLineToPoint(currentContext, 100.0f, 200.0f);
/* 设置线条颜色 */
[UIColor brownColor] set];
/* 画线 */
CGContextStrokePath(currentContext);

如果继续指定第三,第四个点,iOS则会画出诸如折线这样的图案出来。在代码执行完CGContextAddLineToPoint之后,当前画笔的坐标会停留在最后一个点,即收笔的位置,所以第二条线会从第一条线的终点开始画。

在画折线的时候,有一个参数可以用,这个参数是用来制定折角的类型,有三个值:kCGLineJoinMiter, kCGLineJoinBevel和kCGLineJoinRound,这个参数在CGContextSetLineJoin中指定

- (void)textLineJoin
{
    [self drawRooftopAtTopPointof:CGPointMake(160.0f, 40.0f)
                    textToDisplay:@"Miter" lineJoin:kCGLineJoinMiter];
    [self drawRooftopAtTopPointof:CGPointMake(160.0f, 180.0f)
                    textToDisplay:@"Bevel" lineJoin:kCGLineJoinBevel];
    [self drawRooftopAtTopPointof:CGPointMake(160.0f, 320.0f)
                    textToDisplay:@"Round" lineJoin:kCGLineJoinRound];
}

- (void)drawRooftopAtTopPointof:(CGPoint)paramPoint textToDisplay:(NSString *)paramText lineJoin:(CGLineJoin)paramLineJoin
{
    [[UIColor brownColor] set];

    CGContextRef currentContext = UIGraphicsGetCurrentContext();

    /* Set the line join */
    CGContextSetLineJoin(currentContext, paramLineJoin);

    CGContextSetLineWidth(currentContext, 20.0f);

    CGContextMoveToPoint(currentContext, paramPoint.x - 140, paramPoint.y + 100);

    CGContextAddLineToPoint(currentContext, paramPoint.x, paramPoint.y);

    /* Extend the line to another point to make the rooftop */
    CGContextAddLineToPoint(currentContext, paramPoint.x + 140, paramPoint.y + 100);

    CGContextStrokePath(currentContext);

    /* Draw the text in the rooftop using a black color */

    [paramText drawAtPoint:CGPointMake(paramPoint.x - 40.0f, paramPoint.y + 60.0f)
            withAttributes:@{NSFontAttributeName: [UIFont boldSystemFontOfSize:30.0f],
                             NSForegroundColorAttributeName: [UIColor blackColor]}];
}

三段折线与折角的显示效果如下

上面的方法是直接在context上作画,iOS中还提供了另外一只作图方法,叫做path,原理跟context差不了太多,就是一切都定位等操作在path上定义好,然后将path加入到context中,最后完成具体的画图操作。

    /* Create the path */
    CGMutablePathRef path = CGPathCreateMutable();

    CGRect screenBounds = [[UIScreen mainScreen] bounds];

    CGPathMoveToPoint(path, NULL, screenBounds.origin.x, screenBounds.origin.y);

    CGPathAddLineToPoint(path, NULL, screenBounds.size.width, screenBounds.size.height);

    CGPathMoveToPoint(path, NULL, screenBounds.size.width, screenBounds.origin.y);

    CGPathAddLineToPoint(path, NULL, screenBounds.origin.x, screenBounds.size.height);

    /* Get the context that the path has to be drawn on */
    CGContextRef currentContext = UIGraphicsGetCurrentContext();

    CGContextAddPath(currentContext, path);

    [[UIColor blueColor] setStroke];

    CGContextDrawPath(currentContext, kCGPathStroke);

    CGPathRelease(path);

有亮点要注意,一是path在操作完后需要释放掉,使用CGPathRelease,另一个是使用CGContextDrawPath时,需要指定画笔颜色的使用方式,上面使用了Stroke。这个参数有三个值,分别时Stroke,Fill和StrokeFill。Stroke是颜色只用来画边框线,Fill是多边形可用来填充内部颜色,StrokeFill就是边框和填充都使用。具体使用什么颜色在UIColor的setStroke和setFill中设置,如果是[[UIColor redColor] set]这样,则表明Stroke和Fill同时设置。
最终会在屏幕上画一个大大的“X”,效果如下:

下来我们看看怎么画一个矩形,(多边形用折线的方式可以完成),矩形有更方便的方法。

- (void)drawingSeveralRectangles
{
    CGMutablePathRef path = CGPathCreateMutable();

    CGRect rectangle1 = CGRectMake(10.0f, 10.0f, 200.0f, 300.0f);
    CGRect rectangle2 = CGRectMake(40.0f, 100.0f, 90.0f, 300.0f);

    CGRect rectangles[2] = {rectangle1, rectangle2};

    CGPathAddRects(path, NULL, (const CGRect *)&rectangles, 2);

    CGContextRef currentContext = UIGraphicsGetCurrentContext();

    CGContextAddPath(currentContext, path);

    [[UIColor colorWithRed:0.20f green:0.60f blue:0.80f alpha:1.0f] setFill];
    [[UIColor blackColor] setStroke];

    CGContextSetLineWidth(currentContext, 5.0f);

    CGContextDrawPath(currentContext, kCGPathFillStroke);

    CGPathRelease(path);
}

上面代码画了两个矩形,如下

给矩形加阴影效果,通过CGContextSetShadowWithColor可以实现,这个函数有四个参数,第一个是需要使用的context;第二个相当于偏移量,也就是在原矩形的偏移x,y值的地方显示阴影效果;第三个参数表示模糊程度,值越小,越清晰,越大,阴影越模糊;第四个就是阴影所使用的颜色值

    CGContextRef currentContext = UIGraphicsGetCurrentContext();
    CGContextSetShadowWithColor(currentContext,
                                CGSizeMake(10.0f, 10.0f),
                                20.0f,
                                [[UIColor redColor] CGColor]);

    CGMutablePathRef path = CGPathCreateMutable();

    CGRect firstRect = CGRectMake(55.0f, 60.0f, 150.0f, 150.0f);

    CGPathAddRect(path, NULL, firstRect);

    CGContextAddPath(currentContext, path);

    [[UIColor colorWithRed:0.20f green:0.60f blue:0.80f alpha:1.0f] setFill];

    CGContextDrawPath(currentContext, kCGPathFill);

    CGPathRelease(path);

效果如下

这里也有一个问题需要注意,就是shadow一旦添加到context中,那么以后所有的在这个context上画出的东西都含有阴影,比如我在画完这个矩形后,再调用一次前面的函数textLineJoin,可以看到效果如下

这也许并不是我们需要的效果,所以需要在设置shadow前先保存context状态,完成我们的操作后,再把context还原回去,使用CGContextSaveGState和CGContextRestoreGState来完成

    CGContextRef currentContext = UIGraphicsGetCurrentContext();

    /* Save */
    CGContextSaveGState(currentContext);

    CGContextSetShadowWithColor(currentContext,
                                CGSizeMake(10.0f, 10.0f),
                                20.0f,
                                [[UIColor redColor] CGColor]);

    CGMutablePathRef path = CGPathCreateMutable();

    CGRect firstRect = CGRectMake(55.0f, 60.0f, 150.0f, 150.0f);

    CGPathAddRect(path, NULL, firstRect);

    CGContextAddPath(currentContext, path);

    [[UIColor colorWithRed:0.20f green:0.60f blue:0.80f alpha:1.0f] setFill];

    CGContextDrawPath(currentContext, kCGPathFill);

    CGPathRelease(path);

    /* Restore */
    CGContextRestoreGState(currentContext);

这样画完矩形后,如果我们再一次调用textLineJoin,出来的三条折线就不会再有阴影了

Categories: iOS 标签:
普人特福的博客cnzz&51la for wordpress,cnzz for wordpress,51la for wordpress

无觅相关文章插件,快速提升流量