Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. iOS SDK

Dibujo suave a mano alzada en iOS

by
Difficulty:IntermediateLength:LongLanguages:

Spanish (Español) translation by steven (you can also view the original English article)

Este tutorial te enseñará cómo implementar un algoritmo de dibujo avanzado para dibujar a mano alzada y sin problemas en dispositivos iOS. Sigue leyendo

Panorama teórico

El tacto es la principal forma en que un usuario interactúa con los dispositivos iOS. Una de las funcionalidades más naturales y obvias que se espera que proporcionen estos dispositivos es permitir al usuario dibujar en la pantalla con el dedo. Hay muchas aplicaciones de dibujo y toma de notas a mano alzada actualmente en la App Store, y muchas compañías incluso les piden a los clientes que firmen un iDevice al realizar compras. ¿Cómo funcionan realmente estas aplicaciones? Paremos y pensemos por un minuto lo que está pasando "bajo el capó".

Cuando un usuario desplaza una vista de tabla, pellizca para ampliar una imagen o dibuja una curva en una aplicación de pintura, la pantalla del dispositivo se está actualizando rápidamente (por ejemplo, 60 veces por segundo) y el ciclo de ejecución de la aplicación está muestreando constantemente la ubicación de (los) dedo(s) del usuario. Durante este proceso, la entrada "analógica" de un arrastre de dedo a través de la pantalla debe convertirse en un conjunto digital de puntos en la pantalla, y este proceso de conversión puede plantear desafíos importantes. En el contexto de nuestra aplicación de pintura, tenemos un problema de "ajuste de datos" en nuestras manos. A medida que el usuario escribe alegremente en el dispositivo, el programador esencialmente debe interpolar la información analógica faltante ("conectar los puntos") que se ha perdido entre los puntos de contacto muestreados que iOS nos informó. Además, esta interpolación debe ocurrir de tal manera que el resultado sea un trazo que parezca continuo, natural y suave para el usuario final, como si hubiera estado dibujando con un bolígrafo en un bloc de notas hecho con papel.

El propósito de este tutorial es mostrar cómo se puede implementar el dibujo a mano alzada, comenzando desde un algoritmo básico que realiza la interpolación en línea recta y avanzando a un algoritmo más sofisticado que se acerca a la calidad proporcionada por aplicaciones conocidas como Penultimate. Como si no fuera lo suficientemente difícil crear un algoritmo que funcione, también debemos asegurarnos de que el algoritmo funcione bien. Como veremos, una implementación de dibujo ingenua puede llevar a una aplicación con problemas de rendimiento significativos que harán que el dibujo sea engorroso y, a la larga, inutilizable.


Empezando

Supongo que no eres totalmente nuevo en el desarrollo de iOS, así que repasé los pasos de crear un nuevo proyecto, agregar archivos al proyecto, etc. De todos modos, espero que no haya nada demasiado difícil aquí, pero por si acaso, el El código completo del proyecto está disponible para que lo descargues y juegues con él.

Inicia un nuevo proyecto de Xcode iPad basado en la plantilla "Aplicación de vista única" y asígnale el nombre "FreehandDrawingTut". Asegúrate de habilitar el conteo automático de referencias (ARC), pero anula la selección de guiones gráficos y pruebas unitarias. Puedes hacer que este proyecto sea un iPhone o una aplicación universal, dependiendo de los dispositivos que tengas disponibles para probar.

New Project

A continuación, selecciona el proyecto "FreeHandDrawingTut" en Xcode Navigator y asegúrate de que solo se admita la orientación vertical:

Only Support Portrait

Si vas a implementar en iOS 5.x o una versión anterior, puedes cambiar el soporte de orientación de esta manera:

Estoy haciendo esto para mantener las cosas simples y poder enfocarnos en el problema principal que nos ocupa.

