Esta miniserie de dos partes le enseñará cómo crear un impresionante efecto de plegado de página con Core Animation. En esta entrega, primero aprenderá cómo crear una vista de cuaderno de bocetos y luego cómo aplicar una animación de plegado de columna básica en esa vista. Sigue leyendo!
Trabajando con el Vista
La clase es fundamental para el desarrollo de SDK de iOS. Las vistas tienen tanto un aspecto visual (es decir, lo que se ve en la pantalla) como, por lo general, un aspecto de control (interacción del usuario mediante toques y gestos). El aspecto visual es manejado por una clase que pertenece al Core Animation Framework, llamado CALayer
(que a su vez se implementa a través de OpenGL, solo que no nos importa bajar a ese nivel de abstracción aquí). Los objetos de capa son instancias de la CALayer
clase o una de sus subclases. Sin profundizar demasiado en la teoría (¡esto es, después de todo, se supone que es un tutorial práctico!), Debemos tener en cuenta los siguientes puntos:
CALayer
El contenido de 's consiste en un mapa de bits. Si bien la clase base es bastante útil como es, también tiene varias subclases importantes en Core Animation. En particular, hay CAShapeLayer
Lo que nos permite representar formas mediante vectores..Entonces, ¿qué podemos lograr con capas que no podemos hacer fácilmente con vistas directamente? 3D, por ejemplo, que es en lo que nos centraremos aquí.. CALayer
Las capacidades de los usuarios ponen a su alcance efectos 3D y animaciones bastante sofisticados, sin tener que descender al nivel de OpenGL. Este es el objetivo detrás de este tutorial: demostrar un efecto 3D interesante que ofrece una pequeña muestra de lo que podemos lograr con CALayer
s.
Tenemos una aplicación de dibujo que le permite a un usuario dibujar en la pantalla con su dedo (para lo cual reutilizaré el código de un tutorial anterior mío). Si el usuario realiza un gesto de pellizco en el lienzo de dibujo, se dobla a lo largo de una línea vertical que se extiende a lo largo del centro del lienzo (como la columna vertebral de un libro). El libro doblado incluso proyecta una sombra en el fondo..
La parte de dibujo de la aplicación que estoy tomando prestada no es realmente importante aquí, y podríamos usar cualquier imagen para demostrar el efecto de plegado. Sin embargo, el efecto lo convierte en una metáfora visual muy agradable en el contexto de una aplicación de dibujo donde la acción de pellizco expone un libro con varias hojas (que contienen nuestros dibujos anteriores) que podemos explorar. Esta metáfora se ve notablemente en la aplicación Paper. Si bien para los propósitos del tutorial, nuestra implementación será más sencilla y menos sofisticada, pero no está demasiado lejos ... y, por supuesto, puede tomar lo que aprendió en este tutorial y hacerlo aún mejor.!
Recuerde que en el sistema de coordenadas de iOS el origen se encuentra en la esquina superior izquierda de la pantalla, con el eje x aumentando hacia la derecha y el eje y hacia abajo. El marco de una vista describe su rectángulo en su sistema de coordenadas de supervisión. Las capas también pueden ser consultadas por su marco, pero hay otra forma (preferida) de describir la ubicación y el tamaño de una capa. Los motivaremos con un ejemplo simple: imagine dos capas, A y B, como pedazos de papel rectangulares. Le gustaría que la capa B sea una subcapa de A, así que arregla B encima de A con un alfiler, manteniendo los lados rectos en paralelo. El pin pasa a través de dos puntos, uno en A y uno en B. Saber la ubicación de estos dos puntos nos da una manera efectiva de describir la posición de B con respecto a A. Etiquetaremos el punto que el pin perfora en A el " Punto de anclaje "y el punto en posición B". Eche un vistazo a la siguiente figura, para la cual haremos los cálculos:
La figura parece que está pasando mucho, pero no se preocupe, la examinaremos poco a poco:
El cálculo es bastante sencillo, así que asegúrese de que lo comprende bien. Le dará una buena idea de la relación entre la posición, el punto de anclaje y el marco..
El punto de anclaje es bastante significativo porque cuando realizamos transformaciones 3D en la capa, estas transformaciones se realizan con respecto al punto de anclaje. Hablaremos de eso a continuación (y luego verás un código, ¡lo prometo!).
Estoy seguro de que está familiarizado con el concepto de transformaciones, como escalado, traslación y rotación. En la aplicación Fotos en iOS 6, si pellizcas con dos dedos para acercar o alejar una foto de tu álbum, estás realizando una transformación de escala. Si haces un movimiento de giro con tus dos dedos, la foto gira, y si la arrastras manteniendo los lados paralelos, eso es traducción. Animación básica y CALayer
triunfo Vista
permitiéndole realizar transformaciones en 3D en lugar de solo 2D. Por supuesto, en 2013 nuestras pantallas iDevice aún son 2D, por lo que las transformaciones 3D emplean algunos trucos geométricos para engañar a nuestros ojos para que interpreten una imagen plana como un objeto 3D (el proceso no es diferente al de representar un objeto 3D en un dibujo de líneas hecho con un lápiz, en serio). Para lidiar con la tercera dimensión, necesitamos usar un eje z que imaginemos perforando la pantalla de nuestro dispositivo y perpendicular a ella.
El punto de anclaje es importante porque el resultado preciso de la misma transformación aplicada generalmente diferirá dependiendo de él. Esto es especialmente importante, y se entiende más fácilmente, con una transformación de rotación a través del mismo ángulo aplicado con respecto a dos puntos de anclaje diferentes en el rectángulo en la figura de abajo (el punto rojo y el punto azul). Tenga en cuenta que la rotación se encuentra en el plano de la imagen (o alrededor del eje z, si prefiere pensar de esa manera).
Entonces, ¿cómo implementamos el efecto de plegado que buscamos? Mientras que las capas son realmente geniales, ¡no puedes doblarlas en el medio! La solución es, como estoy seguro de que has descubierto, usar dos capas, una para cada página en cada lado del pliegue. Basándonos en lo que discutimos anteriormente, vamos a trabajar de antemano las propiedades geométricas de estas dos capas:
1.0, 0.5
y para la segunda capa es 0.0, 0.5
, en sus respectivos espacios coordinados. Asegúrate de confirmar eso de la figura antes de continuar!ancho / 2, altura / 2
. Recuerde que la propiedad de posición está en coordenadas estándar, no normalizada.ancho / 2, altura
. Ahora sabemos lo suficiente como para escribir un código!
Cree un nuevo proyecto de Xcode con la plantilla "Vaciar aplicación" y llámelo LayerFunTut. Conviértalo en una aplicación para iPad y habilite el conteo automático de referencias (ARC), pero deshabilite las opciones para datos básicos y pruebas unitarias. Guardalo.
En la página Destino> Resumen que aparece, desplácese hasta "Orientaciones de interfaz admitidas" y elija las dos orientaciones horizontales.
Desplácese hacia abajo hasta llegar a "Marcos y bibliotecas vinculadas", haga clic en "+" y agregue el marco central de QuartzCore, que se requiere para la animación central y los CALayers..
Comenzaremos por incorporar nuestra aplicación de dibujo en el proyecto. Crear una nueva clase de Objective-C llamada CanvasView
, convirtiéndolo en una subclase de Vista
. Pegue el siguiente código en CanvasView.h:
// // CanvasView.h // #import@interface CanvasView: UIView @property (nonatomic, strong) UIImage * incrementalImage; @fin
Y luego en CanvasView.m:
// // CanvasView.m // #import "CanvasView.h" @implementation CanvasView UIBezierPath * ruta; Pts CGPoint [5]; uint ctr; - (id) initWithCoder: (NSCoder *) aDecoder if (self = [super initWithCoder: aDecoder]) self.backgroundColor = [UIColor clearColor]; [self setMultipleTouchEnabled: NO]; ruta = [UIBezierPath bezierPath]; [ruta setLineWidth: 6.0]; devuélvete a ti mismo; - (id) initWithFrame: (CGRect) frame self = [super initWithFrame: frame]; if (self) self.backgroundColor = [UIColor clearColor]; [self setMultipleTouchEnabled: NO]; ruta = [UIBezierPath bezierPath]; [ruta setLineWidth: 6.0]; devuélvete a ti mismo; - (void) drawRect: (CGRect) rect [self.incrementalImage drawInRect: rect]; [[UIColor blueColor] setStroke]; [trazo de trayectoria]; - (void) touchesBegan: (NSSet *) toca withEvent: (UIEvent *) event ctr = 0; UITouch * touch = [toca cualquier objeto]; pts [0] = [toque locationInView: self]; - (void) touchesMoved: (NSSet *) toca conEvent: (UIEvent *) evento UITouch * touch = [toca cualquier Objeto]; CGPoint p = [toque locationInView: self]; ctr ++; pts [ctr] = p; if (ctr == 4) pts [3] = CGPointMake ((pts [2] .x + pts [4] .x) /2.0, (pts [2] .y + pts [4] .y) /2.0 ); [ruta moveToPoint: pts [0]]; [ruta addCurveToPoint: pts [3] controlPoint1: pts [1] controlPoint2: pts [2]]; [auto setNeedsDisplay]; pts [0] = pts [3]; pts [1] = pts [4]; ctr = 1; - (void) touchesEnded: (NSSet *) toca withEvent: (UIEvent *) event [self drawBitmap]; [auto setNeedsDisplay]; [ruta removeAllPoints]; ctr = 0; - (void) touchesCancelled: (NSSet *) toca withEvent: (UIEvent *) event [self touchesEnded: toca withEvent: event]; - (void) drawBitmap UIGraphicsBeginImageContextWithOptions (self.bounds.size, NO, 0.0); if (! self.incrementalImage) UIBezierPath * rectpath = [UIBezierPath bezierPathWithRect: self.bounds]; [[UIColor clearColor] setFill]; [rectpath fill]; [self.incrementalImage drawAtPoint: CGPointZero]; [[UIColor blueColor] setStroke]; [trazo de trayectoria]; self.incrementalImage = UIGraphicsGetImageFromCurrentImageContext (); UIGraphicsEndImageContext (); @final
Como se mencionó anteriormente, este es solo el código de otro tutorial que escribí (con algunas modificaciones menores). Asegúrese de verificarlo si no está seguro de cómo funciona el código. Para los propósitos de este tutorial, sin embargo, lo importante es que CanvasView
permite al usuario dibujar trazos suaves en la pantalla. Hemos declarado una propiedad llamada imagen incremental
que almacena una versión de mapa de bits del dibujo del usuario. Esta es la imagen con la que estaremos "doblando". CALayer
s.
Es hora de escribir el código del controlador de vista e implementar las ideas que trabajamos anteriormente. Una cosa que no hemos discutido es cómo conseguimos la imagen dibujada en nuestro CALayer
de modo que la mitad de la imagen se dibuja en la página izquierda y la otra mitad en la página derecha. Por suerte, solo son unas pocas líneas de código, de las que hablaré más adelante..
Crear una nueva clase de Objective-C llamada ViewController
, hazlo una subclase de UIViewController
, y no marque ninguna de las opciones que aparecen.
Pegue el siguiente código en ViewController.m
// // ViewController.m // #import "ViewController.h" #import "CanvasView.h" #import "QuartzCore / QuartzCore.h" #define D2R (x) (x * (M_PI / 180.0)) // macro para convertir los grados a radianes @interface ViewController () @end @implementation ViewController CALayer * leftPage; CALayer * rightPage; UIView * curtainView; - (void) loadView self.view = [[CanvasView alloc] initWithFrame: [[UIScreen mainScreen] applicationFrame]]; - (void) viewDidLoad [super viewDidLoad]; self.view.backgroundColor = [UIColor blackColor]; - (void) viewDidAppear: (BOOL) animated [super viewDidAppear: animated]; self.view.backgroundColor = [UIColor whiteColor]; CGSize size = self.view.bounds.size; leftPage = [capa CALayer]; rightPage = [capa CALayer]; leftPage.anchorPoint = (CGPoint) 1.0, 0.5; rightPage.anchorPoint = (CGPoint) 0.0, 0.5; leftPage.position = (CGPoint) size.width / 2.0, size.height / 2.0; rightPage.position = (CGPoint) size.width / 2.0, size.height / 2.0; leftPage.bounds = (CGRect) 0, 0, size.width / 2.0, size.height; rightPage.bounds = (CGRect) 0, 0, size.width / 2.0, size.height; leftPage.backgroundColor = [UIColor whiteColor] .CGColor; rightPage.backgroundColor = [UIColor whiteColor] .CGColor; leftPage.borderWidth = 2.0; // bordes agregados por ahora, para que podamos distinguir visualmente entre las páginas derecha e izquierda rightPage.borderWidth = 2.0; leftPage.borderColor = [UIColor darkGrayColor] .CGColor; rightPage.borderColor = [UIColor darkGrayColor] .CGColor; //leftPage.transform = makePerspectiveTransform (); // descomentar más tarde //rightPage.transform = makePerspectiveTransform (); // descomentar más tarde curtainView = [[UIView alloc] initWithFrame: self.view.bounds]; curtainView.backgroundColor = [UIColor scrollViewTexturedBackgroundColor]; [curtainView.layer addSublayer: leftPage]; [curtainView.layer addSublayer: rightPage]; UITapGestureRecognizer * foldTap = [[UITapGestureRecognizer alloc] initWithTarget: self action: @selector (fold :)]; [self.view addGestureRecognizer: foldTap]; UITapGestureRecognizer * unfoldTap = [[UITapGestureRecognizer alloc] initWithTarget: self action: @selector (desplegar :)]; unfoldTap.numberOfTouchesRequired = 2; [self.view addGestureRecognizer: unfoldTap]; - (anular) fold: (UITapGestureRecognizer *) gr // dibujando el mapa de bits "incrementalImage" en nuestras capas CGImageRef imgRef = ((CanvasView *) self.view) .incrementalImage.CGImage; leftPage.contents = (__bridge id) imgRef; rightPage.contents = (__bridge id) imgRef; leftPage.contentsRect = CGRectMake (0.0, 0.0, 0.5, 1.0); // este rectángulo representa la mitad izquierda de la imagen rightPage.contentsRect = CGRectMake (0.5, 0.0, 0.5, 1.0); // este rectángulo representa la mitad derecha de la imagen leftPage.transform = CATransform3DScale (leftPage.transform, 0.95, 0.95, 0.95); rightPage.transform = CATransform3DScale (rightPage.transform, 0.95, 0.95, 0.95); leftPage.transform = CATransform3DRotate (leftPage.transform, D2R (7.5), 0.0, 1.0, 0.0); rightPage.transform = CATransform3DRotate (rightPage.transform, D2R (-7.5), 0.0, 1.0, 0.0); [self.view addSubview: curtainView]; - (vacío) se despliega: (UITapGestureRecognizer *) gr leftPage.transform = CATransform3DIdentity; rightPage.transform = CATransform3DIdentity; // leftPage.transform = makePerspectiveTransform (); // descomentar más tarde // rightPage.transform = makePerspectiveTransform (); // descomentar más tarde [curtainView removeFromSuperview]; // UNCOMMENT TARDE: / * CATransform3D makePerspectiveTransform () CATransform3D transform = CATransform3DIdentity; transform.m34 = 1.0 / -2000; transformada de retorno */ @final
Ignorando el código comentado por ahora, puede ver que la configuración de la capa es exactamente como la planeamos anteriormente..
Vamos a discutir este código brevemente:
D2R ()
convertir de radianes a grados. Una observación importante es que las funciones que toman una transformación en su argumento (como CATransform3DScale
y CATransform3DRotate
) "encadenar" una transformación con otra (el valor actual de la propiedad de transformación de capa). Otras funciones, como CATransform3DMakeRotation
, CATransform3DMakeScale
, CATransform3DIdentity
acaba de construir la matriz de transformación adecuada. CATransform3DIdentity
es la "transformación de identidad" que tiene una capa al crear una. Es análogo al número "1" en una multiplicación en el sentido de que la aplicación de una transformación de identidad a una capa deja su transformación sin cambios, al igual que multiplicar un número por uno. Imagen cg
aquí - y también CGColor
antes - en lugar de UIImage
y UIColor
. Esto es porque CALayer
opera en un nivel por debajo de UIKit, y funciona con tipos de datos "opacos" (en términos generales, no preguntes sobre su implementación subyacente) que se definen en el marco de Core Graphics. Clases objetivas-C como UIColor
y UIImage
pueden considerarse como envoltorios orientados a objetos en torno a sus versiones CG más primitivas. Para mayor comodidad, muchos objetos UIKit exponen su tipo de CG subyacente como una propiedad. En el AppDelegate.m archivo, reemplace todo el código con lo siguiente (lo único que hemos agregado es incluir el archivo de encabezado ViewController y hacer un ViewController
Instalar el controlador de vista raíz):
// // AppDelegate.m // #import "AppDelegate.h" #import "ViewController.h" @implementation AppDelegate - (BOOL) application: (UIApplication *) application didFinishLaunchingWithOptions: (NSDictionary *) launchOptions self.window = \ [UIWindow alloc] initWithFrame: [[UIScreen mainScreen] lines]]; self.window.rootViewController = [[ViewController alloc] init]; self.window.backgroundColor = [UIColor whiteColor]; [self.window makeKeyAndVisible]; devuelve SÍ; @final
Cree el proyecto y ejecútelo en el simulador o en su dispositivo. Garabatee un poco en el lienzo y luego toque la pantalla con un solo dedo para activar la acción de reconocimiento de gestos (al tocar con dos dedos, el efecto 3D finaliza y el lienzo de dibujo vuelve a aparecer).
No bastante ¡El efecto que estamos buscando! Que esta pasando?
Primero, observe que las páginas se hacen más pequeñas con cada toque, por lo que el problema no reside en la transformación de escala, solo con la rotación. El problema es que aunque la rotación está ocurriendo en un espacio 3D (matemático), el resultado se proyecta en nuestras pantallas planas de la misma manera que un objeto 3D proyecta su sombra en una pared. Para transmitir profundidad, necesitamos usar algún tipo de señal. La señal más importante es la de la perspectiva: un objeto más cercano a nuestros ojos parece más grande que uno más lejano. Las sombras son otra gran señal, y llegaremos a ellas en breve. Entonces, ¿cómo incorporamos la perspectiva en nuestra transformación??
Hablemos un poco de transformaciones primero. ¿Qué son, en realidad? Hablando matemáticamente, deberías saber que si representamos los puntos de nuestra forma como vectores matemáticos, las transformaciones geométricas, como la escala, la rotación y la traducción, se representan como transformaciones matriciales. Lo que esto significa es que si tomamos una matriz que representa una transformación y la multiplicamos con un vector que representa un punto en nuestra forma, entonces el resultado de la multiplicación (también un vector) representa dónde termina ese punto después de transformarse. No podemos decir mucho más aquí sin sumergirnos en la teoría (de lo que realmente vale la pena aprender, si aún no lo conoces, ¡especialmente si pretendes incorporar efectos 3D geniales en tus aplicaciones!).
¿Qué pasa con el código? Anteriormente, establecemos la geometría de la capa estableciendo su punto de anclaje
, posición
, y límites
. Lo que vemos en la pantalla es la geometría de la capa después de haber sido transformada por su transformar
propiedad. Tenga en cuenta la función de llamadas que se ven como layer.transform = //…
. Ahí es donde estamos configurando la transformación, que internamente es solo un estructura
Representando una matriz 4 x 4 de valores de punto flotante. También tenga en cuenta que las funciones CATransform3DScale
y CATransform3DRotate
tomar la transformación actual de la capa como un parámetro. Eso es porque podemos componer varias transformaciones juntas (lo que significa que multiplicamos sus matrices juntas), con el resultado final como si hubieras realizado estas transformaciones una por una. Tenga en cuenta que solo estamos hablando del resultado final de la transformación, no de cómo la animación Core anima la capa.!
Volviendo al problema de la perspectiva, lo que necesitamos saber es que hay un valor en nuestra matriz de transformación que podemos modificar para obtener el efecto de perspectiva que buscamos. Ese valor es un miembro de la estructura de transformación, llamado m34 (los números indican su posición en la matriz). Para obtener el efecto que deseamos, debemos establecerlo en un número pequeño y negativo.
Descomentar las dos secciones comentadas en el ViewController.m archivo (el CATransform3D makePerspectiveTransform ()
función y las líneas leftPage.transform = makePerspectiveTransform (); rightPage.transform = makePerspectiveTransform ();
y construir de nuevo. Esta vez el efecto 3D se ve más creíble.
También tenga en cuenta que cuando cambiamos la propiedad de transformación de un CALayer
, El trato viene con una animación "gratuita". Esto es lo que queremos aquí, a diferencia de la capa que sufre su transformación de manera abrupta, pero a veces no lo es..
Por supuesto, la perspectiva solo llega tan lejos, cuando nuestro ejemplo se vuelve más sofisticado, ¡también usaremos sombras! También podríamos querer redondear las esquinas de nuestro "libro" y enmascarar nuestras capas de página con un CAShapeLayer
puede ayudar con eso Además, nos gustaría usar un gesto de pellizco para controlar el plegado / desplegado para que se sienta más interactivo. Todo esto será cubierto en la segunda parte de este tutorial mini-series..
Lo invito a experimentar con el código, refiriéndose a la documentación de la API, e intente implementar el efecto deseado de manera independiente (¡incluso podría terminar haciéndolo mejor!).
Diviértete con el tutorial, y gracias por leer.!