自动旋转
旋转设备之后,view的bounds跟着改变了。当设备旋转时controller发生了什么?一是controller的view会调整它们的frame,但只在controller允许的时候,可以实现这个方法:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)orientation
{
return UIInterfaceOrientationIsPortrait(orientation); // only support portrait
return YES; // support all orientations
return (orientation != UIInterfaceOrientationPortraitUpsideDown); // anything but
}
返回controller是否允许它的view自动根据设备的旋转而旋转。这个自动旋转接口包括竖直、上下颠倒、左横向和右横向这4种情况。这儿有个宏UIInterfaceOrientationIsPortrait,返回它来检查是否是你想要的旋转方向。旋转的时候view的bounds会改变,它的子view的frame会变,子view的子view也会变。改变的衡量被称为struts和springs。
当view的bounds改变,drawRect不会再次被默认调用。
通过Xcode的size inspector设置struts和springs,红色的I就是struts,中间的红色箭头就是springs。右边红白色的显示屏似的就是用动画告诉你父view改变时它的变化,白色的是父view,红色的是你选中的view。中间的springs有两个方向,当父view改变大小时会在两个方向都改变大小。4个struts用来保持它到父view的边缘的距离,view会随着父view变大变小。
view里有个办法可以控制bound改变时造成延伸的情况,有个property叫做contentMode,它描述了你的view是做什么的,何时它的bound改变了。
主要有三种模式,第一种是上下左右等关于位置的,它的作用是把view的像素点移到规定的位置上。
@property (nonatomic) UIViewContentMode contentMode;
UIViewContentMode{Left,Right,Top,Right,BottomLeft,BottomRight,TopLeft,TopRight}
如果contentMode是right,那些点就移到右边去。
第二种模式是缩放、填充、内容填充、内容适应,这会对像素点进行拉伸,Tofill是默认的模式,它会自动缩放像素点以填满新的空间,这可能会扭曲图形。
UIViewContentModeScale{ToFill,AspectFill,AspectFit} // bit stretching/shrinking
第三种是重绘,也就是再次调用drawRect。
初始化一个UIView,如果想要为自定义view设置一些初始状态如contentMode,可以重载它的指定初始化initWithFrame,但当你的view离开了storyboard,它的init就不会被调用了。有个方法叫awakeFromNib,当veiw离开storyboard的时候它会被调用,所以任何关于设置的代码在两个方法里都会被调用。
-(void)setup { ... }
-(void)awakeFromNib { [self setup]; }
-(id)initWithFrame:(CGRect)aRect
{
self = [super initWithFrame:aRect];
[self setup];
return self;
}
协议(protocol)
协议没有对应的@implementation,协议的实现在另一个对象里。协议就是一个方法和property的集合,它的实现则是由其它对象完成。
@protocol Foo <Other, NSObject> // implementors must implement Other and NSObject too
- (void)doSomething; // implementors must implement this (methods are @required by default)
@optional
- (int)getSomething; // implementors do not have to implement this
- (void)doSomethingOptionalWithArgument:(NSString *)argument; // also optional
@required
- (NSArray *)getManySomethings:(int)howMany; // back to being “must implement”
@property (nonatomic, strong) NSString *fooProp; // note that you must specify strength
@end
唯一注意的是,可以有一个协议依赖于另一个协议,如这个例子里有人实现协议Foo,就必须实现Other和NSObject。
所有的方法都是必须实现的,除非放到了optional里。@optional表示监听的方法是可选的,直到遇到@required,之后就又变成必须的了。
协议可以有自己的头文件如Foo.h,然后再import到实现和使用的地方。还可以定义在其它类的头文件中。
声明了协议,类就可以在@interface里用<>来实现协议。
#import “Foo.h” // importing the header file that declares the Foo @protocol
@interface MyClass : NSObject <Foo> // MyClass is saying it implements the Foo @protocol
...
@en
新的类型,id<protocol>,它表示一个指向未知类的对象。我可以以这种类型向这些对象发送在我协议里面的消息,而不用做任何内省,编译器会帮我检查。
id<Foo> obj = [[MyClass alloc] init];
不仅可以声明变量,还可以把它们当参数传递。
- (void)giveMeFooObject:(id <Foo>)anObjectImplementingFoo;
@property (nonatomic, weak) id <Foo> myFooProperty; // properties too!
这里的参数id<foo>,也就是一个能够回应foo方法的未知类的对象。
协议最主要的用途是委托(delegate)或数据源(datasource)。委托几乎都是weak的,因为被设为委托的对象通常都是委托对象的所有者或创建者。如controller常常把自己设为view的委托或数据源,你不想要它们互相用strong指针互指。所以view只会weak指向会controller。
scrollView例子,scrollView.h文件:
@protocol UIScrollViewDelegate
@optional
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)sender;
- (void)scrollViewDidEndDragging:(UIScrollView *)sender willDecelerate:(BOOL)decelerate;
@end
@interface UIScrollView : UIView
@property (nonatomic, weak) id <UIScrollViewDelegate> delegate;
@end
@interface MyViewController : UIViewController <UIScrollViewDelegate>
@property (nonatomic, weak) IBOutlet UIScrollView *scrollView;
@end
@implementation MyViewController
- (void)setScrollView:(UIScrollView *)scrollView {
_scrollView = scrollView;
self.scrollView.delegate = self; // compiler won’t complain
}
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)sender { return ... };
@end
手势识别
手势识别是怎么工作的?手势识别是个对象,它监控view的点击事件。当它发现某种点击的时候比如挤压、滑动、拖动、点击之类的,它会发消息给手势识别处理者,就可以做相应的反应了。最基础的类是UIGestureRecognizer,它是抽象的,需要实现。
使用手势识别有两个步骤,先创建一个再把它附在view上,然后当手势被识别的时候进行处理。第一步通常是controller来做的,controller来决定它的view需要实现比如拖动和点击,本质上就是打开这两个开关。但是手势的处理常常是view做的,但还是让controller来添加手势到view上。另外一些手势的处理可能要靠controller来实现,就是涉及到修改model的手势。如果有个手势会改变model,controller会处理,因为view看不到model。所以通常都是controller在添加手势,view对自己添加手势也是可能的,某些view如果手势不能被识别就没有意义,那么就可以自己添加手势。controller可以移除手势。
- (void)setPannableView:(UIView *)pannableView
{
_pannableView = pannableView;
UIPanGestureRecognizer *pangr = [[UIPanGestureRecognizer alloc] initWithTarget:pannableView action:@selector(pan:)];
[pannableView addGestureRecognizer:pangr];
}
这段代码用来添加手势识别到view上。target是手势识别之后的处理者,这里是view自身来处理。然后pan:是发送给view的消息,也就是action发给target。但pan:不是发送者,手势识别调用这个准备发送的消息,所以它不是发送者,手势识别才是。
怎么实现手势识别?每个手势都提供了自己的方法,比如拖动提供了这三个方法:
- (CGPoint)translationInView:(UIView *)aView;
- (CGPoint)velocityInView:(UIView *)aView;
- (void)setTranslation:(CGPoint)translation inView:(UIView *)aView;
第一个会给你一个坐标点告诉你,从上个手势点到这个点的距离。第二个告诉你手指移动的速度,每秒几个像素点。最后一个方法是第一个方法的setter,如果返回0,你就会得到增量的更新。重设translation就是为了得到增量的结果。
除了具体手势识别,还有一个很重要的抽象手势识别提供的property叫做sate。所以手势识别是个状态机。
@property (readonly) UIGestureRecognizerState state;
所有的手势识别初始状态都是possible。如果手势很短比如点击,那么状态就变成Recognized,所以你的处理函数被调用,状态变成Recognized。如果手势一直持续下去比如拖动、缩放,那么开始时候的状态是Began,变化中是Changed,手指抬起来是Ended。还有状态Failed和Cancelled,这两个只有当你实现一个操作的时候才用到。
那pan:到底是什么样的呢?
- (void)pan:(UIPanGestureRecognizer *)recognizer
{
if ((recognizer.state == UIGestureRecognizerStateChanged) || (recognizer.state == UIGestureRecognizerStateEnded)) {
CGPoint translation = [recognizer translationInView:self];
// move something in myself (I’m a UIView) by translation.x and translation.y
// for example, if I were a graph and my origin was set by an @property called origin
self.origin = CGPointMake(self.origin.x+translation.x, self.origin.y+translation.y);
[recognizer setTranslation:CGPointZero inView:self];
}
}
参数是UIPanGestureRecognizer,我要做的是不管状态的拖动,只需要知道Changed和Ended,只关注移动的时候。需要translation也就是拖动的距离,然后要重设translation为0,因为下次拖动的时候我想要的是移动的增量。
UIPinchGestureRecognizer:缩放手势(pinch),缩放开始的时候是1。缩放也可以被重设,然后就得到增量的缩放;也有缩放的速度。
UIRotationGestureRecognizer:旋转手势,两个手指按下,然后旋转,是个弧度,不是角度。
UISwipeGestureRecognizer:滑动有好几种,一指两指都可以。只要创建一个滑动识别,再设置它的一个property表明需要识别多少手指。
UITapGestureRecognizer:点击手势,和滑动识别一样,可以识别多跟手指。
Demo
Model:int happiness,表示幸福度;
View:自定义的view叫做FaceView,会画一些。
Controller:HappinessViewController
关注(watch for):
要在drawRect里加入子程序、如何执行委托;
有两个手势,一个会被view处理,因为它只修改显示,另一个会被controller处理,因为它修改了model,它会改变幸福度。
新建一个项目,名叫Happiness,使用storyboard。
HappinessViewController.h 文件代码:
#import <UIKit/UIKit.h>
@interface HappinessViewController : UIViewController
@property (nonatomic) int happiness; // 0 is sad; 100 is very happy
@end
HappinessViewController.m文件代码:
#import "HappinessViewController.h"
#import "FaceView.h"
@interface HappinessViewController()
@property (nonatomic, weak) IBOutlet FaceView *faceView;
@end
@implementation HappinessViewController
@synthesize happiness = _happiness;
@synthesize faceView = _faceView;
- (void)setHappiness:(int)happiness
{
_happiness = happiness;
[self.faceView setNeedsDisplay]; // any time our Model changes, redraw our View
}
- (void)setFaceView:(FaceView *)faceView
{
_faceView = faceView;
// enable pinch gestures in the FaceView using its pinch: handler
[self.faceView addGestureRecognizer:[[UIPinchGestureRecognizer alloc] initWithTarget:self.faceView action:@selector(pinch:)]];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
{
return YES; // support all orientations
}
@end
FaceView.h文件代码:
#import <UIKit/UIKit.h>
@interface FaceView : UIView
@property (nonatomic) CGFloat scale;
- (void)pinch:(UIPinchGestureRecognizer *)gesture;
@end
FaceView.m文件代码:
#import "FaceView.h"
@implementation FaceView
@synthesize scale = _scale;
#define DEFAULT_SCALE 0.90
- (CGFloat)scale
{
if (!_scale) {
return DEFAULT_SCALE; // don't allow zero scale
} else {
return _scale;
}
}
- (void)setScale:(CGFloat)scale
{
if (scale != _scale) {
_scale = scale;
[self setNeedsDisplay]; // any time our scale changes, call for redraw
}
}
- (void)pinch:(UIPinchGestureRecognizer *)gesture
{
if ((gesture.state == UIGestureRecognizerStateChanged) ||
(gesture.state == UIGestureRecognizerStateEnded)) {
self.scale *= gesture.scale; // adjust our scale
gesture.scale = 1; // reset gestures scale to 1 (so future changes are incremental, not cumulative)
}
}
- (void)setup
{
self.contentMode = UIViewContentModeRedraw; // if our bounds changes, redraw ourselves
}
- (void)awakeFromNib
{
[self setup]; // get initialized when we come out of a storyboard
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setup]; // get initialized if someone uses alloc/initWithFrame: to create us
}
return self;
}
- (void)drawCircleAtPoint:(CGPoint)p withRadius:(CGFloat)radius inContext:(CGContextRef)context
{
UIGraphicsPushContext(context);
CGContextBeginPath(context);
CGContextAddArc(context, p.x, p.y, radius, 0, 2*M_PI, YES); // 360 degree (0 to 2pi) arc
CGContextStrokePath(context);
UIGraphicsPopContext();
}
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGPoint midPoint; // center of our bounds in our coordinate system
midPoint.x = self.bounds.origin.x + self.bounds.size.width/2;
midPoint.y = self.bounds.origin.y + self.bounds.size.height/2;
CGFloat size = self.bounds.size.width / 2;
if (self.bounds.size.height < self.bounds.size.width) size = self.bounds.size.height / 2;
size *= self.scale; // scale is percentage of full view size
CGContextSetLineWidth(context, 5.0);
[[UIColor blueColor] setStroke];
[self drawCircleAtPoint:midPoint withRadius:size inContext:context]; // head
#define EYE_H 0.35
#define EYE_V 0.35
#define EYE_RADIUS 0.10
CGPoint eyePoint;
eyePoint.x = midPoint.x - size * EYE_H;
eyePoint.y = midPoint.y - size * EYE_V;
[self drawCircleAtPoint:eyePoint withRadius:size * EYE_RADIUS inContext:context]; // left eye
eyePoint.x += size * EYE_H * 2;
[self drawCircleAtPoint:eyePoint withRadius:size * EYE_RADIUS inContext:context]; // right eye
#define MOUTH_H 0.45
#define MOUTH_V 0.40
#define MOUTH_SMILE 0.25
CGPoint mouthStart;
mouthStart.x = midPoint.x - MOUTH_H * size;
mouthStart.y = midPoint.y + MOUTH_V * size;
CGPoint mouthEnd = mouthStart;
mouthEnd.x += MOUTH_H * size * 2;
CGPoint mouthCP1 = mouthStart;
mouthCP1.x += MOUTH_H * size * 2/3;
CGPoint mouthCP2 = mouthEnd;
mouthCP2.x -= MOUTH_H * size * 2/3;
float smile = 1.0; // this should be delegated! it's our View's data!
CGFloat smileOffset = MOUTH_SMILE * size * smile;
mouthCP1.y += smileOffset;
mouthCP2.y += smileOffset;
CGContextBeginPath(context);
CGContextMoveToPoint(context, mouthStart.x, mouthStart.y);
CGContextAddCurveToPoint(context, mouthCP1.x, mouthCP2.y, mouthCP2.x, mouthCP2.y, mouthEnd.x, mouthEnd.y); // bezier curve
CGContextStrokePath(context);
}
@end