Quiero desarrollar nuestro código de forma iterativa, mejorándolo de forma incremental, como lo harías de manera realista si empezaras desde cero, en lugar de dejarte la versión final de una sola vez. Espero que este enfoque te permita manejar mejor los diferentes problemas involucrados. Teniendo esto en cuenta, y para evitar tener que eliminar, modificar y agregar el código en el mismo archivo repetidamente, lo que puede causar problemas y errores, seguiré el siguiente enfoque:

  • Para cada iteración, crearemos una nueva subclase UIView. Publicaré todo el código necesario para que puedas simplemente copiar y pegar en el archivo .m de la nueva subclase UIView que crees. No habrá una interfaz pública para ver la funcionalidad de la subclase, lo que significa que no necesitarás tocar el archivo .h.
  • Para probar cada nueva versión, tendremos que asignar la subclase UIView que creamos para que sea la vista que ocupa actualmente la pantalla. Te mostraré cómo hacerlo con Interface Builder la primera vez, siguiendo los pasos detalladamente, y luego te recordaré este paso cada vez que codifiquemos una nueva versión.

Primer intento de dibujar

En Xcode, selecciona Archivo > Nuevo > Archivo..., elige la clase Objective-C como plantilla, y en la siguiente pantalla asigna un nombre al archivo LinearInterpView y conviértelo en una subclase de UIView. Guárdalo. El nombre "LinearInterp" es la abreviatura de "interpolación lineal" aquí. Por el bien del tutorial, nombraré cada subclase UIView que creamos para enfatizar algún concepto o enfoque introducido dentro del código de clase.

Como mencioné anteriormente, puedes dejar el archivo de encabezado tal como está. Elimina todo el código presente en el archivo LinearInterpView.m y reemplázalo con lo siguiente:

En este código, trabajamos directamente con los eventos táctiles que la aplicación nos informa cada vez que tenemos una secuencia táctil. es decir, el usuario coloca un dedo en la vista on-screen, lo mueve y finalmente lo levanta de la pantalla. Para cada evento en esta secuencia, la aplicación nos envía un mensaje correspondiente (en terminología de iOS, los mensajes se envían al "primer respondiente"; puedes consultar la documentación para obtener más información).

Para lidiar con estos mensajes, implementamos los métodos -touchesBegan:WithEvent: y company, que se declaran en la clase UIResponder de la que UIView hereda. Podemos escribir código para manejar los eventos táctiles de la manera que queramos. En nuestra aplicación, queremos consultar la ubicación en pantalla de los toques, hacer un procesamiento y luego dibujar líneas en la pantalla.

Los puntos se refieren a los números comentados correspondientes del código anterior:

  1. Anulamos -initWithCoder: porque la vista nace de un XIB, como lo configuraremos en breve.
  2. Inhabilitamos varios toques: vamos a tratar solo con una secuencia de toques, lo que significa que el usuario solo puede dibujar con un dedo a la vez; Cualquier otro dedo colocado en la pantalla durante ese tiempo será ignorado. ¡Esto es una simplificación, pero no necesariamente irrazonable, ya que las personas tampoco suelen escribir en papel con dos bolígrafos a la vez! En cualquier caso, evitará que nos desviemos demasiado, ya que tenemos suficiente trabajo por hacer.
  3. El UIBezierPath es una clase de UIKit que nos permite dibujar formas en la pantalla compuestas de líneas rectas o ciertos tipos de curvas.
  4. Ya que estamos haciendo dibujos personalizados, debemos anular el método -drawRect: de la vista. Hacemos esto acariciando la ruta cada vez que se agrega un nuevo segmento de línea.
  5. Ten en cuenta también que si bien el ancho de línea es una propiedad de la ruta, el color de la línea en sí es una propiedad del contexto de dibujo. Si no estás familiarizado con los contextos gráficos, puedes leer sobre ellos en los documentos de Apple. Por ahora, piensa en un contexto gráfico como un "lienzo" que dibujas cuando invalidas el método -drawRect: y el resultado de lo que ves es la vista en pantalla. En breve nos encontraremos con otro tipo de contexto de dibujo.

