VC学习笔记

VC学习笔记

image: ../Art/VCPG_ControllerHierarchy_fig_1-1_2x.png

一个视图控制器只应当管理其根视图控制器,规范子视图的尺寸和位置。其子视图的内容应该由其子视图控制器管理。

image: ../Art/VCPG_ContainerViewController_fig_1-2_2x.png

图像:../Art/VCPG_CustomSubclasses_fig_1-3_2x.png

视图控制器充当它管理的视图和应用视图数据之间的中介

用户交互

VC能够处理事件响应链,但是往往将其由views处理并返回结果给代理或者对象类,因此大部分事件是由代理方法或者动作方法进行处理的。

适应性

视图控制器负责将视图呈现并调整该呈现以匹配底层环境,每一个ios都应该在多个不同尺寸的iphone上运行。

视图控制器定义

常用父类

UITableViewController 列表视图
UICollectionViewController 集合视图
UIViewController其他视图

故事板

image: ../Art/storyboard_bird_sightings_2x.png

每一个新的项目均有一个主故事板,可以通过拖拽的形式添加视图控制器

处理交互

VC通常不会直接处理事件。

  • 定义动作方法,主要响应:
    • 具体行动。控件和某些视图调用操作方法来报告特定的交互。
    • 手势识别。手势调用一个动作方法来报告手势的状态,使用其处理更新状态或者响应完成手势。
  • 作为观察者,接受来自系统或者其他类的消息。
  • 作为一个数据源或者代理

如果需要更新view的内容,一般需要使用插座

@interface MyViewController : UIViewController
@property (weak, nonatomic) IBOutlet UIButton *myButton;
@property (weak, nonatomic) IBOutlet UITextField *myTextField;
 
- (IBAction)myButtonAction:(id)sender;
 
@end

在故事板中,我们需要记得在对应的视图中连接插座和动作,

运行时显示视图

UIkit步骤

  1. 使用故事板实例化视图

  2. 连接所有的插座和动作

  3. 将根视图赋值到VC的属性

  4. 调用视图控制器的awakeFromNib方法。

    调用此方法时,视图控制器的特征集合为空,视图可能不在其最终位置。

  5. 调用视图控制器的viewDidLoad方法。

    使用此方法为您的视图添加或删除视图、修改布局约束和加载数据。

除此之外,可以有其他机会对视图显示的之前或者之后进行调整,

  1. Calls the view controller’s viewWillAppear: method to let it know that its views are about to appear onscreen.
  2. Updates the layout of the views.
  3. Displays the views onscreen.
  4. Calls the viewDidAppear: method when the views are onscreen.

管理视图布局

当视图的大小和位置发生变化时,UIKit 会更新视图层次结构的布局信息。对于使用自动布局配置的视图,UIKit 使用自动布局引擎并使用它根据当前约束更新布局。UIKit 还让其他感兴趣的对象,例如活动呈现控制器,知道布局更改,以便它们可以做出相应的响应。

在布局过程中,UIKit 会在多个点通知您,以便您可以执行其他与布局相关的任务。使用这些通知来修改布局约束或在应用布局约束后对布局进行最终调整。在布局过程中,UIKit 会为每个受影响的视图控制器执行以下操作:

  1. 根据需要更新视图控制器及其视图的特征集合;

  2. 调用视图控制器的viewWillLayoutSubviews方法。

  3. 调用containerViewWillLayoutSubviews当前UIPresentationController对象的方法。

  4. 调用layoutSubviews视图控制器的根视图的方法。

    此方法的默认实现使用可用约束计算新布局信息。然后该方法遍历视图层次结构并调用layoutSubviews每个子视图。

  5. 将计算出的布局信息应用于视图。

  6. 调用视图控制器的viewDidLayoutSubviews方法。

  7. 调用containerViewDidLayoutSubviews当前UIPresentationController对象的方法。

有效管理布局的一些技巧:

  • **使用自动布局。**您使用自动布局创建的约束是一种灵活且简单的方式,可以将您的内容放置在不同的屏幕尺寸上。
  • **利用顶部和底部布局指南。**将内容布置到这些指南可确保您的内容始终可见。顶部布局指南的位置影响状态栏和导航栏的高度。同样,底部布局指南的位置会影响选项卡栏或工具栏的高度。
  • **请记住在添加或删除视图时更新约束。**如果动态添加或删除视图,请记住更新相应的约束。
  • **在为视图控制器的视图设置动画时暂时移除约束。**使用 UIKit Core Animation 为视图设置动画时,请移除动画持续时间的约束,并在动画完成时将其添加回来。如果视图的位置或大小在动画期间发生变化,请记住更新约束。

实现容器视图控制器

UINavigationController

图像:../Art/VCPG_structure-of-navigation-interface_5-1_2x.png

UISplitViewController

一个UISplitViewController对象以主从排列的方式显示两个视图控制器的内容。在这种安排下,一个视图控制器(主视图)的内容决定了另一个视图控制器显示的细节。两个视图控制器的可见性是可配置的,但也受当前环境的控制。在规则的水平环境中,拆分视图控制器可以并排显示两个子视图控制器,也可以隐藏主视图控制器并根据需要显示它。在紧凑的环境中,拆分视图控制器一次只显示一个视图控制器。

图像:../Art/VCPG-split-view-inerface_5-2_2x.png

子视图控制器添加

要以编程方式将子视图控制器合并到您的内容中,请通过执行以下操作在相关视图控制器之间创建父子关系:

  1. 调用addChildViewController:容器视图控制器的方法。

    这个方法告诉 UIKit 你的容器视图控制器现在正在管理子视图控制器的视图。

  2. 将子视图的根视图添加到容器的视图层次结构中。

    作为此过程的一部分,请始终记住设置孩子框架的大小和位置。

  3. 添加任何约束来管理子视图的大小和位置。

  4. 调用didMoveToParentViewController:子视图控制器的方法。

向容器添加子视图控制器
- (void) displayContentController: (UIViewController*) content {
   [self addChildViewController:content];
   content.view.frame = [self frameForContentController];
   [self.view addSubview:self.currentClientView];
   [content didMoveToParentViewController:self];
}

在子视图控制器之间转换

- (void)cycleFromViewController: (UIViewController*) oldVC
               toViewController: (UIViewController*) newVC {
   // Prepare the two view controllers for the change.
   [oldVC willMoveToParentViewController:nil];
   [self addChildViewController:newVC];
 
   // Get the start frame of the new view controller and the end frame
   // for the old view controller. Both rectangles are offscreen.
   newVC.view.frame = [self newViewStartFrame];
   CGRect endFrame = [self oldViewEndFrame];
 
   // Queue up the transition animation.
   [self transitionFromViewController: oldVC toViewController: newVC
        duration: 0.25 options:0
        animations:^{
            // Animate the views to their final positions.
            newVC.view.frame = oldVC.view.frame;
            oldVC.view.frame = endFrame;
        }
        completion:^(BOOL finished) {
           // Remove the old view controller and send the final
           // notification to the new view controller.
           [oldVC removeFromParentViewController];
           [newVC didMoveToParentViewController:self];
        }];
}

构建容器视图控制器的建议

设计、开发和测试新的容器视图控制器需要时间。尽管单个行为很简单,但控制器作为一个整体可能非常复杂。在实现自己的容器类时,请考虑以下提示:

  • **仅访问子视图控制器的根视图。**容器应该只访问每个子级的根视图——即子级view属性返回的视图。它永远不应该访问孩子的任何其他视图。
  • **子视图控制器应该对其容器有最少的了解。**子视图控制器应该专注于它自己的内容。如果容器允许其行为受到子项的影响,则应使用委托设计模式来管理这些交互。
  • **首先使用常规视图设计容器。**使用常规视图(而不是来自子视图控制器的视图)让您有机会在简化的环境中测试布局约束和动画过渡。当常规视图按预期工作时,将它们换成子视图控制器的视图。

视图和窗口架构

img

核心动画层对象的使用对性能有重要影响。视图对象的实际绘制代码尽量少调用,当调用代码时,结果会被Core Animation缓存起来,尽可能多的在后面复用。重用已经渲染的内容消除了更新视图通常需要的昂贵的绘制周期。在动画期间重用这些内容尤其重要,在动画中可以操纵现有内容。这种重用比创建新内容要便宜得多。

查看层次结构和子视图管理

除了提供自己的内容之外,视图还可以充当其他视图的容器。当一个视图包含另一个视图时,将在两个视图之间创建父子关系。关系中的子视图称为子视图,父视图称为父视图。这种关系的创建对应用程序的视觉外观和应用程序的行为都有影响。

在视觉上,子视图的内容掩盖了其父视图的全部或部分内容。如果子视图是完全不透明的,那么子视图所占据的区域就完全遮蔽了父视图的对应区域。如果子视图部分透明,则两个视图的内容在显示在屏幕上之前混合在一起。每个超级视图将其子视图存储在有序数组中,该数组中的顺序也会影响每个子视图的可见性。如果两个同级子视图相互重叠,则最后添加(或移动到子视图数组末尾)的一个出现在另一个之上。

