iOS View 编程指导(三)-View

2018-09-15  本文已影响88人  陵无山

View

iOSAPP中和用户打交道最多就是view,view有多作用,下面随便列举几个:

创建和设置View

可以使用代码手动创建也可用XIB创建,创建完view后,将其组合到view的层级树中.

使用XIB创建View

想要学习更多的关于xib使用的知识请参考Apple文档Interface Builder User Guide
以及controller如何加载nib文件,创建自定义viewController请看View Controller Programming Guide for iOS
以及学习如何手动从nib文件中加载UI界面的知识请看Resource Programming Guide中的Nib Files

使用代码创建View

通常使用allocation/initialization模式来创建view的,view的默认初始化方法是initWithFrame:

CGRect  viewRect = CGRectMake(0, 0, 100, 100);
UIView* myView = [[UIView alloc] initWithFrame:viewRect];

注意:虽然所有的view都支持initWithFrame:方法,但有的view有其自己的初始化方法,比如UIButton,通常都是使用buttonWithType:来创建的,UIImageView的initWithImage:等等.

view创建后需要将其添加到window中或其他view中,否则不能显示.

给view的属性赋值

通过UIView的属性来控制view的显示和行为.
下表展示view的属性和作用

Properties Usage
alpha, hidden, opaque 这些控制view透明度. 注意opaque属性,opaque属性设置为YES可以提高性能
bounds,frame,center,transform 这些属性控制view的size和position. transform用来做动画或者做view的复杂整体移动
autoresizingMask, autoresizesSubviews 这些属性用来控制view和subviews的automatic resizing行为. 当superview的bounds发生改变时,autoresizingMask控制view的变化;autoresizesSubviews控制view的subviews是否需要resize.
contentMode,contentStretch,contentScaleFactor 这些影响view的内容绘制. contentScaleFactor属性用于需要自定义重绘view的高分辨率的屏幕.
gestureRecognizer, userInteractionEnabled, MultipleTouchEnabled, exclusiveTouch 这些属性控制view对于touch events的处理.
backgroundColor, subviews, drawRect:,layer 这些属性控制view的内容显示和绘制

想要知道更多请看UIView的接口UIView Class Reference

给view添加一个记号

UIView有个tag属性(integer,整型,默认为0),用来标记view的,方面后续使用tag值从view的层级树中找到该view.使用tag来获取view比遍历寻找要快.

通过UIView的实例方法viewWithTag:,该方法使用深度优先算法(参考数据结构-树)从层级树中搜索目标,而且只会从view的本身和subview开始搜索,view的superview和其他层级树不会搜索,也就是说如果你对root view调该方法,那么它会搜索整个页面的层级树,如果是树中的某个view调用该方法,那么只会搜索某个子树.

创建和管理view的层级树

创建和管理view的层级树,就是创建和管理APP的UI界面,层级树决定了那个view响应事件. 下图展示Clock应用的图层,由许多view构成UI界面:

Clock应用的图层
这一节讲解如何创建view的层级树,以及如何从层级树找到特定的view,转换不同的view的坐标系.

添加移除subview

如果使用xib创建view层级树,那么可以直观地发现view之间的层级(父-子关系),而且界面不需要运行就可以看到.
使用代码创建的话,需要使用下么方法来创建和管理:

当一个subview添加到superview后,会根据frame来确定位置和大小. subview超出superview的区域默认是可见的,如果你想superview裁剪subview,可以将superview的clipsToBounds设置为YES.

往view的层级树中插入subview的代码可以写controller的loadView(适合手动用代码)或者viewDidLoad中(适合xib)

