首页 > 代码库 > [Stanford 2011] UIView、协议、手势识别

[Stanford 2011] UIView、协议、手势识别

1.Autorotation

自动旋转时controller发生了什么?一是controller的view在controller允许的时候调整frame,方法shouldAutorotateToInterfaceOrientation返回controller是否允许view自动根据设备旋转而旋转.自动旋转接口包括竖直、上下颠倒、左横向和右横向这4种情况。只要在controller里实现这个方法就ok。   不管支持的是哪个方向,旋转的时候view的bound会改变,子view的frame会变,子view的子view也会变。这些改变的规则是什么?改变的衡量标准被称为struts and springs。当view的bound改变,drawRect不会再次被默认调用.

 

2.structs  and springs

在Xcode|Inspecttor 下找到struts and springs,红色的I一样的标志就是structs,中间的箭头是springs。右边小窗口,看起来像个电视屏幕,它会用动画演示父view改变时候子view的变化。图中外边白色的是父view,红色是选中的子view。 

structs & springs 到底是干嘛用的?中间spring有两个方向,当父view改变大小的时候会在两个方向都改变大小。4个structs用来保持它到父view边缘的距离,即我们的view会随着父view变大变小。如图1:

例如,如上图中间的struct & spring,只开启了水平的springs和上、左、右的structs,这种情况下目标view会黏住父view的顶部,通过变宽来保持左右的边缘距离,下部就不跟着变了。(即,有横向的spring表示此view可以向左右方向扩展,有纵向的spring表示此view可以向上下方向扩展。此处只有横向spring,表示只能向左右扩展,不能向上下扩展。)所以,手机从横向变为纵向的时候目标view的父view变得很高,目标view仍然会待在顶部,画面会空很多。

上图最右边的struct & spring中,目标view不会调整大小,保持原大小,但是会一直在左上角。

总结:通过设置structs & springs 你可以规定父view改变尺寸的时候子view的规则。但是有些事是structs&springs做不到的,比如想象一下你的计算器从竖直变到横向,你真的可以简单的重新布局好吗?可能不行。这就是你可能会用到代码的情况了,你在controller里可以调用一个方法didRotateToUserInterfaceOrientation,你可以自己控制view布局。

3. Redraw on bounds change?No

当bound改变,比如structs  and springs让view变得更宽,自定义的view会发生什么?它的drawrect会再被调用吗?答:不会,由于性能原因,view的点会延伸或挤压。所以如果放到高分辨率下会有颗粒感。幸运的是在view里有个办法可以控制,有个property叫做contentMode,它描述了view做了什么:

主要有3种模式第一种是上下左右等关于位置的。它们的作用是把你view的像素点移到规定位置上。 这不是structs&spring,structs&spring已经发生过了,这里说的是我的view变得更宽了,那么在变宽之前我view的点会怎么做,如果contentMode是right,那些点会移到右边去。

第二种模式是缩放、填充、内容填充、内容适应。这会对像素点进行拉伸,我们在演示里会看到,Tofill是默认的模式,它会自动缩放像素点以填满新的空间,这很可能会扭曲图形,常常不是你要的,

第三种是我们常常想要的,重绘。也就是再次调用drawRect这是最敏感的一项,但性能也不是最佳的,不过性能也还不错,

默认的scall是ToFill,还有一个propertycontentStretch,可以指定某一个子view的像素点做拉伸, 比如有个frame不能随意拉伸,但是中间是空的就像UIButton一样,button的中间可以被拉伸因为中间没有像素点,但是边缘就不可以了,所以contentStretch可以在view里指定某一个矩形可以被拉伸。

4.初始化UIView

如果要为自定义view设置一些初始状态,比如设置contentMode,可以重载它的指定初始化方法initWithFram但当view离开了storyboard,init方法这时候就不会被调用了 。那么如何应对这种情况呢?答案是有个方法叫做awakeFromeNib,当view离开storyboard的时候它会被调用,所以任何关于设置的代码在这两个方法里都会被调用。

