旋转设备之后,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; }
协议没有对应的@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:点击手势,和滑动识别一样,可以识别多跟手指。
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