下列代码展示了Apple官方demoUIKit Catalog (iOS): Creating and Customizing UIKit Controls中类TransitionsViewController方法viewDidload中的代码. TransitionsViewController用来管理两个view间切换的动画. viewdidload中的代码顺序地创建一个容器view,image views用来做切换动画. 容器view的作用是方面做两个image间的切换动画.

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = NSLocalizedString(@"TransitionsTitle", @"");
    // create the container view which we will use for transition animation (centered horizontally)
    CGRect frame = CGRectMake(round((self.view.bounds.size.width - kImageWidth) / 2.0),
                                                        kTopPlacement, kImageWidth, kImageHeight);
    self.containerView = [[UIView alloc] initWithFrame:frame];
    [self.view addSubview:self.containerView];
 
    // create the initial image view
    frame = CGRectMake(0.0, 0.0, kImageWidth, kImageHeight);
    self.mainView = [[[UIImageView alloc] initWithFrame:frame] autorelease];
    self.mainView.image = [UIImage imageNamed:@"scene1.jpg"];
    [self.containerView addSubview:self.mainView];
 
    // create the alternate image view (to transition between)
    CGRect imageFrame = CGRectMake(0.0, 0.0, kImageWidth, kImageHeight);
    self.flipToView = [[[UIImageView alloc] initWithFrame:imageFrame] autorelease];
    self.flipToView.image = [UIImage imageNamed:@"scene2.jpg"];
}

如果你将一个subview又添加到另一个view,UIKit会通知它superview和它的subview. 如果是自定义的view,你可以在重写下面方法来监听该通知:

隐藏view

注意:如果你隐藏的view当前是first responder,那么事件会继续传递给它,所以你因该在隐藏它同时将其resign first responder. 更多关于响应链的知识请看Event Handling Guide for iOS

如何在层级树中找到特定的view

view的位移/缩放/旋转

// M_PI/4.0 is one quarter of a half circle, or 45 degrees.
CGAffineTransform xform = CGAffineTransformMakeRotation(M_PI/4.0);
self.view.transform = xform;

下图展示了transform如何旋转一张图片:


旋转一张图片

切换不同的坐标系

很多时候,特别是在处理touch events的时候,经常要计算一个view的坐标在其他view中的坐标; 比如要计算touches在某个view中的坐标. UIView提供了下面的方法用来计算其他view在该view本地坐标:

上面方法中convert...:fromView:将其他view中的坐标转换到当前view的坐标,相反地,convert...:toView:试讲当前view的坐标转换到其他view中的坐标.在上面4个方法中如果view的值设为nil,那么自动地认为和window进行转换.

UIWindow也停供了和UIView类似的工具方法:

这里有个涉及将一个旋转过的view中的坐标转换到其他view的问题,UIKit会算出该旋转view刚好包含旋转view的矩形框,然后再讲矩形框转换到其他view的坐标,看下图解释:


转换旋转后的view的坐标

如何在运行时调整view的大小和位置

只要view的size改变了,那么view的subview的position和size也要相应的改变. UIView提供两种方式进行View的布局:①自动布局(当superview变动时,设置view间的布局规则,实际的位置和大小有系统根据前面设置的规则自己计算) ②手动布局(superview的size改变时,开发者自己计算subview的size和position)

为布局变动做准备

布局的变动会因为下面的这些原因:

使用Autoresizing(和autolayout不一样)进行布局

在自动布局的时候,给view设置autoresizingMask很重要,下表列举了autoresizingMask(宽高上下左右)可能的取值,和每一个值对应的布局操作,并且这些值可以叠加(做或运算),然后赋值给view的autoresizingMask. 如果你是XIB来矩形局部可以使用Autosizing inspector进行相应的设置.

Autoresizing Mask 描述
UIViewAutoresizingNone 不进行autoresize(默认值)
UIViewAutoresizingFlexibleHeight 高度随superview而变,如果不包含该值,高度不会改变
UIViewAutoresizingFlexibleWidth 宽度随superview而变,如果不包含该值,宽度不变
UIViewAutoresizingFlexibleLeftMargin view的左边和superview左边的距离可以可变,如果不包含该值,那么间距不变
UIViewAutoresizingFlexibleRightMargin view的右边和superview右边的距离可以可变,如果不包含该值,那么间距不变
UIViewAutoresizingFlexibleBottomMargin view的底边和superview底边的距离可以可变,如果不包含该值,那么间距不变
UIViewAutoresizingFlexibleTopMargin view的顶边和superview顶边的距离可以可变,如果不包含该值,那么间距不变

下图展示上面取值代表物理意义上的图示,某一个值的缺失代表这一物理意义是固定值,否则是随superview的大小可变. 如果你对view进行配置是,在同一轴上有多个可变配置,比如你对一个view同时设置UIViewAutoresizingFlexibleTopMarginUIViewAutoresizingFlexibleBottomMargin,那么UIKit会这一轴上平均的分配任意大小