超视图-子视图关系也会影响多个视图行为。更改父视图的大小会产生连锁反应,这会导致任何子视图的大小和位置也发生变化。当您更改父视图的大小时,您可以通过适当地配置视图来控制每个子视图的大小调整行为。影响子视图的其他更改包括隐藏超级视图、更改超级视图的 alpha(透明度)或将数学变换应用于超级视图的坐标系。

视图层次结构中视图的排列也决定了您的应用程序如何响应事件。当在特定视图内发生触摸时,系统将带有触摸信息的事件对象直接发送到该视图进行处理。然而,如果视图不处理特定的触摸事件,它可以将事件对象传递给它的超级视图。如果 superview 不处理事件,它会将事件对象传递给它的 superview,依此类推响应者链。特定的视图也可以将事件对象传递给介入的响应者对象,例如视图控制器。如果没有对象处理该事件,它最终会到达应用程序对象,该对象通常会丢弃它。

视图绘制周期

UIView课程使用按需绘图模型来呈现内容。当一个视图第一次出现在屏幕上时,系统要求它绘制它的内容。系统捕获此内容的快照并将该快照用作视图的视觉表示。如果您从不更改视图的内容,则可能永远不会再次调用视图的绘制代码。大多数涉及视图的操作都会重复使用快照图像。如果您确实更改了内容,则会通知系统视图已更改。然后视图重复绘制视图和捕获新结果的快照的过程。

当视图的内容发生更改时,您不会直接重绘这些更改。相反,您可以使用setNeedsDisplayorsetNeedsDisplayInRect:方法使视图无效。这些方法告诉系统视图的内容发生了变化,需要在下一次重绘。在启动任何绘图操作之前,系统会等待当前运行循环结束。这种延迟使您有机会一次性使多个视图无效、在层次结构中添加或删除视图、隐藏视图、调整视图大小和重新定位视图。然后,您所做的所有更改都会同时反映出来。

当需要渲染视图的内容时,实际的绘制过程会因视图及其配置而异。系统视图通常实现私有绘图方法来呈现其内容。这些相同的系统视图通常会公开可用于配置视图实际外观的接口。对于自定义UIView子类,您通常会覆盖drawRect:视图的方法并使用该方法绘制视图的内容。还有其他方法可以提供视图的内容,例如直接设置底层的内容,但覆盖该drawRect:方法是最常用的技术

内容模式

每个视图都有一个内容模式,用于控制视图如何回收其内容以响应视图几何形状的变化以及是否回收其内容。当一个视图第一次显示时,它会像往常一样呈现其内容,并将结果捕获在底层位图中。之后,对视图几何的更改并不总是会导致重新创建位图。相反,contentMode属性中的值决定了位图是应该缩放以适应新的边界还是简单地固定到视图的一个角落或边缘。

每当您执行以下操作时,都会应用视图的内容模式:

  • 更改视图framebounds矩形的宽度或高度。
  • 将包含缩放因子的转换分配给视图的transform 属性。

默认情况下,contentMode大多数视图的属性设置为UIViewContentModeScaleToFill,这会导致视图的内容被缩放以适应新的框架大小。图 1-2显示了一些可用内容模式的结果。从图中可以看出,并非所有的内容模式都会导致视图的边界被完全填充,而那些这样做可能会扭曲视图的内容。

内容模式比较

可伸缩视图

您可以将视图的一部分指定为可拉伸的,这样当视图的大小发生变化时,只会影响可拉伸部分的内容。您通常将可拉伸区域用于按钮或其他视图,其中视图的一部分定义了可重复的模式。您指定的可拉伸区域可以允许沿视图的一个轴或两个轴进行拉伸。当然,当沿着两个轴拉伸视图时,视图的边缘也必须定义一个可重复的模式以避免任何扭曲。图 1-3显示了这种扭曲如何在视图中表现出来。每个视图原始像素的颜色被复制以填充较大视图中的相应区域。

图 1-3 拉伸按钮的背景img

您可以使用该contentStretch属性指定视图的可拉伸区域。此属性接受一个矩形,其值被归一化到范围0.01.0。拉伸视图时,系统会将这些归一化值乘以视图的当前边界和比例因子,以确定需要拉伸的一个或多个像素。标准化值的使用减轻了contentStretch每次视图边界更改时更新属性的需要。

视图的内容模式也在决定如何使用视图的可拉伸区域方面发挥作用。可拉伸区域仅在内容模式会导致视图内容被缩放时使用。这意味着,拉伸的观点仅与支持UIViewContentModeScaleToFillUIViewContentModeScaleAspectFit以及UIViewContentModeScaleAspectFill内容模式。如果您指定将内容固定到边缘或角落的内容模式(因此实际上不会缩放内容),则视图将忽略可拉伸区域。

内置动画支持

在每个视图后面都有一个图层对象的好处之一是您可以轻松地为许多与视图相关的更改设置动画。动画是一种向用户传达信息的有用方式,在设计应用程序时应始终加以考虑。UIView该类的许多属性都是可动画的——也就是说,存在半自动支持从一个值到另一个值的动画。要为这些可动画属性之一执行动画,您所要做的就是:

  1. 告诉 UIKit 你想要执行一个动画。
  2. 更改属性的值。

您可以在UIView对象上设置动画的属性如下:

  • frame- 使用它来为视图的位置和大小变化设置动画。
  • bounds- 使用它来动画更改视图的大小。
  • center- 使用它来为视图的位置设置动画。
  • transform- 使用它来旋转或缩放视图。
  • alpha— 使用它来更改视图的透明度。
  • backgroundColor— 使用它来更改视图的背景颜色。
  • contentStretch— 使用它来更改视图内容的拉伸方式。

动画非常重要的一个地方是从一组视图过渡到另一组视图时。通常,您使用视图控制器来管理与用户界面各部分之间的主要更改相关联的动画。例如,对于涉及从较高级别导航到较低级别信息的界面,您通常使用导航控制器来管理显示每个连续数据级别的视图之间的转换。但是,您也可以使用动画而不是视图控制器在两组视图之间创建过渡。您可能会在标准视图控制器动画无法产生您想要的结果的地方这样做。

除了使用 UIKit 类创建的动画之外,您还可以使用 Core Animation 层创建动画。下拉到图层级别可以让您更好地控制动画的时间和属性。

坐标系

查看坐标系

一般有两个坐标系,“绝对坐标”和”相对坐标“,分别对应UIWindow和UIView。

框架、边界和中心属性的关系

  • Frame指定视图在其父视图坐标系中的大小和位置
  • bounds指定其本地坐标系中的大小和内容原点
  • center包含视图在其父视图坐标系中的已知中心点

如果改变其大小,首选使用center,即使经过缩放和旋转均有效,

The same is not true for the value in the frame property, which is considered invalid if the view’s transform is not equal to the identity transform.

变换往往会改变frame的值

这三者可以独立于其他属性,但是改变其中的一个属性会导致:

  • 设置frame的时候,bounds会更改以匹配新的大小,center属性中的值会类似的更改匹配中心点
  • 设置center的时候,frame中的原点值会更改
  • 设置bounds的时候,frame中的size会更改以匹配新的bounds

坐标系变换

  • 修改整个视图,需要修改transform中的affine transform
  • 修改drawrect中的特定内容,需要修改其上下文的affine transform

每一个子视图的坐标系建立在其祖先的坐标系上,因此修改transform属性的时候,这个更改会影响视图和其所有的子视图。每一个视图绘制的内容相对于其边界布置。

点和像素

任何坐标和距离均使用浮点坐标值表示,这个可测量的点值由系统进行转换成绘制的像素。

基于点的测量使用设备定义的用户坐标空间。一个点不一定对应屏幕上的一个像素

自定义视图

主要集成点

  • 事件处理方法(event-handling methods)

    • touchesBegan:withEvent:
    • touchesMoved:withEvent:
    • touchesEnded:withEvent:
    • touchesCancelled:withEvent:
  • layoutSubview方法,

  • drawRect方法

这是通常来说主要重写的几个方法。如果使用gestrue recognizers处理事件,则不需要重写任何事件处理方法。类似的,如果不需要包含子视图或者不需要改变其size,则不需要重写layoutSubview。draw一般用来画图(UIKit和CG)

更有效的使用视图

一般来说,在性能优化之前,可以首先进行数据收集,测试性能并确认由性能问题,然后再针对问题进行性能优化。

V和VC一般不是一一对应的关系

VC一般来说控制其行为。

最少化自定义画图

如果可以实现,应该尽量使用有着更好的性能和兼容性的系统视图。

使用内容模式

内容模式可以控制显示的内容。这种模式最大限度的减少了重绘视图所花费的时间。默认情况下,视图使用 UIViewContentModeScaleToFill内容模式,它缩放视图的现有内容以适合视图的框架矩形。您可以根据需要更改此模式以不同方式调整您的内容,但如果可以,应该避免使用内容模式UIViewContentModeRedraw。无论哪种内容模式有效,您终可以通过调用setNeedsDisplay或来强制您的视图重绘其内容setNeedsDisplayInRect:

尽可能将视图声明为不透明(Opaque)

使用不透明来确定视图是否可以优化合成操作。声明为不透明的内容将不会对后面的内容进行渲染。

滚动时调整视图的绘制行为

滚动时往往需要在短时间绘制大量的视图更新,可以考虑暂时降低渲染内容的质量或者更改内容模式。当滚动停止的时候可以将视图返回至之前的状态并根据需要更新内容。