上面就是代码大概的样子,有了setup,在init和awakeFromNib里都会调用它。(这和viewDidLoad有什么区别?)不建议把设置放到init方法里,可以放在其他地方比如setter,不要觉得必须重载initWithFrame和awakeFromNib。

5.Protocols(1)

协议(Protocol)的语法和interface非常像。注意几件事:一是协议没有对应的@implementation,协议的实现在另一个对象里,所以协议就是一个方法和property的集合,它的实现由其他对象完成唯一语法上要注意的是,你可以有一个协议依赖于另一个协议,如下图中的例子中,如果要实现协议Foo,那么就必须实现协议Other和协议NSObject,协议NSObject基本包括了所有NSObject的方法。所以如果协议后面有个尖括号里面是NSObject,你的意思就是说Foo必须是个NSObject实现。。(我想你在Xcode里输入@property会自动补上<NSObject>,因为几乎所有的iOS对象都是NSObject。)所以之后你就开始监听这个方法来,所有的方法都是必须的,除非你放到了@optional里,@optional表示监听的方法是可选的,直到遇到@required之后就又变成必须的了。所以在这个协议里,doSomething,getManySomethings和Foo都是必须的(required),但是中间这两个getSomething和doSomethingOptionalWithArgument是可选的,你可以用这个协议,但是不实现这两个方法。

除了NSObject还有什么可以放在协议里面?答案是任何对象都可以。现实是我们一直都是从NSObject继承对象,我们一直这么做,但我们不是必须这么做, 但是协议和NSObject没有直接联系。iOS上有什么对象不是继承自NSObject的吗?答案是还没有发现。

 头文件里协议的声明:协议可以有自己的头文件,比如Foo.h,仅包含自己的协议,然后再import到实现和使用的地方但是大多数情况只有一个对象会用到这里协议,所以就把协议放到它的头文件里,比如ScrollViewDelegate协议就是定义在UIScrollView.h里,所以你可以把协议放在其他头文件里或者自己的头文件里。

#import "Foo.h" //importing the header file that declares the Foo @protocol@interface MyClass:NSObject<Foo> //MyClass is saying it implements the Foo @protocol   ...@end

 

所以,你已经声明了协议Foo,之后类MyClass就在@interface里用<>来实现协议。上面代码中,类MyClass继承自NSObject实现了协议Foo,所以我在声明我的协议Foo的实现,现在我必须实现Foo中的非可选方法,否则编译器会报错,编译器会说未完成的实现,必须实现所有Foo协议里要求的方法。

有了协议的声明,有了会实现协议的对象,看下图

(1)声明一个id的变量比如id<protocal>,图中即是id<Foo>,它表示一个指向未知类的对象。我可以以向这种类型的对象发在协议里面的消息,而不用做任何内省,编译器会帮我检查。比如id<Foo> obj = [[Myclass alloc] init]是可以的,因为在MyClass里有Foo的实现,所以编译会通过。如果是id<Foo>obj =[NSArray array]就不可以,因为NSArray没有Foo的实现,编译会报错。

(2)不仅可以像上面一样声明变量,还可以把id<Foo>当参数传递,这里的id<Foo>就是一个能够回应Foo方法的未知类的对象

(3)还可以有这种类型的property,比如上面图中的myFooProperty

6.Protocols(2)

协议的用途:看下图

(1)就像静态类型一样,所有这些东西都是为了让编译器能发现bug,在运行时都是一样的,编译器会去自己找实现方法,找到就发消息,找不到就发exception。

(2)可以把协议当作是你所允许向这些id类型发送的消息的文档,它就是个文档,比如一个未知类型的对象,但是通过文档我知道它实现了某个

方法。