Antes de poder crear la aplicación, debemos configurar la subclase de vista que acabamos de crear en la vista on-screen.

  1. En el panel del navegador, haz clic en ViewController.xib (en caso de que hayas creado una aplicación Universal, simplemente realiza este paso para los archivos ViewController~iPhone.xib y ViewController~iPad.xib).
  2. Cuando la vista aparezca en el lienzo del generador de interfaces, haz clic en él para seleccionarlo. En el panel de utilidades, haz clic en el "Inspector de identidades" (tercer botón de la derecha en la parte superior del panel). La sección superior dice "Custom Class", aquí es donde establecerás la clase de la vista en la que hiciste clic.
  3. En este momento debería decir "UIView", pero debemos cambiarlo a (lo has adivinado) LinearInterpView. Escribe el nombre de la clase (simplemente escribiendo "L" deberías hacer que el autocompletar suene de forma tranquilizadora).
  4. Nuevamente, si vas a probar esto como una aplicación universal, repite este paso exacto para los dos archivos XIB que la plantilla creó para ti.
Changing the view controller's view's class to our custom UIView subclass

Ahora compila la aplicación. Deberías obtener una vista en blanco brillante que puedes dibujar con el dedo. ¡Teniendo en cuenta las pocas líneas de código que hemos escrito, los resultados no están tan mal! Por supuesto, tampoco son espectaculares. La apariencia de conectar los puntos es bastante notoria (y sí, mi escritura también apesta).

Our first attempt at freehand drawing

Asegúrate de ejecutar la aplicación no solo en el simulador, sino también en un dispositivo real.

Si juegas con la aplicación por un tiempo en tu dispositivo, estás obligado a notar algo: finalmente, la respuesta de la interfaz de usuario comienza a demorarse, y en lugar de los ~ 60 puntos de contacto que se adquirían por segundo, por alguna razón, la cantidad de puntos La interfaz de usuario es capaz de muestrear gotas más y más. Como los puntos se están separando, la interpolación en línea recta hace que el dibujo sea "más bloqueado" que antes. Esto es ciertamente indeseable. Entonces, ¿qué está pasando?


Mantener el rendimiento y la capacidad de respuesta

Revisemos lo que hemos estado haciendo: a medida que dibujamos, adquirimos puntos, los agregamos a una ruta en constante crecimiento y luego representamos la ruta *completa* en cada ciclo del ciclo principal. Entonces, a medida que el camino se hace más largo, en cada iteración, el sistema de dibujo tiene más que dibujar y, finalmente, se vuelve demasiado, lo que dificulta que la aplicación se mantenga al día. Dado que todo está sucediendo en el hilo principal, nuestro código de dibujo está compitiendo con el código de UI que, entre otras cosas, tiene que probar los toques en la pantalla.

Te perdonarían por pensar que había una forma de dibujar "encima de" lo que ya estaba en la pantalla; desafortunadamente, aquí es donde debemos liberarnos de la analogía del lápiz en el papel, ya que el sistema de gráficos no funciona de esa manera por defecto. Aunque, en virtud del código que vamos a escribir a continuación, indirectamente vamos a implementar el enfoque de "dibujar en la parte superior".

Si bien hay algunas cosas que podemos intentar para arreglar el rendimiento de nuestro código, solo vamos a implementar una idea, ya que resulta suficiente para nuestras necesidades actuales.

Crea una nueva subclase UIView como lo hiciste antes, denominándola CachedLIView (el LI es para recordarnos que todavía estamos haciendo interpolación lineal). Borra todos los contenidos de CachedLIView.m y reemplázalos con lo siguiente:

Después de guardar, ¡recuerda cambiar la clase del objeto de vista en tu XIB (s) a CachedLIView!

Cuando el usuario coloca su dedo en la pantalla para dibujar, comenzamos con un camino nuevo sin puntos o líneas en él, y le agregamos segmentos de línea como lo hicimos antes.

De nuevo, refiriéndose a los números en los comentarios:

  1. Además, mantenemos en la memoria una imagen de mapa de bits (fuera de pantalla) del mismo tamaño que nuestro lienzo (es decir, en la vista de pantalla), en la que podemos almacenar lo que hemos dibujado hasta ahora.
  2. Dibujamos los contenidos en pantalla en este búfer cada vez que el usuario levanta su dedo (señalado por -touchesEnded:WithEvent).
  3. El método drawBitmap crea un contexto de mapa de bits: los métodos UIKit necesitan un "contexto actual" (un lienzo) para dibujar. Cuando estamos dentro de -drawRect: este contexto se pone automáticamente a nuestra disposición y refleja lo que dibujamos en nuestra vista en pantalla. Por el contrario, el contexto del mapa de bits debe crearse y destruirse explícitamente, y los contenidos dibujados residen en la memoria.
  4. Al almacenar en caché el dibujo anterior de esta manera, podemos deshacernos del contenido anterior de la ruta, y de esta manera evitar que la ruta se haga demasiado larga.
  5. Ahora, cada vez que se llama a drawRect:, primero dibujamos los contenidos del búfer de memoria en nuestra vista que (por diseño) tiene exactamente el mismo tamaño, y para el usuario mantenemos la ilusión de dibujo continuo, solo de una manera diferente a antes de.