不要通过嵌入子视图自定义控件

即使可以通过将子视图添加到标准控件。如果该控件的实现发生更改,有可能导致运行不正确。

Window

Window创建

self.window = [[[UIWindows alloc] initWithFrame:[[UIScreen mainScreee] bounds]] autorelease];

添加内容

可以通过[window addSubview:viewController.view];或者设置rootViewController实现。

更改窗口层次

每个UIWindow对象都有一个可配置的windowLevel属性,用于确定该窗口相对于其他窗口的定位方式。大多数情况下,您不需要更改应用程序窗口的级别。新窗口在创建时自动分配到正常窗口级别。正常窗口级别表示该窗口呈现与应用程序相关的内容。较高的窗口级别保留用于需要浮动在应用程序内容之上的信息,例如系统状态栏或警报消息。尽管您可以自己将窗口分配到这些级别,但是当您使用特定界面时,系统通常会为您执行此操作。例如,当您显示或隐藏状态栏或显示警报视图时,系统会自动创建所需的窗口来显示这些项目。

PropertiesUsage
alpha, hidden, opaqueThese properties affect the opacity of the view. The alpha and hidden properties change the view’s opacity directly.The opaque property tells the system how it should composite your view. Set this property to YES if your view’s content is fully opaque and therefore does not reveal any of the underlying view’s content. Setting this property to YES improves performance by eliminating unnecessary compositing operations.
bounds, frame, center, transformThese properties affect the size and position of the view. The center and frame properties represent the position of the view relative to its parent view. The frame also includes the size of the view. The bounds property defines the view’s visible content area in its own coordinate system.The transform property is used to animate or move the entire view in complex ways. For example, you would use a transform to rotate or scale the view. If the current transform is not the identity transform, the frame property is undefined and should be ignored.For information about the relationship between the bounds, frame, and center properties, see The Relationship of the Frame, Bounds, and Center Properties. For information about how transforms affect a view, see Coordinate System Transformations.
autoresizingMask, autoresizesSubviewsThese properties affect the automatic resizing behavior of the view and its subviews. The autoresizingMask property controls how a view responds to changes in its parent view’s bounds. The autoresizesSubviews property controls whether the current view’s subviews are resized at all.
contentMode, contentStretch, contentScaleFactorThese properties affect the rendering behavior of content inside the view. The contentMode and contentStretch properties determine how the content is treated when the view’s width or height changes. The contentScaleFactor property is used only when you need to customize the drawing behavior of your view for high-resolution screens.For more information on how the content mode affects your view, see Content Modes. For information about how the content stretch rectangle affects your view, see Stretchable Views. For information about how to handle scale factors, see Supporting High-Resolution Screens In Views in Drawing and Printing Guide for iOS.
gestureRecognizers, userInteractionEnabled, multipleTouchEnabled, exclusiveTouchThese properties affect how your view processes touch events. The gestureRecognizers property contains gesture recognizers attached to the view. The other properties control what touch events the view supports.For information about how to respond to events in your views, see Event Handling Guide for iOS.
backgroundColor, subviews, drawRect: method, layer, (layerClass method)These properties and methods help you manage the actual content of your view. For simple views, you can set a background color and add one or more subviews. The subviews property itself contains a read-only list of subviews, but there are several methods for adding and rearranging subviews. For views with custom drawing behavior, you must override the drawRect: method.For more advanced content, you can work directly with the view’s Core Animation layer. To specify an entirely different type of layer for the view, you must override the layerClass method.

创建和管理视图层次结构

管理视图层次结构是开发应用程序用户界面的关键部分。视图的组织会影响应用程序的视觉外观以及应用程序对更改和事件的响应方式。例如,视图层次结构中的父子关系决定了哪些对象可以处理特定的触摸事件。同样,父子关系定义了每个视图如何响应界面方向的变化。

时钟应用程序中的分层视图

将子视图添加到其父视图时,子视图的当前框架矩形表示其在父视图内的初始位置。默认情况下不会剪裁框架位于其父视图可见边界之外的子视图。如果你希望你的子视图被裁剪到超级视图的边界,你必须明确地将clipsToBounds超级视图的属性设置为YES

隐藏视图

要在视觉上隐藏视图,您可以将其hidden 属性设置为YES或将其alpha属性更改为0.0。隐藏视图不会从系统接收触摸事件。但是,隐藏视图确实参与了与视图层次结构相关的自动调整大小和其他布局操作。因此,隐藏视图通常是从视图层次结构中删除视图的一种方便的替代方法,特别是如果您打算很快再次显示视图。

在视图层次结构中定位视图

有两种方法可以在视图层次结构中定位视图:

  • 将指向任何相关视图的指针存储在适当的位置,例如在拥有视图的视图控制器中。
  • 为每个视图的tag属性分配一个唯一的整数并使用该viewWithTag:方法定位它。

存储对相关视图的引用是定位视图的最常用方法,并使访问这些视图非常方便。如果您使用 Interface Builder 创建视图,则可以使用outlet将 nib 文件中的对象(包括代表管理控制器对象的 File’s Owner 对象)相互连接。对于以编程方式创建的视图,您可以将这些视图的引用存储在私有成员变量中。无论您使用出口还是私有成员变量,您都有责任根据需要保留视图,然后也释放它们。确保对象被正确保留和释放的最佳方法是使用声明的属性。

标签是减少硬编码依赖和支持更动态和灵活的解决方案的有用方法。您可以使用它的标签来定位它,而不是存储一个指向视图的指针。标签也是一种更持久的引用视图的方式。例如,如果您想保存应用程序中当前可见的视图列表,您可以将每个可见视图的标签写出到一个文件中。这比归档实际的视图对象更简单,尤其是在您只跟踪当前可见的视图的情况下。当您的应用程序随后被加载时,您将重新创建您的视图并使用保存的标签列表来设置每个视图的可见性,从而将您的视图层次结构返回到之前的状态。

平移、缩放和旋转视图

每个视图都有一个关联的仿射变换,您可以使用它来平移、缩放或旋转视图的内容。视图变换会改变视图的最终渲染外观,通常用于实现滚动、动画或其他视觉效果。

transform属性UIView包含CGAffineTransform要应用的转换的结构。默认情况下,此属性设置为标识转换,它不会修改视图的外观。您可以随时为此属性分配新的转换。例如,要将视图旋转 45 度,您可以使用以下代码:

// M_PI/4.0 是四分之一圆,或 45 度。
CGAffineTransform xform = CGAffineTransformMakeRotation(M_PI/4.0);
self.view.transform = xform;

在视图层次结构中转换坐标

在不同的时间,特别是在处理事件时,应用程序可能需要将坐标值从一个参考系转换为另一个参考系。例如,触摸事件报告每次触摸在窗口坐标系中的位置,但视图对象通常需要视图局部坐标系中的信息。在UIView类定义的坐标转换成与从视图的局部坐标系统中的以下方法:

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

这些convert...:fromView:方法将坐标从某个其他视图的坐标系转换为当前视图的局部坐标系(边界矩形)。相反,这些convert...:toView:方法将坐标从当前视图的局部坐标系(边界矩形)转换为指定视图的坐标系。如果您将nil任何方法指定为参考视图,则会在包含该视图的窗口的坐标系之间进行转换。

除了UIView转换方法之外,UIWindow该类还定义了几种转换方法。这些方法与UIView版本相似,不同之处在于这些方法不是在视图的局部坐标系之间进行转换,而是在窗口的坐标系之间进行转换。

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

在旋转视图中转换坐标时,UIKit 会在假设您希望返回的矩形反映源矩形覆盖的屏幕区域的情况下转换矩形。图 3-3显示了在转换过程中旋转如何导致矩形大小发生变化的示例。在图中,外部父视图包含旋转子视图。将子视图坐标系中的矩形转换为父视图坐标系会产生一个物理上更大的矩形。这个较大的矩形实际上outerView是完全包围旋转矩形的边界中的最小矩形。

使用自动调整大小规则自动处理布局更改

当您更改视图的大小时,任何嵌入的子视图的位置和大小通常需要更改以考虑其父视图的新大小。父视图的autoresizesSubviews 属性决定了子视图是否完全调整大小。如果此属性设置为YES,则视图使用autoresizingMask每个子视图的属性来确定如何调整子视图的大小和位置。任何子视图的大小更改都会为其嵌入的子视图触发类似的布局调整。

对于视图层次结构中的每个视图,将该视图的autoresizingMask属性设置为适当的值是处理自动布局更改的重要部分。表 3-2列出了可以应用于给定视图的自动调整大小选项,并描述了它们在布局操作期间的效果。您可以使用 OR 运算符组合常量,或者在将它们分配给autoresizingMask属性之前将它们添加在一起。如果你使用 Interface Builder 来组装你的视图,你可以使用 Autosizing 检查器来设置这些属性。