autoresizingMask图示

上面的配置同xib中的Autoresizing inspector来设置autoresizingMask最简单,而且还有一个动画展示方便理解.

注意:如果view的transform的值不为identity transform,那么view的frame会失效,同样地对autoresizingMask也是一样.

当对view进行了autoresizing设置好,UIKit还有提供一个接口开发者手动的调整view的布局.

手动对view的布局进行调整

当一个view的size改变时,UIKit利用autoresizingMask对view的subview进行autoresizing,然后调用view的layoutSubViews方法,以供开发者手动调整.你可以在自定义view中重写该方法:

特别提醒:如果你的应用中有个需要滚动显示大量视图的view,那么layoutSubviews方法中的代码很重要. 因为用一大块显示所有的内容是不现实的,通常的做法是将大量内容分块显示在subview中,就像砖头(tile View)一样,可以复用. 所以view滚动时,在layoutSubViews中需要将显示完的tile View的位置放到即将要显示的位置,然后重绘它的内容. 关于如何显示tileview的具体做法可以参考Apple的demoScrollViewSuite

当你进行布局时,代码中要确认下面几件事:

在运行时修改view

view会因为用户事件改变(size,position,hidden,或者创建一个view的层级树等),在iOS中view的改变可以发生下面的位置或者一下面的方法就行改变:

view controller是view的层级树的管理者,大部分的view的修改都发生在这里,controller是view改变的终极负责人. 特别地,你可以在view controller中的setEditing:animated:方法中,将用户界面切换到可编辑模式.

Animation block中是另一个频繁需要修改view的地方. UIView内置的动画接口可以做一些简单的动画,比如你可以用如下几个方法进行view的切换动画:

CoreAnimation Layers的交互

每个view都一个layer用来展示内容和动画. 尽管你通过view可以做很多,但你也可以直接操作view的layer

修改view的layer class

view中的layer类型在view创建后是不能修改的,因此可以通过view的layerClass类方法修改layer的类型.这个方法的默认实现是返回[CALayer class],你可以在自定义view中重写该方法然后返回想要的layer类型,如下代码返回CATiledLayer类型.

+ (Class)layerClass {
    return [CATiledLayer class];
}

每个view在初始化实例之前会调用上面的方法返回layer的类型,然后根据类型创建layer对象. 另外将view自己设置为layer的delegate,此时layer和view的联系就建立起来了,之后不能改变,你不能再将view自己设置为别的layer的delegate,如果你修改layer和view之间的关系,会导致view的内容绘制出问题,和其他一些不可预的问题(比如crash掉)

知道其他Layer类型和作用吗?请看Core Animation Reference Collection

往view中插入其他layer对象

如果你偏向使用layer而不是view,那么你可以将一个自定义的layer插入到view中. 一个自定义的layer对象是一个没有任何view绑定的CALayer实例. 自定义layer中要使用Core Animation代码,layer无法响应事件只能绘制内容,可以响应view的size变化
下面的代码展示了,如何使用layer,该layer用来显示一个图像:

- (void)viewDidLoad {
    [super viewDidLoad];
 
    // Create the layer.
    CALayer* myLayer = [[CALayer alloc] init];
 
    // Set the contents of the layer to a fixed image. And set
    // the size of the layer to match the image size.
    UIImage layerContents = [[UIImage imageNamed:@"myImage"] retain];
    CGSize imageSize = layerContents.size;
 
    myLayer.bounds = CGRectMake(0, 0, imageSize.width, imageSize.height);
    myLayer = layerContents.CGImage;
 
    // Add the layer to the view.
    CALayer*    viewLayer = self.view.layer;
    [viewLayer addSublayer:myLayer];
 
    // Center the layer in the view.
    CGRect        viewBounds = backingView.bounds;
    myLayer.position = CGPointMake(CGRectGetMidX(viewBounds), CGRectGetMidY(viewBounds));
}

你可以往view中加入多个layer,因为view的layer也有个数组属性sublayers来保存加入view中的layer, 具体请看Core Animation Programming Guide

如何自定义view

当UIKit提供的view无法满足需求时,就必须走上自定义view的道路. 自定义view可以完全由你控制,非常灵活.