(3)在iOS里协议最主要的用途是委托(delegate)和数据源(datasource)。

        回想一下第一课,在MVC里讲过view从不用知道它的controller的类,因为view是通用的。比如UIButton内部不可能知道什么是calculatorViewController,所以view需要以通用的方式对话controller,一个方法是target action,另一个方法是委托(delegation)委托是这门课的一个重点。数据源(datasource)也是委托,只是具体到了数据的委托,记住view不拥有数据,view不会有实例变量property,view只有指向数据的指针。他们可以有property表示如何去绘图,可能是颜色之类的,但是它们不拥有显示的数据,那是controller的活,因为只有controller可以对话model,model提供了view显示的数据。上图中有一个典型的委托声明,委托几乎都是weak的,因为被设为委托的对象通常都是委托对象的所有者或者创建者,举个例子controller常常把自己设为view的委托或数据源,你不想要它们互相用strong指针互指,因为这样的话堆上的指针strong指向其他东西,这样就很难让两者同时释放,所以view只会weak指向controller,如果controller不在了,view也没什么用了

7.Protocols(3)

      以下是scrollView.h,可以看到它声明了scrollView的委托协议,所有的方法都是可选的,没全写出来。
      下面是真正的@interface,去掉了很多东西除了这个weak的property,它的类型是id<UISCrollViewDelegate>,名字是delegate,问题是既然都是可选的,为什么我还要用协议?再说一次协议是个文档,当你说你是scrollView的委托,你就查看这个协议可以实现的方法有哪些,所以它是个文档。

       然后,如果有个类MyViewController想要使用scrollView,注意MyViewController类声明了UIScrollViewDelegate的实现,MyViewController类有一个outlet指向scrollView。MyViewController类想要成为scrollView的委托,所以在scrollView会收到委托消息。所以比如这个scrollView的setter,self.scrollView.delegate = self就把self设为了scrollView的委托。想着像下面的这个viewForZoomingInScrollView这样的方法,都将会发送给MyViewController

8. 手势识别

手势识别是怎么工作的?手势识别是个对象,它监控view的点击事件。当它发现某种点击的时候比如挤压、滑动、拖动、点击之类的,它会发消息给手势识别处理者,就可以做相应的反应了。

最基础的类是UIGestureRecognizer,它是抽象的,所以你从没真正用它创建实例,它有很多实在的子类才是你真正用来附在view上的。

使用手势识别有两个步骤,先是创建一个手势识别再添加到view上,然后当手势被识别的时候进行处理。第一步通常都是controller来做的,controller来决定它的view需要实现比如拖动和点击,本质上就是打开这两个开关,但是手势的处理常常是view来做的,因为比如自定义view它自己知道如何处理手势和内部状态,比controller做得好多了,但是我们还是让controller来添加手势识别到view上,有点像打开开关。另外一些手势的处理可能要靠controller实现,更明确点就是涉及到修改model的手势。如果你有个手势会改变model,controller会处理,因为view看不到model,所以通常都是controller添加手势,view对自己添加手势也是可能的,某些view如果手势不能被识别就没有意义,那么就可以自己添加手势。比如scrollView,它会自己添加拖动和缩放,因为scrollView就是用来干这个的,不能拖动缩放的话scrollView就没意义了。但是controller可以移除这些手势,如果scrollView不需要拖动,controller可以把手势移除,但是通常情况下不会去移除。手势控制也可以被任何人实现,但通常都是controller或者view。

//Adding a gusture recognizer to a UIView from a Controller-(void)setPannableView:(UIView *) pannableView{    _pannableView = pannableView;    UIPanGestureRecognizer *pangr = [[UIPanGestureRecognizer alloc] initWithTarget:pannableView action:@selector(pan:)];}

 这段代码是用来添加手势识别到view上,view有个可以识别的手势列表。这个例子是个outlet的setter。例子中创建一个拖动的手势识别,它是一个具体的UIGestureRecognizer子类,叫做UIPanGestureRecognizer。手势识别的初始化initWithTarget:action:这也算是某种意义上的target action,不一样但有点像。所以target是手势识别之后的处理者,这里是view自身来处理。然后pan:是发送给view的消息,也就是action发给target,但pan:不是发送者,手势识别调用这个准备发送的消息,手势识别是发送者。所以要回过去问手势识别详细信息。之后调用addGestureRecognizer就完成了添加,当发生拖动的时候会发送pan:到target也就是view自己。