自动调整蒙版描述
UIViewAutoresizingNone视图不会自动调整大小。(这是默认值。)
UIViewAutoresizingFlexibleHeight当父视图的高度改变时,视图的高度也会改变。如果不包含此常量,则视图的高度不会改变。
UIViewAutoresizingFlexibleWidth当父视图的宽度改变时,视图的宽度也会改变。如果不包含此常量,则视图的宽度不会改变。
UIViewAutoresizingFlexibleLeftMargin视图左边缘和父视图左边缘之间的距离根据需要增长或缩小。如果不包含此常量,则视图的左边缘与父视图的左边缘保持固定距离。
UIViewAutoresizingFlexibleRightMargin视图的右边缘和父视图的右边缘之间的距离根据需要增长或缩小。如果不包含此常量,则视图的右边缘与父视图的右边缘保持固定距离。
UIViewAutoresizingFlexibleBottomMargin视图底部边缘和父视图底部边缘之间的距离根据需要增长或缩小。如果不包含此常量,则视图的底部边缘与父视图的底部边缘保持固定距离。
UIViewAutoresizingFlexibleTopMargin视图的顶部边缘和父视图的顶部边缘之间的距离根据需要增长或缩小。如果不包含此常量,则视图的顶部边缘与父视图的顶部边缘保持固定距离。

文档存档开发商

搜索

查看 iOS 编程指南

  • 目录

介绍视图和窗口架构查看架构基础查看几何和坐标系视图的运行时交互模型有效使用视图的技巧视窗观看次数创建和配置视图对象创建和管理视图层次结构在运行时调整视图的大小和位置在运行时修改视图与核心动画层交互定义自定义视图动画修订记录

下一个以前的

观看次数

因为视图对象是您的应用程序与用户交互的主要方式,所以它们有许多职责。这里仅仅是少数:

  • 布局和子视图管理
    • 视图定义了与父视图相关的默认大小调整行为。
    • 一个视图可以管理一个子视图列表。
    • A view can override the size and position of its subviews as needed.
    • A view can convert points in its coordinate system to the coordinate systems of other views or the window.
  • Drawing and animation
    • A view draws content in its rectangular area.
    • Some view properties can be animated to new values.
  • Event handling
    • A view can receive touch events.
    • A view participates in the responder chain.

This chapter focuses on the steps for creating, managing, and drawing views and for handling the layout and management of view hierarchies. For information about how to handle touch events (and other events) in your views, see Event Handling Guide for iOS.

Creating and Configuring View Objects

You create views as self-contained objects either programmatically or using Interface Builder, and then you assemble them into view hierarchies for use.

Creating View Objects Using Interface Builder

The simplest way to create views is to assemble them graphically using Interface Builder. From Interface Builder, you can add views to your interface, arrange those views into hierarchies, configure each view’s settings, and connect view-related behaviors to your code. Because Interface Builder uses live view objects—that is, actual instances of the view classes—what you see at design time is what you get at runtime. You then save those live objects in a nib file, which is a resource file that preserves the state and configuration of your objects.

You usually create nib files in order to store an entire view hierarchy for one of your application’s view controllers. The top level of the nib file usually contains a single view object that represents your view controller’s view. (The view controller itself is typically represented by the File’s Owner object.) The top-level view should be sized appropriately for the target device and contain all of the other views that are to be presented. It is rare to use a nib file to store only a portion of your view controller’s view hierarchy.

When using nib files with a view controller, all you have to do is initialize the view controller with the nib file information. The view controller handles the loading and unloading of your views at the appropriate times. However, if your nib file is not associated with a view controller, you can load the nib file contents manually using an NSBundle or UINib object, which use the data in the nib file to reconstitute your view objects.

For more information about how to use Interface Builder to create and configure your views, see Interface Builder User Guide. For information about how view controllers load and manage their associated nib files, see Creating Custom Content View Controllers in View Controller Programming Guide for iOS. For more information about how to load views programmatically from a nib file, see Nib Files in Resource Programming Guide.

Creating View Objects Programmatically

If you prefer to create views programmatically, you can do so using the standard allocation/initialization pattern. The default initialization method for views is the initWithFrame: method, which sets the initial size and position of the view relative to its (soon-to-be-established) parent view. For example, to create a new generic UIView object, you could use code similar to the following:

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

Note: Although all views support the initWithFrame: method, some may have a preferred initialization method that you should use instead. For information about any custom initialization methods, see the reference documentation for the class.

After you create a view, you must add it to a window (or to another view in a window) before it can become visible. For information on how to add views to your view hierarchy, see Adding and Removing Subviews.

Setting the Properties of a View

The UIView class has several declared properties for controlling the appearance and behavior of the view. These properties are for manipulating the size and position of the view, the view’s transparency, its background color, and its rendering behavior. All of these properties have appropriate default values that you can change later as needed. You can also configure many of these properties from Interface Builder using the Inspector window.

Table 3-1 lists some of the more commonly used properties (and some methods) and describes their usage. Related properties are listed together so that you can see the options you have for affecting certain aspects of the view.

PropertiesUsage
alpha, hidden, opaqueThese properties affect the opacity of the view. The alpha and hidden properties change the view’s opacity directly.The opaque property tells the system how it should composite your view. Set this property to YES if your view’s content is fully opaque and therefore does not reveal any of the underlying view’s content. Setting this property to YES improves performance by eliminating unnecessary compositing operations.
bounds, frame, center, transformThese properties affect the size and position of the view. The center and frame properties represent the position of the view relative to its parent view. The frame also includes the size of the view. The bounds property defines the view’s visible content area in its own coordinate system.The transform property is used to animate or move the entire view in complex ways. For example, you would use a transform to rotate or scale the view. If the current transform is not the identity transform, the frame property is undefined and should be ignored.For information about the relationship between the bounds, frame, and center properties, see The Relationship of the Frame, Bounds, and Center Properties. For information about how transforms affect a view, see Coordinate System Transformations.
autoresizingMask, autoresizesSubviewsThese properties affect the automatic resizing behavior of the view and its subviews. The autoresizingMask property controls how a view responds to changes in its parent view’s bounds. The autoresizesSubviews property controls whether the current view’s subviews are resized at all.
contentMode, contentStretch, contentScaleFactorThese properties affect the rendering behavior of content inside the view. The contentMode and contentStretch properties determine how the content is treated when the view’s width or height changes. The contentScaleFactor property is used only when you need to customize the drawing behavior of your view for high-resolution screens.For more information on how the content mode affects your view, see Content Modes. For information about how the content stretch rectangle affects your view, see Stretchable Views. For information about how to handle scale factors, see Supporting High-Resolution Screens In Views in Drawing and Printing Guide for iOS.
gestureRecognizers, userInteractionEnabled, multipleTouchEnabled, exclusiveTouchThese properties affect how your view processes touch events. The gestureRecognizers property contains gesture recognizers attached to the view. The other properties control what touch events the view supports.For information about how to respond to events in your views, see Event Handling Guide for iOS.
backgroundColor, subviews, drawRect: method, layer, (layerClass method)These properties and methods help you manage the actual content of your view. For simple views, you can set a background color and add one or more subviews. The subviews property itself contains a read-only list of subviews, but there are several methods for adding and rearranging subviews. For views with custom drawing behavior, you must override the drawRect: method.For more advanced content, you can work directly with the view’s Core Animation layer. To specify an entirely different type of layer for the view, you must override the layerClass method.

For information about the basic properties common to all views, see UIView Class Reference. For more information about specific properties of a view, see the reference documentation for that view.

Tagging Views for Future Identification

The UIView class contains a tag property that you can use to tag individual view objects with an integer value. You can use tags to uniquely identify views inside your view hierarchy and to perform searches for those views at runtime. (Tag-based searches are faster than iterating the view hierarchy yourself.) The default value for the tag property is 0.

To search for a tagged view, use the viewWithTag: method of UIView. This method performs a depth-first search of the receiver and its subviews. It does not search superviews or other parts of the view hierarchy. Thus, calling this method from the root view of a hierarchy searches all views in the hierarchy but calling it from a specific subview searches only a subset of views.

Creating and Managing a View Hierarchy

Managing view hierarchies is a crucial part of developing your application’s user interface. The organization of your views influences both the visual appearance of your application and how your application responds to changes and events. For example, the parent-child relationships in the view hierarchy determine which objects might handle a specific touch event. Similarly, parent-child relationships define how each view responds to interface orientation changes.

Figure 3-1 shows an example of how the layering of views creates the desired visual effect for an application. In the case of the Clock application, the view hierarchy is composed of a mixture of views derived from different sources. The tab bar and navigation views are special view hierarchies provided by the tab bar and navigation controller objects to manage portions of the overall user interface. Everything between those bars belongs to the custom view hierarchy that the Clock application provides.

Figure 3-1 Layered views in the Clock application时钟应用程序中的分层视图

There are several ways to build view hierarchies in iOS applications, including graphically in Interface Builder and programmatically in your code. The following sections show you how to assemble your view hierarchies and, having done that, how to find views in the hierarchy and convert between different view coordinate systems.

Adding and Removing Subviews

Interface Builder is the most convenient way to build view hierarchies because you assemble your views graphically, see the relationships between the views, and see exactly how those views will appear at runtime. When using Interface Builder, you save your resulting view hierarchy in a nib file, which you load at runtime as the corresponding views are needed.