Si bien esto no es perfecto (¿qué pasaría si nuestro usuario sigue dibujando sin levantar el dedo, alguna vez?), Será lo suficientemente bueno para el alcance de este tutorial. Te recomendamos que experimentes por tu cuenta para encontrar un método mejor. Por ejemplo, podrías intentar guardar el dibujo periódicamente en lugar de cuando el usuario levanta el dedo. A medida que sucede, este procedimiento de almacenamiento en caché fuera de la pantalla nos brinda la oportunidad de procesar en segundo plano, en caso de que decidamos implementarlo. Pero no vamos a hacer eso en este tutorial. Sin embargo, ¡estás invitado a probar por tu cuenta!


Mejorar la calidad del trazo visual

Ahora vamos a centrar nuestra atención en hacer que el dibujo se vea mejor. Hasta ahora, hemos estado uniendo puntos de contacto adyacentes con segmentos de línea recta. Pero normalmente cuando dibujamos a mano alzada, nuestro trazo natural tiene una apariencia fluida y curvilínea (en lugar de rígida y con bloques). Tiene sentido que intentemos interpolar nuestros puntos con curvas en lugar de segmentos de línea. Afortunadamente, la clase UIBezierPath nos permite dibujar su homónimo: curvas Bezier.

¿Qué son las curvas de Bezier? Sin invocar la definición matemática, una curva Bezier se define por cuatro puntos: dos puntos finales a través de los cuales pasa una curva y dos "puntos de control" que ayudan a definir las tangentes que la curva debe tocar en sus puntos finales (esto es técnicamente una curva Bezier cúbica, pero por simplicidad me referiré a esto simplemente como una "curva de Bezier").

A cubic Bezier curve

Las curvas de Bezier nos permiten dibujar todo tipo de formas interesantes.

Interesting shapes that cubic Beziers are capable of making

Lo que intentaremos ahora es agrupar secuencias de cuatro puntos de contacto adyacentes e interpolar la secuencia de puntos dentro de un segmento de curva Bezier. Cada par adyacente de segmentos Bezier compartirá un punto final en común para mantener la continuidad del trazo.

Ya conoces el ejercicio. Crea una nueva subclase UIView y llámala BezierInterpView. Pega el siguiente código en el archivo .m:

Como lo indican los comentarios en línea, el cambio principal es la introducción de un par de nuevas variables para realizar un seguimiento de los puntos en nuestros segmentos Bezier, y una modificación del método -(void)touchesMoved:withEvent: para dibujar un segmento Bezier para cada cuatro puntos (en realidad, cada tres puntos, en términos de los toques que nos informa la aplicación, porque compartimos un punto final para cada par de segmentos adyacentes de Bezier).

Podría indicar aquí que hemos descuidado el caso de que el usuario levante su dedo y termine la secuencia táctil antes de que tengamos suficientes puntos para completar nuestro último segmento Bezier. Si es así, tendrías razón! Mientras que visualmente esto no hace mucha diferencia, en ciertos casos importantes lo hace. Por ejemplo, trata de dibujar un círculo pequeño. Puede que no se cierre completamente, y en una aplicación real querría manejar esto de manera apropiada en el método -touchesEnded: WithEvent. Mientras estamos en ello, tampoco hemos prestado ninguna atención especial al caso de la cancelación de toque. El método de instancia touchesCancelled: WithEvent maneja esto. Eche un vistazo a la documentación oficial y vea si hay casos especiales que deba manejar aquí.