注意:如果你使用OpenGL ES绘制内容的话,你必须使用GLKView代替继承UIView.具体请看OpenGL ES Programming Guide

关于实现自定义View的基本操作

实现自定义view要做的事主要有两件:①展示内容 ②管理view的交互,当想更好的实现自定义view光这两点还不够,下面列举了实现自定义view需要完成的步骤:

另外,在重写上面提到的方法中,你可以对view的许多属性进行操作,比如contentMode,也可以直接地或间接地的操作layer

初始化自定义view

每个自定义的view都需要提供initWithFrame:初始化方法.该方法在你手动创建的view初始化时调用.下面的代码展示了一个initWithFrame:方法的模板,在重写该方法时,你需要调用父类的的方法,设置view的状态,初始化实例变量,然后再将初始化完成的view返回.

- (id)initWithFrame:(CGRect)aRect {
    self = [super initWithFrame:aRect];
    if (self) {
          // setup the initial properties of the view
          ...
       }
    return self;
}

如果从nib文件中加载view,那么你要记得回调用initWithCoder:方法而不是initWithFrame:,该方法是协议NSCoding的一部分. 在该方法中,你可以view的状态进行设置,也可以重写awakeFromNib方法对view进一步设置.

实现重绘

如果自定义view需要绘制内容,那么需要重写drawRect:方法,在刚方法中实现重绘. Apple建议如果不是迫不得已的话,最好还是不要走重绘的路,可以用其他view代替.

drawRect:方法中只能干和内容绘制相关的内容,像更APP的数据结构等其他和绘制无关的操作千万不要放到这个方法中.该方法中的任务要尽量快速完成,如果你频繁调该方法的话,那么需要优化你的绘制算法,能够快速完成.

在调用drawRect:方法前,UIKit会先给view配置内容绘制环境. 特别是创建graphic context对象和调整坐标系. 当环境创建后,你才能用UIKit和core graphic等技术进行绘制.可以通过UIGraphicsGetCurrentContext方法来获取当前绘画上下文.

注意:当前绘画上下文(current graphics context)只要在调用drawRect:时有效. UIKit可能会在不同绘制操作步骤中创建不同的绘画上下文,所以你不要将该对像缓存起来供未来使用.

下面代码展示了使用drawRect:方法绘制一个边宽为10.0的view:

- (void)drawRect:(CGRect)rect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGRect    myFrame = self.bounds;
 
    // Set the line width to 10 and inset the rectangle by
    // 5 pixels on all sides to compensate for the wider line.
    CGContextSetLineWidth(context, 10);
    CGRectInset(myFrame, 5, 5);
 
    [[UIColor redColor] set];
    UIRectFrame(myFrame);
}

如果你知道你的view的内容是不透明的,那么你可以将view的opaque属性设置为YES,这样可以提高性能. 如果你设置NO的话,UIKit还要绘制被view遮住的内容.
另外一个提高view性能的操作是设置clearsContextBeforeDrawing为NO,特别地,当滚动view的时候.如果你设置为YES的话,在drawRect方法更新内容之前,UIKit要自动地将view设置透明黑色. 设置NO可以避免这一操作.

响应事件

view是一个响应者(因为UIView集成UIResponder). 为了能够直接响应事件,view可以通过手势监听像taps,swipes,pinches等等这些手势,但这是Apple封装好的,你要可以重写view的touches方法来自定义响应事件:

如果你想开启多手指事件设置multipleTouchEnable为YES.
有的view,比如label是默认关闭监听用户事件的,既可以设置userInteractionEnabled为YES
你可以通过UIApplication对象的beginIgnoringInteractionEventsendIgnoringInteractionEvents方法来控制整个APP的事件响应能力

注意:用UIView提供的动画方法进行动画时是无法响应用户事件的. 你可以通过重写相应方法来配置相应的特性,具体细节请看本系列文章(四)
在事件传递过程中,可以通过hitTest:withEvent:pointInside:withEvent:方法判断一个view是否具有响应特定event的能力.

垃圾清理-dealloc

自定义view有时需要用到该方法来清理垃圾. 不过很少用.

上一篇 下一篇

猜你喜欢

热点阅读