If you prefer to create your views programmatically instead, you create and initialize them and then use the following methods to arrange them into hierarchies:

  • To add a subview to a parent, call the addSubview: method of the parent view. This method adds the subview to the end of the parent’s list of subviews.
  • To insert a subview in the middle of the parent’s list of subviews, call any of the insertSubview:... methods of the parent view. Inserting a subview in the middle of the list visually places that view behind any views that come later in the list.
  • To reorder existing subviews inside their parent, call the bringSubviewToFront:, sendSubviewToBack:, or exchangeSubviewAtIndex:withSubviewAtIndex: methods of the parent view. Using these methods is faster than removing the subviews and reinserting them.
  • To remove a subview from its parent, call the removeFromSuperview method of the subview (not the parent view).

When adding a subview to its parent, the subview’s current frame rectangle denotes its initial position inside the parent view. A subview whose frame lies outside of its superview’s visible bounds is not clipped by default. If you want your subview to be clipped to the superview’s bounds, you must explicitly set the clipsToBounds property of the superview to YES.

One place where you might add subviews to a view hierarchy is in the loadView or viewDidLoad methods of a view controller. If you are building your views programmatically, you put your view creation code in the loadView method of your view controller. Whether you create your views programmatically or load them from a nib file, you could include additional view configuration code in the viewDidLoad method.

Listing 3-1 shows the viewDidLoad method of the TransitionsViewController class from the UIKit Catalog (iOS): Creating and Customizing UIKit Controls sample application. The TransitionsViewController class manages the animations associated with transitioning between two views. The application’s initial view hierarchy (consisting of a root view and toolbar) is loaded from a nib file. The code in the viewDidLoad method subsequently creates the container view and image views used to manage the transitions. The purpose of the container view is to simplify the code needed to implement the transition animations between the two image views. The container view has no real content of its own.

Listing 3-1 Adding views to an existing view hierarchy

- (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] autorelease];
[self.view addSubview:self.containerView];
// The container view can represent the images for accessibility.
[self.containerView setIsAccessibilityElement:YES];
[self.containerView setAccessibilityLabel:NSLocalizedString(@"ImagesTitle", @"")];
// 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"];
}

Important: Superviews automatically retain their subviews, so after embedding a subview it is safe to release that subview. In fact, doing so is recommended because it prevents your application from retaining the view one time too many and causing a memory leak later. Just remember that if you remove a subview from its superview and intend to reuse it, you must retain the subview again. The removeFromSuperview method autoreleases a subview before removing it from its superview. If you do not retain the view before the next event loop cycle, the view will be released.

For more information about Cocoa memory management conventions, see Advanced Memory Management Programming Guide.

When you add a subview to another view, UIKit notifies both the parent and child views of the change. If you implement custom views, you can intercept these notifications by overriding one or more of the willMoveToSuperview:, willMoveToWindow:, willRemoveSubview:, didAddSubview:, didMoveToSuperview, or didMoveToWindow methods. You can use these notifications to update any state information related to your view hierarchy or to perform additional tasks.

After creating a view hierarchy, you can navigate it programmatically using the superview and subviews properties of your views. The window property of each view contains the window in which that view is currently displayed (if any). Because the root view in a view hierarchy has no parent, its superview property is set to nil. For views that are currently onscreen, the window object is the root view of the view hierarchy.

Hiding Views

To hide a view visually, you can either set its hidden property to YES or change its alpha property to 0.0. A hidden view does not receive touch events from the system. However, hidden views do participate in autoresizing and other layout operations associated with the view hierarchy. Thus, hiding a view is often a convenient alternative to removing views from your view hierarchy, especially if you plan to show the views again at some point soon.

Important: If you hide a view that is currently the first responder, the view does not automatically resign its first responder status. Events targeted at the first responder are still delivered to the hidden view. To prevent this from happening, you should force your view to resign the first responder status when you hide it. For more information about the responder chain, see Event Handling Guide for iOS.

If you want to animate a view’s transition from visible to hidden (or the reverse), you must do so using the view’s alpha property. The hidden property is not an animatable property, so any changes you make to it take effect immediately.

Locating Views in a View Hierarchy

There are two ways to locate views in a view hierarchy:

  • Store pointers to any relevant views in an appropriate location, such as in the view controller that owns the views.
  • Assign a unique integer to each view’s tag property and use the viewWithTag: method to locate it.

Storing references to relevant views is the most common approach to locating views and makes accessing those views very convenient. If you used Interface Builder to create your views, you can connect objects in your nib file (including the File’s Owner object that represents the managing controller object) to one another using outlets. For views you create programmatically, you can store references to those views in private member variables. Whether you use outlets or private member variables, you are responsible for retaining the views as needed and then releasing them as well. The best way to ensure objects are retained and released properly is to use declared properties.

Tags are a useful way to reduce hard-coded dependencies and support more dynamic and flexible solutions. Rather than storing a pointer to a view, you could locate it using its tag. Tags are also a more persistent way of referring to views. For example, if you wanted to save the list of views that are currently visible in your application, you would write out the tags of each visible view to a file. This is simpler than archiving the actual view objects, especially in situations where you are tracking only which views are currently visible. When your application is subsequently loaded, you would then re-create your views and use the saved list of tags to set the visibility of each view, and thereby return your view hierarchy to its previous state.

Translating, Scaling, and Rotating Views

Every view has an associated affine transform that you can use to translate, scale, or rotate the view’s content. View transforms alter the final rendered appearance of the view and are often used to implement scrolling, animations, or other visual effects.

The transform property of UIView contains a CGAffineTransform structure with the transformations to apply. By default, this property is set to the identity transform, which does not modify the appearance of the view. You can assign a new transform to this property at any time. For example, to rotate a view by 45 degrees, you could use the following code:

// 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;

Applying the transform in the preceding code to a view would rotate that view clockwise about its center point. Figure 3-2 shows how this transformation would look if it were applied to an image view embedded in an application.

Figure 3-2 Rotating a view 45 degreesimg

When applying multiple transformations to a view, the order in which you add those transformations to the CGAffineTransform structure is significant. Rotating the view and then translating it is not the same as translating the view and then rotating it. Even if the amounts of rotation and translation are the same in each case, the sequence of the transformations affects the final results. In addition, any transformations you add are applied to the view relative to its center point. Thus, applying a rotation factor rotates the view around its center point. Scaling a view changes the width and height of the view but does not change its center point.

For more information about creating and using affine transforms, see Transforms in Quartz 2D Programming Guide.

Converting Coordinates in the View Hierarchy

At various times, particularly when handling events, an application may need to convert coordinate values from one frame of reference to another. For example, touch events report the location of each touch in the window’s coordinate system but view objects often need that information in the view’s local coordinate system. The UIView class defines the following methods for converting coordinates to and from the view’s local coordinate system:

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

The convert...:fromView: methods convert coordinates from some other view’s coordinate system to the local coordinate system (bounds rectangle) of the current view. Conversely, the convert...:toView: methods convert coordinates from the current view’s local coordinate system (bounds rectangle) to the coordinate system of the specified view. If you specify nil as the reference view for any of the methods, the conversions are made to and from the coordinate system of the window that contains the view.

In addition to the UIView conversion methods, the UIWindow class also defines several conversion methods. These methods are similar to the UIView versions except that instead of converting to and from a view’s local coordinate system, these methods convert to and from the window’s coordinate system.

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

在旋转视图中转换坐标时,UIKit 会在假设您希望返回的矩形反映源矩形覆盖的屏幕区域的情况下转换矩形。图 3-3显示了在转换过程中旋转如何导致矩形大小发生变化的示例。在图中,外部父视图包含旋转子视图。将子视图坐标系中的矩形转换为父视图坐标系会产生一个物理上更大的矩形。这个较大的矩形实际上outerView是完全包围旋转矩形的边界中的最小矩形。

图 3-3 在旋转视图中转换值在旋转视图中转换值

在运行时调整视图的大小和位置

每当视图的大小发生变化时,其子视图的大小和位置必须相应地改变。的UIView类支持两者的视图在视图层级中的自动和手动布局。使用自动布局,您可以设置每个视图在其父视图调整大小时应遵循的规则,然后完全忘记调整大小操作。使用手动布局,您可以根据需要手动调整视图的大小和位置。

为布局更改做好准备

每当视图中发生以下任何事件时,都可能发生布局更改:

  • 视图边界矩形的大小发生变化。
  • 发生界面方向更改,这通常会触发根视图边界矩形的更改。
  • 与视图层相关联的一组核心动画子层发生变化并需要布局。
  • 您的应用程序通过调用视图的setNeedsLayoutorlayoutIfNeeded方法强制进行布局。
  • 您的应用程序通过调用setNeedsLayout视图的底层图层对象的方法来强制布局。

使用自动调整大小规则自动处理布局更改

当您更改视图的大小时,任何嵌入的子视图的位置和大小通常需要更改以考虑其父视图的新大小。父视图的autoresizesSubviews 属性决定了子视图是否完全调整大小。如果此属性设置为YES,则视图使用autoresizingMask每个子视图的属性来确定如何调整子视图的大小和位置。任何子视图的大小更改都会为其嵌入的子视图触发类似的布局调整。

对于视图层次结构中的每个视图,将该视图的autoresizingMask属性设置为适当的值是处理自动布局更改的重要部分。表 3-2列出了可以应用于给定视图的自动调整大小选项,并描述了它们在布局操作期间的效果。您可以使用 OR 运算符组合常量,或者在将它们分配给autoresizingMask属性之前将它们添加在一起。如果你使用 Interface Builder 来组装你的视图,你可以使用 Autosizing 检查器来设置这些属性。

