美文网首页
iOS View 编程指导(三)-View

iOS View 编程指导(三)-View

作者: 陵无山 | 来源:发表于2018-09-15 18:53 被阅读88次

    View

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

    • 布局管理
      • a view能够定义和父视图相关的,默认的大小变动行为
      • a view使用一个数组管理它的subviews
      • a view能够改变subview的size和position
      • a view能将一个坐标系下的point转换到另一个坐标系的点
    • 绘制内容和动画
      • 能够在一个矩形区域绘制内容
      • view的属性可以做动画
    • 响应事件
      • view可以接受事件
      • view参与响应链
        本文讲解如何创建view,管理views,绘制内容,view的层级树,view如何处理事件传递(更多内容请看Event Handling Guide for iOS)

    创建和设置View

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

    使用XIB创建View

    • xib创建view是一种便捷方式,在xib中你可以拖拽UI元素进入你的界面,配置各种属性. xib的另一个好处是,所见即所得(xib中见到和运行时的一样)方便调试. 你可以将view的行为和代码绑定起来,这样view可以进行用户交互. 创建好后,xib会将view和view状态等配置信息保存在nib文件中(一种资源文件)
    • 通常一个nib文件代表一个整个view层级树,顶层是controller的view,然后再往controller的view中添加其他view. 要注意顶层view的大小要和设备以及内容匹配.
    • 通常一个nib文件是和viewController绑定在一起的,在使用是controller会自动从nib中加载UI界面; 如果你的nib文件没有和controller绑定在一起的话,可以使用NSBundle或者UINib来手动从nib文件中加载界面.

    想要学习更多的关于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使用addSubview:方法,该方法将subview添加superview的属性subviews数组中末尾
    • 要将subview加入superview的subviews中的某一个为使用方法insertSubView:...
    • 要想将某个view位置改变一下,可以使用bringSubviewToFront:,sendSubviewToBack:,exchangeSubviewAtIndex:withSubviewAtIndex:,使用这些方法比使用add,remove,insert等方法要快.
    • 想将一个view从superview中移除,可以使用removeFromSuperview方法

    当一个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,你可以在重写下面方法来监听该通知:

    • willMoveToSuperview:,willMoveToWindow:,willRemoveSubview:
    • didAddSubview:,didMoveToSuperview,didMoveToWindow
      你可以使用上述通知来做一些和view层级树变动有关的操作

    隐藏view

    • 有两种方式:①设置属性hidden为YES②设置属性alpha为0.0
    • 隐藏的view不会响应事件,但会参与view的布局
    • 如果想移除一个view通常隐藏该view,特别是当该view在未来某刻需要显示
    • 如果想给view做个隐藏/显示动画,那么你应该使用alpha而不是hidden

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

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

    • 有两种方法:①通过保存一个该view的一个引用 ②设定一个唯一性的tag值,在使用viewWithTag:寻找
    • 通过引用方法的经常使用,但使用tag的方法更加灵活硬编码少点.而且tag的方式也可以用来做数据的持久化操作,界面的恢复.比如,在做界面恢复操作时,可以先用个文件保存view的tag,然后将该文件写到磁盘中,比把正界面保存好多了.在界面恢复时,根据tag值可以快速确定view间的关系和是否需要显示.

    view的位移/缩放/旋转

    • 每个view都有个transform属性用来给view做仿射变换的, 改变view的transform会影响view的最终渲染的结果,一般用于实现滚动,动画,等视觉效果.
    • view的属性transform的类型是一个CGAffineTransform结构体,默认值是identity transform(不会改变view外观).你可以随时给transform赋值,如下:
    // 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如何旋转一张图片:


    旋转一张图片
    • 给view添加的多个仿射变换的顺序会影响最终结果,比如选择后位移和位移后选择的结果是不一样的,即使旋转和位移的次数相同.做放射变换时view的center是不会变的,想知道更多的知识请看文档Quartz 2D Programming Guide中的Transforms

    切换不同的坐标系

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

    • convertPoint:fromView:
    • convertRect:fromView:
    • convertPoint:toView:
    • convertRect:toView:

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

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

    • convertPoint:fromWindow:
    • convertRect:fromWindow:
    • convertPoint:toWindow:
    • convertRect:toWindow:

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


    转换旋转后的view的坐标

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

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

    为布局变动做准备

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

    • 改变view中bounds的size
    • 旋转了界面方向,通常会改变root view的bounds
    • view的layer中加了CoreAnimation要求改变布局
    • 调用了view的setNeedsLayoutlayoutIfNeeded方法
    • 给view的layer发送setNeedsLayout消息

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

    • 当view的size改变时,view可以用属性autoresizesSubviews来控制subviews是否要重新resize. 如果给整个属性设置为NO,那么当view改变时它的subview也不会重新布局.
    • subview使用autoresizingMask来决定subview如何进行大小和位置的设置.
    • 同样的规则对subview的subview同样有效.

    在自动布局的时候,给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中重写该方法:

    • 调整subview的size和position
    • 添加或者移除subview或者CoreAnimation layer
    • 给subview发送setNeedsDisplaysetNeedsDisplayInRect:消息强制subview重绘

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

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

    • 当旋转手机屏幕时,你的布局代码是否还能正确生效
    • 你的布局代码能否适应status bar高度的改变,因为status bar有时会变,比如电话进来后status bar的高会变化.
      想学更多关于autoresizing的知识请参考苹果文档Handling Layout Changes Automatically Using Autoresizing Rules

    在运行时修改view

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

    • 在view controller中
      • view controller负责创建界面需要的view,可以从nib文件中加载,也可以从代码中创建,而且controller也负责干掉无用了的view
      • 当屏幕旋转时,controller负责调整view(大小位置隐藏创建等改变)
      • 当controller处理可编辑内容时,在进入/退出可编辑状态时,controller可能会调整view的层级树; 比如,添加一个额外的button和其他控件来处理编辑内容,这需要调整view的层级树.
    • 在Animation block中
      • 你可能会在Animation block处理两组view的切换,隐藏界面中的一组view然后显示另一组view
      • 当你需要实现一个特殊的动画时,你在Animation block中会对view的属性进行各种调整; 比如改变一个view的size
    • 其他方式
      • 你可能创建一组新的view以响应手势或者其它用户事件
      • 当你滚动scroll view时,你可能会同时隐藏和显示tile subview
      • 当键盘显示时,你可能会reposition和resize被键盘遮住的部分view,关于更多和键盘交互的知识请看Text Programming Guide for iOS

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

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

    • transitionWithView:duration:options:animations:completion:
    • transitionFromView:toView:duration:options:completion:

    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定义几个何时的初始化方法:
      • 如果手动创建,需要重写initWithFrame:方法,或者自定义一个初始化方法
      • 如果重nib文件中加载,重写initWithCoder:方法,在该方法中对view进行一些状态设置
    • 显示dealloc方法,用来销毁一些对象的
    • 要想定制任何内容就需要重写drawRect:方法:
    • 设置属性autoresizingMask给view加上autoresizing功能
    • 如果你的view需要集成和管理许多的subview,那么:
      • 在初始化view的时候,创建subviews
      • 在创建subview的时候顺便设置各个subview的autoresizingMask属性
      • 如果view的subview需要手动布局,重写view的layoutSubviews
    • 实现touch-event,那么:
      • 通过addGestureRecognizer:方法给view添加合适的手势
      • 如果你想手动处理touches,那么可以重写view的touchesBegan:withEvent:, touchesMoved:withEvent:, touchesEnded:withEvent:,touchesCancelled:withEvent: 四个方法(不管其他touch方法有没有重写,牢记你需要始终重写touchesCancelled:withEvent:方法)
    • 如果你想定制打印的view,那么你需要重写drawRect:forViewPrintFormatter:方法,具体请看Drawing and Printing Guide for iOS

    另外,在重写上面提到的方法中,你可以对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方法来自定义响应事件:

    • touchesBegan:withEvent:
    • touchesMoved:withEvent:
    • touchesEnded:withEvent:
    • touchesCancelled:withEvent:

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

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

    垃圾清理-dealloc

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

    相关文章

      网友评论

          本文标题:iOS View 编程指导(三)-View

          本文链接:https://www.haomeiwen.com/subject/hnetnftx.html