Entonces, ¿cómo se ven los resultados? Una vez más, les recuerdo que establezcan la clase correcta en el XIB antes de construir.

Minor improvement, if any

Hum. No parece una gran mejora, ¿verdad? Creo que podría ser un poco mejor que la interpolación en línea recta, o tal vez eso es sólo una ilusión. En cualquier caso, no vale la pena presumir.


Seguir mejorando la calidad del trazo

Esto es lo que creo que está sucediendo: mientras nos tomamos la molestia de interpolar cada secuencia de cuatro puntos con un segmento de curva suave, no estamos haciendo ningún esfuerzo para hacer que un segmento de curva pase sin problemas al siguiente, por lo que efectivamente todavía tengo un problema con el resultado final.

Entonces, ¿qué podemos hacer al respecto? Si vamos a seguir el enfoque que comenzamos en la última versión (es decir, usando curvas Bezier), debemos cuidar la continuidad y la suavidad en el "punto de unión" de dos segmentos Bezier adyacentes. Las dos tangentes en el punto final con los puntos de control correspondientes (segundo punto de control del primer segmento y primer punto de control del segundo segmento) parecen ser la clave; Si estas dos tangentes tuvieran la misma dirección, entonces la curva sería más suave en la unión.

Tangents are the junction of two Bezier segments are

¿Qué sucede si movemos el punto final común en algún lugar de la línea que une los dos puntos de control? Sin utilizar datos adicionales sobre los puntos de contacto, el mejor punto parece ser el punto medio de la línea que une los dos puntos de control en consideración, y nuestro requisito impuesto sobre la dirección de las dos tangentes se cumpliría. ¡Intentemos esto!

Shifting the junction point to make the intersegment transition smooth

Crea una subclase UIView (una vez más) y asígnale el nombre SmoothedBIView. Reemplaza el código completo en el archivo .m con lo siguiente:

El quid del algoritmo que analizamos anteriormente se implementa en el método -touchesMoved:WithEvent:. Los comentarios en línea te ayudarán a vincular la discusión con el código.

Entonces, ¿cómo van los resultados, visualmente hablando? Recuerda hacer la cosa con el XIB.

Not bad at all!

Afortunadamente, hay mejoras sustanciales en esta ocasión. Teniendo en cuenta la simplicidad de nuestra modificación, se ve bastante bien (¡si lo digo yo mismo!). Nuestro análisis del problema con la iteración anterior y nuestra solución propuesta también se han validado.


A dónde ir desde aquí

Espero que hayas encontrado este tutorial beneficioso. Esperemos que desarrolles tus propias ideas sobre cómo mejorar el código. Una de las mejoras más importantes (pero fáciles) que puedes incorporar es manejar el final de las secuencias táctiles con más gracia, como se explicó anteriormente.

Otro caso que descuidé es el manejo de una secuencia táctil. La cual consiste en que el usuario toque la vista con el dedo y luego la levante sin haberla movido, con un toque efectivo en la pantalla. El usuario probablemente esperaría dibujar un punto o un pequeño garabato en la vista de esta manera, pero con nuestra implementación actual no pasa nada porque nuestro código de dibujo no se activa a menos que nuestra vista reciba el mensaje -touchesMoved:WithEvent:. Es posible que desees echar un vistazo a la documentación de la clase UIBezierPath para ver qué otros tipos de rutas puedes construir.

Si tu aplicación hace más trabajo que el que hicimos aquí (¡y en una aplicación de dibujo que vale la pena enviar, lo haría!), Diseñándola de tal manera que el código de no UI (en particular, el almacenamiento en caché fuera de la pantalla) se ejecute en un hilo de fondo, hace una diferencia significativa en un dispositivo multinúcleo (iPad 2 en adelante). Incluso en un dispositivo de un solo procesador, como el iPhone 4, el rendimiento debería mejorar, ya que creo que el procesador dividirá el trabajo de almacenamiento en caché que, después de todo, ocurre solo una vez cada pocos ciclos del ciclo principal.

Te animo a que flexiones tus músculos de codificación y juegue con UIKit API para desarrollar y mejorar algunas de las ideas implementadas en este tutorial. ¡Diviértete, y gracias por leer!

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.