自动调整蒙版描述
UIViewAutoresizingNone视图不会自动调整大小。(这是默认值。)
UIViewAutoresizingFlexibleHeight当父视图的高度改变时,视图的高度也会改变。如果不包含此常量,则视图的高度不会改变。
UIViewAutoresizingFlexibleWidth当父视图的宽度改变时,视图的宽度也会改变。如果不包含此常量,则视图的宽度不会改变。
UIViewAutoresizingFlexibleLeftMargin视图左边缘和父视图左边缘之间的距离根据需要增长或缩小。如果不包含此常量,则视图的左边缘与父视图的左边缘保持固定距离。
UIViewAutoresizingFlexibleRightMargin视图的右边缘和父视图的右边缘之间的距离根据需要增长或缩小。如果不包含此常量,则视图的右边缘与父视图的右边缘保持固定距离。
UIViewAutoresizingFlexibleBottomMargin视图底部边缘和父视图底部边缘之间的距离根据需要增长或缩小。如果不包含此常量,则视图的底部边缘与父视图的底部边缘保持固定距离。
UIViewAutoresizingFlexibleTopMargin视图的顶部边缘和父视图的顶部边缘之间的距离根据需要增长或缩小。如果不包含此常量,则视图的顶部边缘与父视图的顶部边缘保持固定距离。

图 3-4显示了自动调整大小掩码中的选项如何应用于视图的图形表示。给定常量的存在表明视图的指定方面是灵活的,并且可能会随着父视图边界的变化而变化。没有常量表示视图的布局在该方面是固定的。当你配置一个沿单个轴具有多个灵活属性的视图时,UIKit 会在相应的空间中均匀分布任何大小变化。

图 3-4 查看自动调整大小掩码常量查看自动调整大小掩码常量

配置自动调整大小规则的最简单方法是使用界面生成器的大小检查器中的自动调整大小控件。上图中的灵活宽度和高度常量与 Autosizing 控件图中的宽度和大小指示器具有相同的行为。然而,保证金指标的行为和使用实际上被逆转了。在 Interface Builder 中,边距指示器的存在意味着边距具有固定大小,而没有指示器意味着边距具有灵活的大小。幸运的是,Interface Builder 提供了一个动画来向您展示自动调整大小行为的更改如何影响您的视图。

重要提示: 如果视图的transform属性不包含标识变换,则该视图的框架未定义,其自动调整大小行为的结果也是如此。

在应用了所有受影响视图的自动调整大小规则后,UIKit 返回并让每个视图都有机会对其父视图进行任何必要的手动调整。有关如何手动管理视图布局的更多信息,请参阅手动调整视图布局

手动调整视图的布局

每当视图的大小发生变化时,UIKit 会应用该视图子视图的自动调整大小行为,然后调用该视图的layoutSubviews方法以让它进行手动更改。layoutSubviews当自动调整大小行为本身没有产生您想要的结果时,您可以在自定义视图中实现该方法。您对该方法的实现可以执行以下任一操作:

  • 调整任何直接子视图的大小和位置。
  • 添加或删除子视图或核心动画层。
  • 通过调用其setNeedsDisplaysetNeedsDisplayInRect:方法强制重绘子视图。

应用程序经常手动布置子视图的一个地方是在实现大的可滚动区域时。因为为其可滚动内容使用单个大视图是不切实际的,应用程序通常实现一个包含许多较小平铺视图的根视图。每个磁贴代表可滚动内容的一部分。当滚动事件发生时,根视图调用其setNeedsLayout方法来启动布局更改。layoutSubviews然后它的方法根据发生的滚动量重新定位平铺视图。当瓷砖滚出视图的可见区域时,该layoutSubviews方法将瓷砖移动到传入边缘,在此过程中替换它们的内容。

在编写布局代码时,请务必通过以下方式测试您的代码:

  • 更改视图的方向以确保布局在所有支持的界面方向上看起来都是正确的。
  • 确保您的代码对状态栏高度的变化做出适当的响应。当通话处于活动状态时,状态栏的高度会增大,当用户结束通话时,状态栏的尺寸会减小。

有关自动调整大小行为如何影响视图的大小和位置的信息,请参阅使用自动调整大小规则自动处理布局更改。有关如何实现平铺的示例,请参阅*ScrollViewSuite*示例。

在运行时修改视图

当应用程序接收来自用户的输入时,它们会根据该输入调整其用户界面。应用程序可能会通过重新排列视图、更改其大小或位置、隐藏或显示它们或加载全新的视图集来修改其视图。在 iOS 应用程序中,您可以通过多种方式执行这些类型的操作:

  • 在视图控制器中:
    • 视图控制器必须在显示它们之前创建它的视图。它可以从 nib 文件加载视图或以编程方式创建它们。当不再需要这些视图时,它会处理它们。
    • 当设备改变方向时,视图控制器可能会调整视图的大小和位置以匹配。作为对新方向进行调整的一部分,它可能会隐藏一些视图并显示其他视图。
    • 当视图控制器管理可编辑内容时,它可能会在进入和离开编辑模式时调整其视图层次结构。例如,它可能会添加额外的按钮和其他控件以方便编辑其内容的各个方面。这可能还需要调整任何现有视图的大小以容纳额外的控件。
  • 在动画块中:
    • 当您想要在用户界面中的不同视图集之间转换时,您可以在动画块内隐藏一些视图并显示其他视图。
    • 在实现特殊效果时,您可能会使用动画块来修改视图的各种属性。例如,要对视图大小的更改进行动画处理,您可以更改其框架矩形的大小。
  • 其他方法:
    • 当发生触摸事件或手势时,您的界面可能会通过加载一组新的视图或更改当前的一组视图来做出响应。有关处理事件的信息,请参阅iOS 事件处理指南
    • 当用户与滚动视图交互时,大的可滚动区域可能会隐藏和显示平铺子视图。有关支持可滚动内容的更多信息,请参阅*适用于 iOS 的滚动视图编程指南*。
    • 当键盘出现时,您可以重新定位视图或调整视图大小,使它们不会位于键盘下方。有关如何与键盘交互的信息,请参阅*iOS 文本编程指南*。

视图控制器是启动视图更改的常见位置。因为视图控制器管理与显示内容相关联的视图层次结构,它最终负责发生在这些视图上的所有事情。在加载视图或处理方向更改时,视图控制器可以添加新视图、隐藏或替换现有视图,并进行任意数量的更改以使视图准备好显示。如果您实现了对编辑视图内容的支持,则 中的setEditing:animated:方法UIViewController为您提供了一个将视图转换为可编辑版本或从可编辑版本转换的地方。

动画块是启动与视图相关的更改的另一个常见位置。UIView类中内置的动画支持可以轻松地为视图属性的更改设置动画。您还可以使用transitionWithView:duration:options:animations:completion:transitionFromView:toView:duration:options:completion:方法将整个视图集替换为新视图。

与核心动画层交互

每个视图对象都有一个专用的核心动画层,用于管理视图内容在屏幕上的呈现和动画。尽管您可以对视图对象进行很多操作,但您也可以根据需要直接使用相应的图层对象。视图的图层对象存储在视图的layer属性中。

更改与视图关联的图层类

创建视图后,无法更改与视图关联的图层类型。因此,每个视图都使用layerClass 类方法来指定其图层对象的类。此方法的默认实现返回CALayer类,更改此值的唯一方法是创建子类、覆盖该方法并返回不同的值。您可以更改此值以使用不同类型的图层。例如,如果您的视图使用平铺来显示大的可滚动区域,您可能希望使用CATiledLayer该类来支持您的视图。

layerClass方法的实现应该简单地创建所需的Class对象并返回它。例如,使用平铺的视图将具有此方法的以下实现:

+ (Class)layerClass
{
    返回 [CATiledLayer 类];
}

每个视图layerClass在其初始化过程的早期调用其方法,并使用返回的类来创建其图层对象。此外,视图总是将自己分配为其层对象的委托。此时,视图拥有它的图层,视图和图层之间的关系不能改变。您也不得分配与任何其他图层对象的委托相同的视图。更改视图的所有权或委托关系将导致应用程序出现绘制问题和潜在崩溃.

在视图中嵌入图层对象

如果您更喜欢主要使用图层对象而不是视图,您可以根据需要将自定义图层对象合并到您的视图层次结构中。自定义图层对象是CALayer不属于视图的任何实例。您通常以编程方式创建自定义图层并使用核心动画例程合并它们。自定义层不接收事件或参与响应者链,但会根据核心动画规则绘制自己并响应其父视图或层中的大小变化。

- (void)viewDidLoad {
    [超级viewDidLoad];
 
    // 创建图层。
    CALayer* myLayer = [[CALayer alloc] init];
 
    // 将图层的内容设置为固定图像。并设置
    // 图层的大小以匹配图像大小。
    UIImage layerContents = [[UIImage imageNamed:@"myImage"] 保留];
    CGSize imageSize = layerContents.size;
 
    myLayer.bounds = CGRectMake(0, 0, imageSize.width, imageSize.height);
    myLayer = layerContents.CGImage;
 
    // 将图层添加到视图中。
    CALayer* viewLayer = self.view.layer;
    [viewLayer addSublayer:myLayer];
 
    // 在视图中居中图层。
    CGRect viewBounds = backingView.bounds;
    myLayer.position = CGPointMake(CGRectGetMidX(viewBounds), CGRectGetMidY(viewBounds));
 
    // 释放图层,因为它被视图的图层保留
    [myLayer 发布];
}