手势识别一直都会把自己当参数传过去吗?事实上如果不写pan:就没有参数传过去,比如点击就不需要参数,通常都会加上pan:,这样手势识别可以识别自己。

怎样实现手势识别,比如刚刚的pan:?每个手势都提供了自己的方法,比如拖动提供了下面这3个方法:(1)第一个是translationInView,它会给你一个坐标点告诉你,从上个手势点到这个点的距离,第一下点击是起始点,移动之后它会告诉你它到起始点的距离,起始点也可以重设,那么它就会告诉你一个增量的距离;(2)velocityInView告诉你手指移动的速率,指的是即时速率,每秒几个像素点;(3)setTranslation,这就是第一个方法的setter,如果设回0,你就会得到增量的更新。重设translation很常见,就是为了得到增量的结果。

除了具体手势识别,还有一个很重要的抽象手势识别提供的property叫做state,所以手势识别是个状态机,所有的手势识别初始状态都是possible。如果手势很短比如点击,那么状态就变成Recognized,所以处理函数被调用,状态变成Recognized;如果手势一直持续下去,比如拖动、缩放,那么开始的时候状态是Began,变化中状态是Changed,当手指抬起来状态是Ended,还有状态Failed和Cancelled这两个只有当你实现一个操作的时候才用到,比如说Begin的时候你自己设了一些状态,然后你跟踪状态,然后在Ended的时候撤销这些状态。通常我们不会这么去实现手势识别,我们希望实现的时候不用管状态。所以如果我们手指收缩一点,我们修改view一点,然后回到之前的状态,如果继续收缩那就继续修改,不要保持一个状态,这样实现就简单多了。唯一用到Failed和Cancelled是你保持了一个状态,直到你去清除这个状态,Failed和Cancelled可能在一个手势中被另一个手势干扰时发生。比如你在拖动,但变成了3个手指点击之类的或者你在做手势的时候来电话了,那么就取消手势。

//For example, UIPanGestureRecognizer provides 3 methods:-(CGPoint)translationInView:(UIView*)aView;-(CGPoint)velocityInView:(UIView*)aView;-(void)setTanslation:(CGPoint)translation inView:(UIView*)aView;//Also,the base class,UIGestureRecognizer provides this @property@property(readonly)UIGestureRecognizerState state;

pan:到底是什么样的?pan只关注移动的时候。下面代码中translation是拖动的距离,下面这段代码是在view里,所以参数self就是view。有了translation之后我要移动某些东西,然后我要重设translation为0,因为下次拖动的时候我想要的是移动的增量,我不需要从起点开始的总距离,只要每次移动的距离。因为每次移动后都会重设,每次新的动作进来都好像没有移动过一样。

-(void)pan:(UIPanGestureRecognizer*)recognizer{   if((recognizer.state==UIGestureRecognizerStateChanged)||//We‘re going to update our view every time the touch moves (and when the touch ends)      (recognizer.state==UIGestureRecognizerStateEnded) )   {     
CGPoint translation
= [recognizer translationInView:self];//"translation" is the cumulative distance this gesture has moved.//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];//Here we are resetting the cumulative distance to zero. }}//Now each time this is called,we‘ll get the "incremental" movement of the gesture(Which is what we want).
//If we wanted the "cumulative" movement of the gesture,we would not include this line of code.

手势缩放(pinch):(1)缩放开始的时候是1,变大就变成1.7之类的,变小就变成0.8什么的,同样缩放也可以被重设,然后就得到增量的缩放。(2)它也有速率(velocity),缩放的速率.
旋转手势(rotation)就是按下两只手指,然后旋转它是弧度的,不是角度的它也是可以重设的。

滑动手势(swipe),滑动有好几种,一指两指都可以,只要创建一个滑动识别,再设置它的一个property表明需要识别多少手指。

手势缩放、旋转手势、滑动手势的property如下图:

 

[Stanford 2011] UIView、协议、手势识别