实现自定义视图的清单

自定义视图的工作是呈现内容并管理与该内容的交互。然而,自定义视图的成功实现不仅仅涉及绘制和处理事件。以下清单包括在实现自定义视图时您可以覆盖的更重要的方法(以及您可以提供的行为):

  • 为您的视图定义适当的初始化方法:
    • 对于您计划以编程方式创建的视图,覆盖该initWithFrame:方法或定义自定义初始化方法。
    • 对于您计划从 nib 文件加载的视图,覆盖该initWithCoder:方法。使用此方法初始化您的视图并将其置于已知状态。
  • 实现一个dealloc方法来处理任何自定义数据的清理。
  • 要处理任何自定义绘图,请覆盖该drawRect:方法并在那里进行绘图。
  • 设置autoresizingMask视图的属性以定义其自动调整大小行为。
  • 如果您的视图类管理一个或多个完整的子视图,请执行以下操作:
    • 在视图的初始化序列期间创建这些子视图。
    • autoresizingMask在创建时设置每个子视图的属性。
    • 如果您的子视图需要自定义布局,请覆盖该layoutSubviews方法并在那里实现您的布局代码。
  • 要处理基于触摸的事件,请执行以下操作:
    • 使用该addGestureRecognizer:方法将任何合适的手势识别器附加到视图。
    • 对于那些你想自己处理触摸的情况下,覆盖touchesBegan:withEvent:touchesMoved:withEvent:touchesEnded:withEvent:,和touchesCancelled:withEvent:方法。(请记住touchesCancelled:withEvent:,无论您覆盖了哪些其他与触摸相关的方法,您都应该始终覆盖该方法。)

初始化您的自定义视图

您定义的每个新视图对象都应该包含一个自定义initWithFrame: 初始化方法。此方法负责在创建时初始化类并将您的视图对象置于已知状态。在您的代码中以编程方式创建视图实例时,您可以使用此方法。

清单 3-3显示了标准initWithFrame:方法的骨架实现。该方法首先调用该方法的继承实现,然后在返回初始化对象之前初始化类的实例变量和状态信息。调用继承的实现传统上是先执行的,这样如果出现问题,您可以中止自己的初始化代码并返回nil

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

实现你的绘图代码

对于需要进行自定义绘图的视图,您需要覆盖该drawRect:方法并在那里进行绘图。仅建议将自定义绘图作为最后的手段。一般来说,如果您可以使用其他视图来呈现您的内容,那是首选。

您的drawRect:方法的实现应该只做一件事:绘制您的内容。此方法不是更新应用程序数据结构或执行任何与绘图无关的任务的地方。它应该配置绘图环境,绘制您的内容,并尽快退出。如果您的drawRect:方法可能会被频繁调用,您应该尽一切努力优化您的绘图代码,并在每次调用该方法时尽可能少地绘制。

在调用视图的drawRect:方法之前,UIKit 会为视图配置基本的绘图环境。具体来说,它创建一个图形上下文并调整坐标系和裁剪区域以匹配坐标系和视图的可见边界。因此,在drawRect:调用您的方法时,您可以开始使用本机绘图技术(例如 UIKit 和 Core Graphics)绘制内容。您可以使用该UIGraphicsGetCurrentContext函数获取指向当前图形上下文的指针。

重要提示: 当前图形上下文仅在对视图drawRect:方法的一次调用期间有效。UIKit 可能会为此方法的每次后续调用创建不同的图形上下文,因此您不应尝试缓存该对象并在以后使用它。

- (void)drawRect:(CGRect)rect {
    CGContextRef 上下文 = UIGraphicsGetCurrentContext();
    CGRect myFrame = self.bounds;
 
    // 将线宽设置为 10 并将矩形插入
    // 各边 5 个像素以补偿较宽的线。
    CGContextSetLineWidth(context, 10);
    CGRectInset(myFrame, 5, 5);
 
    [[UIColor redColor] 设置];
    UIRectFrame(myFrame);
}

响应事件

视图对象是响应者对象——UIResponder类的实例——因此能够接收触摸事件。当触摸事件发生时,窗口将相应的事件对象分派给发生触摸的视图。如果你的视图对一个事件不感兴趣,它可以忽略它或将它传递到响应者链上,由不同的对象处理。

除了直接处理触摸事件之外,视图还可以使用手势识别器来检测点击、滑动、捏合和其他类型的常见触摸相关手势。手势识别器在跟踪触摸事件并确保它们遵循正确的标准以将其限定为目标手势方面进行了艰苦的工作。您的应用程序不必跟踪触摸事件,您可以创建手势识别器,为其分配适当的目标对象和操作方法,然后使用该addGestureRecognizer:方法将其安装在您的视图上。然后,手势识别器会在相应手势发生时调用您的操作方法。

如果你更喜欢直接处理触摸事件,你可以为你的视图实现以下方法,在iOS 事件处理指南中有更详细的描述:

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

视图的默认行为是一次仅响应一次触摸。如果用户放下第二根手指,系统会忽略触摸事件并且不会将其报告给您的视图。如果您打算从视图的事件处理程序方法中跟踪多指手势,则需要通过将视图的multipleTouchEnabled 属性设置为 来启用多点触控事件YES

一些视图,例如标签和图像,最初完全禁用事件处理。您可以通过更改视图的userInteractionEnabled属性值来控制视图是否能够接收触摸事件。您可以暂时将此属性设置为 ,NO以防止用户在长时间操作挂起时操作视图的内容。为了防止事件到达您的任何视图,您还可以使用对象的beginIgnoringInteractionEventsendIgnoringInteractionEvents方法UIApplication。这些方法会影响整个应用程序的事件传递,而不仅仅是单个视图。

注意:UIView通常在动画进行时禁用触摸事件 的动画方法。您可以通过适当地配置动画来覆盖此行为。

下一个以前的

动画

什么可以动画?

UIKit 和 Core Animation 都提供对动画的支持,但每种技术提供的支持程度各不相同。在 UIKit 中,动画是使用UIView对象来执行的。视图支持涵盖许多常见任务的一组基本动画。例如,您可以对视图属性的更改进行动画处理,或使用过渡动画将一组视图替换为另一组视图。

财产你可以做出的改变
frame修改此属性以更改视图相对于其父视图坐标系的大小和位置。(如果transform属性不包含恒等变换,请改为修改boundscenter属性。)
bounds修改此属性以更改视图的大小。
center修改此属性以更改视图相对于其父视图坐标系的位置。
transform修改此属性以相对于其中心点缩放、旋转或平移视图。使用此属性的转换始终在 2D 空间中执行。(要执行 3D 转换,您必须使用 Core Animation 为视图的图层对象设置动画。)
alpha修改此属性以逐渐更改视图的透明度。
backgroundColor修改此属性以更改视图的背景颜色。
contentStretch修改此属性以更改视图内容被拉伸以填充可用空间的方式。

表 4-1列出了类的可动画属性(具有内置动画支持的UIView属性)。可动画化并不意味着动画会自动发生。更改这些属性的值通常只会立即更新属性(和视图)而无需动画。要为此类更改设置动画,您必须从动画块内部更改属性值,这在 View中的动画属性更改中进行了描述。

动画视图转换是您对视图层次结构进行更改的一种方式,超出了视图控制器提供的更改范围。尽管您应该使用视图控制器来管理简洁的视图层次结构,但有时您可能想要替换全部或部分视图层次结构。在这些情况下,您可以使用基于视图的过渡来动画添加和删除视图。

在你想要执行更复杂的动画,或者UIView类不支持的动画的地方,你可以使用 Core Animation 和视图的底层来创建动画。由于视图和图层对象错综复杂地链接在一起,因此对视图图层的更改会影响视图本身。使用核心动画,您可以为视图层的以下类型的更改设置动画:

  • 图层的大小和位置
  • 执行转换时使用的中心点
  • 在 3D 空间中转换到图层或其子图层
  • 从层层次结构中添加或删除层
  • 图层相对于其他同级图层的 Z 顺序
  • 图层的阴影
  • 图层的边框(包括图层的角是否圆角)
  • 在调整大小操作期间拉伸的图层部分
  • 图层的不透明度
  • 位于图层边界之外的子图层的裁剪行为
  • 图层的当前内容
  • 图层的光栅化行为

动画

动画在用户界面的不同状态之间提供流畅的视觉转换。在 iOS 中,动画被广泛用于重新定位视图、更改其大小、从视图层次结构中删除它们以及隐藏它们。您可以使用动画向用户传达反馈或实现有趣的视觉效果。

在 iOS 中,创建复杂的动画不需要您编写任何绘图代码。本章中描述的所有动画技术都使用 Core Animation 提供的内置支持。您所要做的就是触发动画并让 Core Animation 处理单个帧的渲染。这使得只需几行代码即可轻松创建复杂的动画。

什么可以动画?

UIKit 和 Core Animation 都提供对动画的支持,但每种技术提供的支持程度各不相同。在 UIKit 中,动画是使用UIView对象来执行的。视图支持涵盖许多常见任务的一组基本动画。例如,您可以对视图属性的更改进行动画处理,或使用过渡动画将一组视图替换为另一组视图。

表 4-1列出了类的可动画属性(具有内置动画支持的UIView属性)。可动画化并不意味着动画会自动发生。更改这些属性的值通常只会立即更新属性(和视图)而无需动画。要为此类更改设置动画,您必须从动画块内部更改属性值,这在 View中的动画属性更改中进行了描述。

财产你可以做出的改变
frame修改此属性以更改视图相对于其父视图坐标系的大小和位置。(如果transform属性不包含恒等变换,请改为修改boundscenter属性。)
bounds修改此属性以更改视图的大小。
center修改此属性以更改视图相对于其父视图坐标系的位置。
transform修改此属性以相对于其中心点缩放、旋转或平移视图。使用此属性的转换始终在 2D 空间中执行。(要执行 3D 转换,您必须使用 Core Animation 为视图的图层对象设置动画。)
alpha修改此属性以逐渐更改视图的透明度。
backgroundColor修改此属性以更改视图的背景颜色。
contentStretch修改此属性以更改视图内容被拉伸以填充可用空间的方式。

动画视图转换是您对视图层次结构进行更改的一种方式,超出了视图控制器提供的更改范围。尽管您应该使用视图控制器来管理简洁的视图层次结构,但有时您可能想要替换全部或部分视图层次结构。在这些情况下,您可以使用基于视图的过渡来动画添加和删除视图。

在你想要执行更复杂的动画,或者UIView类不支持的动画的地方,你可以使用 Core Animation 和视图的底层来创建动画。由于视图和图层对象错综复杂地链接在一起,因此对视图图层的更改会影响视图本身。使用核心动画,您可以为视图层的以下类型的更改设置动画:

  • 图层的大小和位置
  • 执行转换时使用的中心点
  • 在 3D 空间中转换到图层或其子图层
  • 从层层次结构中添加或删除层
  • 图层相对于其他同级图层的 Z 顺序
  • 图层的阴影
  • 图层的边框(包括图层的角是否圆角)
  • 在调整大小操作期间拉伸的图层部分
  • 图层的不透明度
  • 位于图层边界之外的子图层的裁剪行为
  • 图层的当前内容
  • 图层的光栅化行为

注意: 如果您的视图承载自定义图层对象(即没有关联视图的图层对象),您必须使用 Core Animation 为它们的任何更改设置动画。

动画视图中的属性更改

为了对UIView类的属性进行动画更改,您必须将这些更改包装在动画块中。术语动画块在一般意义上用于指代指定可动画更改的任何代码。在 iOS 4 及更高版本中,您可以使用块对象创建动画块。在早期版本的 iOS 中,您可以使用类的特殊类方法来标记动画块的开始和结束UIView。这两种技术都支持相同的配置选项,并提供对动画执行的相同控制量。然而,只要有可能,基于块的方法是首选。

清单 4-1 执行一个简单的基于块的动画

[UIView animateWithDuration:1.0 动画:^{
        firstView.alpha = 0.0;
        secondView.alpha = 1.0;
}];

前面示例中的动画仅使用缓入缓出动画曲线运行一次。如果要更改默认动画参数,则必须使用该animateWithDuration:delay:options:animations:completion:方法来执行动画。此方法可让您自定义以下动画参数:

  • 在开始动画之前使用的延迟
  • 动画期间使用的时序曲线类型
  • 动画应该重复的次数
  • 动画是否应该在到达结束时自动反转
  • 是否在动画进行时将触摸事件传递给视图
  • 动画是否应该中断任何正在进行的动画或等到这些动画完成后再开始

animateWithDuration:animations:completion:animateWithDuration:delay:options:animations:completion:方法都支持的另一件事是指定完成处理程序块的能力。您可以使用完成处理程序向您的应用程序发出特定动画已完成的信号。完成处理程序也是将单独的动画链接在一起的方式。

重要提示: 在涉及该属性的动画已经在进行中时更改该属性的值不会停止当前动画。相反,当前动画会继续并按照您刚刚分配给属性的新值进行动画处理。

配置开始/提交动画的参数

要为开始/提交动画块配置动画参数,您可以使用几种UIView 类方法中的任何一种。表 4-2列出了这些方法并描述了如何使用它们来配置动画。这些方法中的大多数只能从开始/提交动画块内部调用,但有些方法也可以与基于块的动画一起使用。如果不从动画块调用这些方法之一,则使用相应属性的默认值。有关与每个方法关联的默认值的更多信息,请参阅*UIView 类参考中*的方法说明。

方法用法
setAnimationStartDate:``setAnimationDelay:使用这些方法中的任何一种来指定执行应何时开始执行。如果指定的开始日期在过去(或延迟为 0),则动画尽快开始。
setAnimationDuration:使用此方法设置执行动画的时间段。
setAnimationCurve:使用此方法设置动画的时序曲线。这控制动画是线性执行还是在特定时间改变速度。
setAnimationRepeatCount:``setAnimationRepeatAutoreverses:使用这些方法可以设置动画重复的次数以及动画在每个完整循环结束时是否反向运行。有关使用这些方法的更多信息,请参阅实现反转自身的动画
setAnimationDelegate:``setAnimationWillStartSelector:``setAnimationDidStopSelector:使用这些方法在动画之前或之后立即执行代码。有关使用委托的更多信息,请参阅配置动画委托
setAnimationBeginsFromCurrentState:使用此方法立即停止所有以前的动画并从停止点开始新的动画。如果你传递NO给这个方法,而不是YES,新的动画直到之前的动画停止后才会开始执行。

嵌套动画块

您可以通过嵌套其他动画块为动画块的各个部分分配不同的计时和配置选项。顾名思义,嵌套动画块是在现有动画块内创建的新动画块。嵌套动画与任何父动画同时启动,但(在大多数情况下)使用自己的配置选项运行。默认情况下,嵌套动画确实会继承父级的持续时间和动画曲线,但即使这些选项也可以根据需要覆盖。

实现反转自己的动画

结合重复计数创建可逆动画时,请考虑为重复计数指定一个非整数值。对于自动反转动画,动画的每个完整循环都涉及从原始值到新值再返回的动画。如果您希望动画以新值结束,添加0.5到重复计数会导致动画完成以新值结束所需的额外半个周期。如果不包括这半步,您的动画将动画到原始值,然后快速捕捉到新值,这可能不是您想要的视觉效果。

在视图之间创建动画过渡

视图转换可帮助您隐藏与在视图层次结构中添加、删除、隐藏或显示视图相关的突然更改。您可以使用视图转换来实现以下类型的更改:

  • **更改现有视图的可见子视图。**当您想要对现有视图进行相对较小的更改时,通常会选择此选项。
  • **用不同的视图替换视图层次结构中的一个视图。**当您想要替换跨越整个或大部分屏幕的视图层次结构时,您通常会选择此选项。

Changing the Subviews of a View

Changing the subviews of a view allows you to make moderate changes to the view. For example, you might add or remove subviews to toggle the superview between two different states. By the time the animations finish, the same view is displayed but its contents are now different.

In iOS 4 and later, you use the transitionWithView:duration:options:animations:completion: method to initiate a transition animation for a view. In the animations block passed to this method, the only changes that are normally animated are those associated with showing, hiding, adding, or removing subviews. Limiting animations to this set allows the view to create a snapshot image of the before and after versions of the view and animate between the two images, which is more efficient. However, if you need to animate other changes, you can include the UIViewAnimationOptionAllowAnimatedContent option when calling the method. Including that option prevents the view from creating snapshots and animates all changes directly.

Listing 4-6 is an example of how to use a transition animation to make it seem as if a new text entry page has been added. In this example, the main view contains two embedded text views. The text views are configured identically, but one is always visible while the other is always hidden. When the user taps the button to create a new page, this method toggles the visibility of the two views, resulting in a new empty page with an empty text view ready to accept text. After the transition is complete, the view saves the text from the old page using a private method and resets the now hidden text view so that it can be reused later. The view then arranges its pointers so that it can be ready to do the same thing if the user requests yet another new page.

- (IBAction)displayNewPage:(id)sender
{
    [UIView transitionWithView:self.view
        duration:1.0
        options:UIViewAnimationOptionTransitionCurlUp
        animations:^{
            currentTextView.hidden = YES;
            swapTextView.hidden = NO;
        }
        completion:^(BOOL finished){
            // Save the old text and then swap the views.
            [self saveNotes:temp];
 
            UIView*    temp = currentTextView;
            currentTextView = swapTextView;
            swapTextView = temp;
        }];
}

Replacing a View with a Different View

Listing 4-8 Toggling between two views in a view controller

- (IBAction)toggleMainViews:(id)sender {
    [UIView transitionFromView:(displayingPrimary ? primaryView : secondaryView)
        toView:(displayingPrimary ? secondaryView : primaryView)
        duration:1.0
        options:(displayingPrimary ? UIViewAnimationOptionTransitionFlipFromRight :
                    UIViewAnimationOptionTransitionFlipFromLeft)
        completion:^(BOOL finished) {
            if (finished) {
                displayingPrimary = !displayingPrimary;
            }
    }];
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值