Freihandzeichnen auf iOS

In diesem Lernprogramm erfahren Sie, wie Sie einen erweiterten Zeichnungsalgorithmus implementieren, der das nahtlose Zeichnen auf iOS-Geräten ermöglicht. Weiter lesen!

Theoretische Übersicht

Berührung ist die primäre Art und Weise, wie ein Benutzer mit iOS-Geräten interagiert. Eine der natürlichsten und naheliegendsten Funktionen, die diese Geräte bieten sollen, besteht darin, dass der Benutzer mit dem Finger auf dem Bildschirm zeichnen kann. Derzeit befinden sich im App Store viele Freihand-Zeichnungs- und Notiz-Apps, und viele Unternehmen bitten Kunden sogar, beim Kauf ein iDevice zu unterzeichnen. Wie funktionieren diese Anwendungen eigentlich? Lass uns kurz anhalten und überlegen, was "unter der Haube" los ist.

Wenn ein Benutzer durch eine Tabellenansicht blättert, ein Bild vergrößert oder eine Kurve in einer Mal-App zeichnet, aktualisiert sich die Geräteanzeige schnell (z. B. 60-mal pro Sekunde) und die Anwendungsschleife ist konstant Probenahme die Position der Finger des Benutzers. Während dieses Vorgangs muss die "analoge" Eingabe eines Fingers, der über den Bildschirm gezogen wird, in einen digitalen Satz von Punkten auf der Anzeige umgewandelt werden, und dieser Umwandlungsvorgang kann erhebliche Herausforderungen darstellen. Im Zusammenhang mit unserer Mal-App haben wir ein "Datenanpassungsproblem" an uns. Da der Benutzer fröhlich auf dem Gerät kritzelt, muss der Programmierer im Wesentlichen fehlende analoge Informationen ("Verbindungspunkte") interpolieren, die zwischen den abgetasteten Berührungspunkten, die iOS uns gemeldet hat, verloren gegangen sind. Ferner muss diese Interpolation so erfolgen, dass das Ergebnis ein Strich ist, der für den Endbenutzer kontinuierlich, natürlich und glatt erscheint, als hätte er mit einem Stift auf einem Notizblock aus Papier skizziert.

In diesem Lernprogramm soll gezeigt werden, wie Freihandzeichnen unter iOS implementiert werden kann, angefangen von einem grundlegenden Algorithmus, der eine Interpolation mit geraden Linien durchführt, und zu einem komplexeren Algorithmus, der die Qualität erreicht, die von bekannten Anwendungen wie Penultimate erreicht wird. Wenn das Erstellen eines Algorithmus, der funktioniert, nicht schwer genug ist, müssen wir auch sicherstellen, dass der Algorithmus gut funktioniert. Wie wir sehen werden, kann eine naive Zeichnungsimplementierung zu einer App mit erheblichen Leistungsproblemen führen, die das Zeichnen umständlich und möglicherweise unbrauchbar macht.


Fertig machen

Ich gehe davon aus, dass Sie nicht völlig neu in der iOS-Entwicklung sind. Ich habe also die Schritte zum Erstellen eines neuen Projekts, zum Hinzufügen von Dateien zum Projekt usw. überarbeitet. Hoffentlich ist hier sowieso nichts zu schwierig, aber nur für den Fall Sie können den vollständigen Projektcode herunterladen und mit diesem spielen.

Starten Sie ein neues Xcode iPad-Projekt basierend aufEinzelansicht-Anwendung"Vorlage und Namen"FreehandDrawingTut". Aktivieren Sie die automatische Referenzzählung (Automatic Reference Counting, ARC), deaktivieren Sie jedoch Storyboards und Unit-Tests. Sie können dieses Projekt entweder als iPhone oder als Universal-App erstellen, je nachdem, welche Geräte Sie zum Testen zur Verfügung haben.

Fahren Sie anschließend fort und wählen Sie im Xcode Navigator das Projekt "FreeHandDrawingTut" aus, und stellen Sie sicher, dass nur die Ausrichtung im Hochformat unterstützt wird:

Wenn Sie eine Bereitstellung auf iOS 5.x oder früher durchführen möchten, können Sie die Unterstützung für die Ausrichtung folgendermaßen ändern:

 - (BOOL) shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation) interfaceOrientation return (interfaceOrientation == UIInterfaceOrientationPortrait); 

Ich mache das, um die Dinge einfach zu halten, damit wir uns auf das Hauptproblem konzentrieren können.

Ich möchte unseren Code iterativ weiterentwickeln und ihn schrittweise verbessern - wie Sie es von Grund auf tun würden, wenn Sie bei Null anfangen würden -, anstatt die endgültige Version in einem Zug auf Sie zu legen. Ich hoffe, mit diesem Ansatz können Sie die verschiedenen Themen besser in den Griff bekommen. Um dies zu bedenken, und um das wiederholte Löschen, Ändern und Hinzufügen von Code in derselben Datei zu vermeiden, der möglicherweise unordentlich und fehleranfällig wird, gehe ich folgendermaßen vor:

  • Für jede Iteration erstellen wir eine neue UIView-Unterklasse. Ich werde den gesamten erforderlichen Code bereitstellen, damit Sie einfach die .m-Datei der neuen UIView-Unterklasse kopieren und einfügen können, die Sie erstellen. Es gibt keine öffentliche Schnittstelle für die Funktionalität der Ansichtsunterklasse, was bedeutet, dass Sie die .h-Datei nicht berühren müssen.
  • Um jede neue Version zu testen, müssen wir die UIView-Unterklasse, die wir erstellt haben, der aktuell zugewiesenen Ansicht zuweisen. Ich zeige Ihnen, wie Sie das mit Interface Builder beim ersten Mal machen, indem Sie die einzelnen Schritte ausführlich durchgehen und Sie dann jedes Mal daran erinnern, wenn wir eine neue Version programmieren.

Erster Versuch beim Zeichnen

Wählen Sie in Xcode Datei> Neu> Datei… , Wählen Sie als Vorlage Objective-C-Klasse aus und geben Sie auf dem nächsten Bildschirm die Datei an LinearInterpView und mache es eine Unterklasse von UIView. Speichern Sie es. Der Name "LinearInterp" steht hier für "lineare Interpolation". Für das Tutorial benenne ich jede UIView-Unterklasse, die wir erstellen, um ein Konzept oder einen Ansatz hervorzuheben, der im Klassencode eingeführt wurde.

Wie bereits erwähnt, können Sie die Header-Datei unverändert lassen. Löschen alles den in der Datei LinearInterpView.m vorhandenen Code, und ersetzen Sie ihn durch Folgendes:

 #import "LinearInterpView.h" @implementation LinearInterpView UIBezierPath * path; // (3) - (id) initWithCoder: (NSCoder *) aDecoder // (1) if (self = [super initWithCoder: aDecoder]) [self setMultipleTouchEnabled: NO]; // (2) [selfsetBackgroundColor: [UIColor whiteColor]]; Pfad = [UIBezierPath BezierPath]; [Pfad setLineWidth: 2.0];  return self;  - (void) drawRect: (CGRect) rect // (5) [[UIColor blackColor] setStroke]; [Pfadschlag];  - (void) touchesBegan: (NSSet *) berührt withEvent: (UIEvent *) - Ereignis UITouch * touch = [berührt anyObject]; CGPoint p = [touch locationInView: self]; [path moveToPoint: p];  - (void) touchesMoved: (NSSet *) berührt withEvent: (UIEvent *) ereignis UITouch * touch = [berührt anyObject]; CGPoint p = [touch locationInView: self]; [Pfad addLineToPoint: p]; // (4) [self setNeedsDisplay];  - (void) touchesEnded: (NSSet *) berührt withEvent: (UIEvent *) - Ereignis [self touchesMoved: berührt mitEvent: Ereignis];  - (void) touchesCancelled: (NSSet *) berührt withEvent: (UIEvent *) - Ereignis [self touchesEnded: berührt mitEvent: Ereignis];  @Ende

In diesem Code arbeiten wir direkt mit den Berührungsereignissen, die uns die Anwendung bei jeder Berührungssequenz meldet. Das heißt, der Benutzer legt einen Finger auf die Bildschirmansicht, bewegt den Finger darüber und hebt schließlich den Finger vom Bildschirm. Für jedes Ereignis in dieser Reihenfolge sendet die Anwendung eine entsprechende Nachricht (in iOS-Terminologie werden die Nachrichten an den "Ersthelfer" gesendet; Details finden Sie in der Dokumentation.).

Um mit diesen Nachrichten umzugehen, implementieren wir die Methoden -berührtBegan: WithEvent: und company, die in der UIResponder-Klasse deklariert sind, von der UIView erbt. Wir können Code schreiben, um die Berührungsereignisse auf beliebige Weise zu handhaben. In unserer App möchten wir die Position der Berührungen auf dem Bildschirm abfragen, einige Verarbeitungsschritte ausführen und dann Linien auf dem Bildschirm zeichnen.

Die Punkte beziehen sich auf die entsprechenden kommentierten Zahlen aus dem obigen Code:

  1. Wir überschreiben -initWithCoder: weil die Sicht von einer XIB getragen wird, wie wir in Kürze einrichten werden.
  2. Wir haben mehrere Berührungen deaktiviert: Wir werden nur eine Berührungssequenz behandeln, was bedeutet, dass der Benutzer nur mit einem Finger gleichzeitig zeichnen kann. Alle anderen Finger, die sich während dieser Zeit auf dem Bildschirm befinden, werden ignoriert. Dies ist eine Vereinfachung, aber nicht unbedingt unvernünftig. Normalerweise schreiben die Leute nicht mit zwei Stiften gleichzeitig auf Papier! In jedem Fall werden wir uns nicht zu weit davon entfernen, da wir bereits genug Arbeit haben.
  3. Das UIBezierPath ist eine UIKit-Klasse, mit der wir auf dem Bildschirm Formen zeichnen können, die aus geraden Linien oder bestimmten Kurvenarten bestehen.
  4. Da wir benutzerdefinierte Zeichnungen erstellen, müssen wir die Ansicht überschreiben -drawRect: Methode. Wir tun dies, indem wir den Pfad jedes Mal streicheln, wenn ein neues Liniensegment hinzugefügt wird.
  5. Beachten Sie auch, dass die Linienbreite zwar eine Eigenschaft des Pfads ist, die Farbe der Linie selbst jedoch eine Eigenschaft des Zeichnungskontexts ist. Wenn Sie mit Grafikkontexten nicht vertraut sind, können Sie dies in den Apple-Dokumenten nachlesen. Stellen Sie sich einen Grafikkontext vorerst als eine "Leinwand" vor, in die Sie beim Überschreiben des Zeichens zeichnen -drawRect: Methode und das Ergebnis dessen, was Sie sehen, ist die Ansicht auf dem Bildschirm. In Kürze werden wir auf eine andere Art von Zeichnungskontext stoßen.

Bevor wir die Anwendung erstellen können, müssen wir die soeben erstellte Ansichtsunterklasse auf die Bildschirmansicht setzen.

  1. Klicken Sie im Navigatorfenster auf ViewController.xib (Falls Sie eine Universal-App erstellt haben, führen Sie diesen Schritt einfach für beide aus ViewController ~ iPhone.xib und ViewController ~ iPad.xib Dateien).
  2. Wenn die Ansicht im Interface-Builder-Canvas angezeigt wird, klicken Sie darauf, um sie auszuwählen. Klicken Sie im Dienstprogrammbereich auf "Identities Inspector" (dritte Schaltfläche von rechts oben im Fensterbereich). Im obersten Abschnitt wird "Benutzerdefinierte Klasse" angezeigt. Hier legen Sie die Klasse der Ansicht fest, auf die Sie geklickt haben.
  3. Im Moment sollte es "UIView" sagen, aber wir müssen es ändern (Sie haben es erraten) LinearInterpView. Geben Sie den Namen der Klasse ein (einfach durch Eingabe von "L" klingt Autocomplete beruhigend).
  4. Wenn Sie dies als Universal-App testen möchten, wiederholen Sie diesen Schritt genau für beide XIB-Dateien, die die Vorlage für Sie erstellt hat.

Erstellen Sie jetzt die Anwendung. Sie sollten eine glänzende weiße Ansicht erhalten, in die Sie mit Ihrem Finger zeichnen können. In Anbetracht der wenigen Codezeilen, die wir geschrieben haben, sind die Ergebnisse nicht zu schäbig! Natürlich sind sie auch nicht spektakulär. Die Erscheinung der Verbindungspunkte ist ziemlich auffällig (und ja, meine Handschrift ist auch scheiße).

Stellen Sie sicher, dass Sie die App nicht nur auf dem Simulator ausführen, sondern auch auf einem echten Gerät.

Wenn Sie eine Weile mit der Anwendung auf Ihrem Gerät spielen, werden Sie sicherlich etwas bemerken: Irgendwann beginnt die Reaktion der Benutzeroberfläche zu verzögern, und anstelle der ~ 60 Berührungspunkte, die pro Sekunde erfasst wurden, aus irgendeinem Grund die Anzahl der Punkte Die Benutzeroberfläche kann Tropfen immer weiter untersuchen. Da die Punkte weiter auseinander liegen, wird die Zeichnung durch die Interpolation der geraden Linie noch "blockierter" als zuvor. Dies ist sicherlich unerwünscht. So was ist los?


Leistung und Reaktionsfähigkeit erhalten

Lassen Sie uns einen Blick auf das werfen, was wir gemacht haben: Beim Zeichnen sammeln wir Punkte, fügen sie einem ständig wachsenden Pfad hinzu und rendern dann den * vollständigen * Pfad in jedem Zyklus der Hauptschleife. Da der Pfad länger wird, muss das Zeichensystem in jeder Iteration mehr zeichnen und schließlich wird es zu viel, was es der App schwer macht, mitzuhalten. Da alles im Haupt-Thread geschieht, konkurriert unser Zeichnungscode mit dem UI-Code, der unter anderem die Berührungen auf dem Bildschirm abtasten muss.

Es wäre Ihnen verzeiht zu denken, dass es eine Möglichkeit gibt, "über das" zu zeichnen, was bereits auf dem Bildschirm war; Leider müssen wir uns hier von der Stift-auf-Papier-Analogie befreien, da das Grafiksystem standardmäßig nicht so funktioniert. Obwohl wir aufgrund des Codes als nächstes schreiben werden, werden wir indirekt den "Draw-on-Top" -Ansatz implementieren.

Es gibt zwar ein paar Dinge, mit denen wir versuchen könnten, die Leistung unseres Codes zu verbessern, aber wir werden nur eine Idee umsetzen, da sich herausstellt, dass sie für unsere heutigen Bedürfnisse ausreichend ist.

Erstellen Sie eine neue UIView-Unterklasse wie zuvor und benennen Sie sie CachedLIView (Das LI soll uns daran erinnern, dass wir noch tun Lim Ohr ichInterpolation). Löschen Sie den gesamten Inhalt von CachedLIView.m und ersetze es durch folgendes:

 #import "CachedLIView.h" @implementation CachedLIView UIBezierPath * -Pfad; UIImage * incrementalImage; // (1) - (id) initWithCoder: (NSCoder *) aDecoder if (self = [super initWithCoder: aDecoder]) [self setMultipleTouchEnabled: NO]; [self setBackgroundColor: [UIColor whiteColor]]; Pfad = [UIBezierPath BezierPath]; [Pfad setLineWidth: 2.0];  return self;  - (void) drawRect: (CGRect) rect [incrementalImage drawInRect: rect]; // (3) [Pfadstrich];  - (void) touchesBegan: (NSSet *) berührt withEvent: (UIEvent *) - Ereignis UITouch * touch = [berührt anyObject]; CGPoint p = [touch locationInView: self]; [path moveToPoint: p];  - (void) touchesMoved: (NSSet *) berührt withEvent: (UIEvent *) ereignis UITouch * touch = [berührt anyObject]; CGPoint p = [touch locationInView: self]; [Pfad addLineToPoint: p]; [self setNeedsDisplay];  - (void) touchesEnded: (NSSet *) berührt withEvent: (UIEvent *) Ereignis // (2) UITouch * touch = [berührt anyObject]; CGPoint p = [touch locationInView: self]; [Pfad addLineToPoint: p]; [self drawBitmap]; // (3) [self setNeedsDisplay]; [Pfad removeAllPoints]; // (4) - (void) touchesCancelled: (NSSet *) berührt withEvent: (UIEvent *) event [self touchesEnded: berührt mitEvent: event];  - (void) drawBitmap // (3) UIGraphicsBeginImageContextWithOptions (self.bounds.size, YES, 0.0); [[UIColor blackColor] setStroke]; if (! incrementalImage) // first draw; Malen Sie den Hintergrund weiß mit… UIBezierPath * rectpath = [UIBezierPath bezierPathWithRect: self.bounds]; // Umschließen der Bitmap durch ein Rechteck, das von einem anderen UIBezierPath-Objekt definiert wird [[UIColor whiteColor] setFill]; [rectpath fill]; // mit weiß füllen [incrementalImage drawAtPoint: CGPointZero]; [Pfadschlag]; incrementalImage = UIGraphicsGetImageFromCurrentImageContext (); UIGraphicsEndImageContext ();  @Ende

Denken Sie nach dem Speichern daran, die Klasse des Ansichtsobjekts in Ihren XIBs in CachedLIView zu ändern!

Wenn der Benutzer den Finger zum Zeichnen auf den Bildschirm legt, beginnen wir mit einem neuen Pfad ohne Punkte oder Linien und fügen Liniensegmente wie zuvor hinzu.

Wieder unter Bezugnahme auf die Zahlen in den Kommentaren:

  1. Außerdem speichern wir ein Bitmap-Bild (offscreen) in derselben Größe wie unsere Leinwand (d. H. In der Bildschirmansicht), in dem wir speichern können, was wir bisher gezeichnet haben.
  2. Wir ziehen den Inhalt auf dem Bildschirm jedes Mal in diesen Puffer, wenn der Benutzer den Finger anhebt (signalisiert durch -touchesEnded: WithEvent)..
  3. Die drawBitmap-Methode erstellt einen Bitmap-Kontext. UIKit-Methoden benötigen einen "aktuellen Kontext" (eine Zeichenfläche), in den gezeichnet werden kann. Wenn wir drinnen sind -drawRect: Dieser Kontext wird uns automatisch zur Verfügung gestellt und spiegelt wider, was wir in unsere Bildschirmansicht ziehen. Im Gegensatz dazu muss der Bitmap-Kontext explizit erstellt und gelöscht werden, und der gezeichnete Inhalt befindet sich im Speicher.
  4. Indem Sie die vorherige Zeichnung auf diese Weise zwischenspeichern, können Sie die vorherigen Inhalte des Pfads loswerden und auf diese Weise verhindern, dass der Pfad zu lang wird.
  5. Jetzt jedes mal drawRect: heißt, wir ziehen zunächst den Inhalt des Speicherpuffers in unsere Ansicht, die (vom Design her) genau die gleiche Größe hat, und so halten wir für den Benutzer die Illusion einer kontinuierlichen Zeichnung nur auf eine andere Art und Weise als zuvor.

Dies ist zwar nicht perfekt (was ist, wenn unser Benutzer ständig zeichnet, ohne den Finger zu heben, oder?), Ist aber für den Umfang dieses Tutorials ausreichend. Sie werden aufgefordert, selbst zu experimentieren, um eine bessere Methode zu finden. Sie können beispielsweise versuchen, die Zeichnung regelmäßig zu zwischenspeichern, anstatt nur, wenn der Benutzer den Finger hebt. Dieses Off-Screen-Caching-Verfahren bietet uns die Möglichkeit der Hintergrundverarbeitung, falls wir uns für die Implementierung entscheiden. Aber das machen wir in diesem Tutorial nicht. Sie sind jedoch eingeladen, es selbst auszuprobieren!


Verbesserung der visuellen Schlaganfallqualität

Wenden wir uns nun der Verbesserung der Zeichnung zu. Bisher haben wir benachbarte Berührungspunkte mit geraden Liniensegmenten verbunden. Wenn wir jedoch freihändig zeichnen, wirkt unser natürlicher Schlaganfall normalerweise fließend und kurvig (nicht blockig und starr). Es ist sinnvoll, dass wir versuchen, unsere Punkte mit Kurven anstatt mit Liniensegmenten zu interpolieren. Glücklicherweise lässt uns die UIBezierPath-Klasse ihren Namensvetter zeichnen: Bezier-Kurven.

Was sind Bezierkurven? Ohne die mathematische Definition aufzurufen, wird eine Bezier-Kurve durch vier Punkte definiert: zwei Endpunkte, durch die eine Kurve verläuft, und zwei "Kontrollpunkte", die helfen, Tangenten zu definieren, die die Kurve an ihren Endpunkten berühren muss (dies ist technisch gesehen eine kubische Bezier-Kurve, jedoch der Einfachheit halber werde ich es einfach als "Bezier-Kurve" bezeichnen).

Bezier-Kurven ermöglichen uns das Zeichnen aller Arten von interessanten Formen.

Wir werden jetzt versuchen, Sequenzen von vier benachbarten Berührungspunkten zu gruppieren und die Punktsequenz innerhalb eines Bezier-Kurvensegments zu interpolieren. Jedes benachbarte Paar von Beziersegmenten hat einen gemeinsamen Endpunkt, um die Kontinuität des Strichs zu gewährleisten.

Sie kennen den Bohrer inzwischen. Erstellen Sie eine neue UIView-Unterklasse und benennen Sie sie BezierInterpView. Fügen Sie den folgenden Code in die M-Datei ein:

 #import "BezierInterpView.h" @implementation BezierInterpView UIBezierPath * path; UIImage * incrementalImage; CGPoint-Punkte [4]; // um die vier Punkte unseres Bezier-Segments zu verfolgen uint ctr; // eine Zählervariable, um den Punktindex zu verfolgen - (id) initWithCoder: (NSCoder *) aDecoder if (self = [super initWithCoder: aDecoder]) [self setBackgroundColor: [UIColor whiteColor]]; Pfad = [UIBezierPath BezierPath]; [Pfad setLineWidth: 2.0];  return self;  - (void) drawRect: (CGRect) rect [incrementalImage drawInRect: rect]; [Pfadschlag];  - (void) touchesBegan: (NSSet *) berührt withEvent: (UIEvent *) event ctr = 0; UITouch * touch = [berührt anyObject]; pts [0] = [touch locationInView: self];  - (void) touchesMoved: (NSSet *) berührt withEvent: (UIEvent *) ereignis UITouch * touch = [berührt anyObject]; CGPoint p = [touch locationInView: self]; ctr ++; pts [ctr] = p; if (ctr == 3) // vierter Punkt [path moveToPoint: pts [0]]; [Pfad addCurveToPoint: pts [3] controlPoint1: pts [1] controlPoint2: pts [2]]; // So wird eine Bezier-Kurve an einen Pfad angehängt. Wir fügen ein kubisches Bezier von pt [0] bis pt [3] hinzu, mit den Kontrollpunkten pt [1] und pt [2] [self setNeedsDisplay]; pts [0] = [Pfadstrompunkt]; ctr = 0;  - (void) touchesEnded: (NSSet *) berührt withEvent: (UIEvent *) event [self drawBitmap]; [self setNeedsDisplay]; pts [0] = [Pfadstrompunkt]; // Der zweite Endpunkt des aktuellen Bezier-Segments sei der erste für das nächste Bezier-Segment [path removeAllPoints]; ctr = 0;  - (void) touchesCancelled: (NSSet *) berührt withEvent: (UIEvent *) - Ereignis [self touchesEnded: berührt mitEvent: Ereignis];  - (void) drawBitmap UIGraphicsBeginImageContextWithOptions (self.bounds.size, YES, 0.0); [[UIColor blackColor] setStroke]; if (! incrementalImage) // erstes Mal; Hintergrundfarbe malen UIBezierPath * rectpath = [UIBezierPath bezierPathWithRect: self.bounds]; [[UIColor whiteColor] setFill]; [rectpath fill];  [incrementalImage drawAtPoint: CGPointZero]; [Pfadschlag]; incrementalImage = UIGraphicsGetImageFromCurrentImageContext (); UIGraphicsEndImageContext ();  @Ende

Wie aus den Inline-Kommentaren hervorgeht, ist die Hauptänderung die Einführung einiger neuer Variablen, um die Punkte in unseren Bezier-Segmenten zu verfolgen, und eine Änderung der -(void) touchesMoved: withEvent: Methode zum Zeichnen eines Bezier-Segments für jeweils vier Punkte (eigentlich alle drei Punkte in Bezug auf die Berührungen, die uns von der App gemeldet werden, da wir einen Endpunkt für jedes benachbarte Paar von Bezier-Segmenten gemeinsam nutzen).

Sie können hier darauf hinweisen, dass wir den Fall vernachlässigt haben, in dem der Benutzer seinen Finger hob und die Berührungssequenz beendet, bevor wir genug Punkte haben, um unser letztes Bezier-Segment abzuschließen. Wenn ja, hättest du recht! Dies macht zwar keinen großen Unterschied, in bestimmten wichtigen Fällen jedoch. Versuchen Sie beispielsweise, einen kleinen Kreis zu zeichnen. Es wird möglicherweise nicht vollständig geschlossen, und in einer echten App möchten Sie dies in der -touchesEnded: WithEvent Methode. Wenn wir gerade dabei sind, haben wir uns auch nicht besonders mit dem Fall der Berührungsstornierung befasst. Das touchesCancelled: WithEvent Instanzmethode behandelt dies. Schauen Sie sich die offizielle Dokumentation an und prüfen Sie, ob es spezielle Fälle gibt, mit denen Sie hier umgehen müssen.

Wie sehen also die Ergebnisse aus? Ich erinnere Sie noch einmal daran, vor dem Bauen die richtige Klasse in der XIB einzustellen.

Huh. Es scheint nicht viel Verbesserung zu sein, oder? Ich denke es könnte sein leicht besser als gerade Interpolation, oder vielleicht ist es nur Wunschdenken. In jedem Fall lohnt es sich nicht zu prahlen.


Die Schlagqualität weiter verbessern

Ich denke, dass dies passiert: Während wir uns die Mühe machen, jede Folge von vier Punkten mit einem glatten Kurvensegment zu interpolieren, Wir bemühen uns nicht, ein Kurvensegment so zu gestalten, dass es reibungslos in das nächste übergeht, So effektiv haben wir immer noch ein Problem mit dem Endergebnis.

Was können wir dagegen tun? Wenn wir an dem Ansatz festhalten, den wir in der letzten Version begonnen haben (d. H. Mit Bezier-Kurven), müssen wir auf die Kontinuität und Glätte am "Verbindungspunkt" zweier benachbarter Bezier-Segmente achten. Die zwei Tangenten am Endpunkt mit den entsprechenden Kontrollpunkten (zweiter Kontrollpunkt des ersten Segments und erster Kontrollpunkt des zweiten Segments) scheinen der Schlüssel zu sein; Wenn beide Tangenten die gleiche Richtung hätten, wäre die Kurve an der Kreuzung glatter.

Was wäre, wenn wir den gemeinsamen Endpunkt auf der Verbindungslinie zwischen den beiden Kontrollpunkten verschieben? Ohne zusätzliche Daten zu den Berührungspunkten zu verwenden, scheint der beste Punkt der Mittelpunkt der Linie zu sein, die die beiden Kontrollpunkte miteinander verbindet, und unsere Anforderung an die Richtung der beiden Tangenten wäre erfüllt. Lass uns das versuchen!

Erstellen Sie eine UIView-Unterklasse (erneut) und nennen Sie sie SmoothedBIView. Ersetzen Sie den gesamten Code in der .m-Datei durch Folgendes:

 #import "SmoothedBIView.h" @implementation SmoothedBIView UIBezierPath * -Pfad; UIImage * incrementalImage; CGPoint-Punkte [5]; // Jetzt müssen wir die vier Punkte eines Bezier-Segments und den ersten Kontrollpunkt des nächsten Segments verfolgen. uint ctr;  - (id) initWithCoder: (NSCoder *) aDecoder if (self = [super initWithCoder: aDecoder]) [self setMultipleTouchEnabled: NO]; [self setBackgroundColor: [UIColor whiteColor]]; Pfad = [UIBezierPath BezierPath]; [Pfad setLineWidth: 2.0];  return self;  - (id) initWithFrame: (CGRect) frame self = [super initWithFrame: frame]; if (self) [self setMultipleTouchEnabled: NO]; Pfad = [UIBezierPath BezierPath]; [Pfad setLineWidth: 2.0];  return self;  // Überschreiben Sie nur drawRect: wenn Sie eine benutzerdefinierte Zeichnung ausführen. // Eine leere Implementierung wirkt sich nachteilig auf die Leistung während der Animation aus. - (void) drawRect: (CGRect) rect [incrementalImage drawInRect: rect]; [Pfadschlag];  - (void) touchesBegan: (NSSet *) berührt withEvent: (UIEvent *) event ctr = 0; UITouch * touch = [berührt anyObject]; pts [0] = [touch locationInView: self];  - (void) touchesMoved: (NSSet *) berührt withEvent: (UIEvent *) ereignis UITouch * touch = [berührt anyObject]; CGPoint p = [touch 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 ); // Verschiebe den Endpunkt in die Mitte der Linie, die den zweiten Kontrollpunkt des ersten Bezier-Segments und den ersten Kontrollpunkt des zweiten Bezier-Segments verbindet [path moveToPoint: pts [0]]; [Pfad addCurveToPoint: pts [3] controlPoint1: pts [1] controlPoint2: pts [2]]; // füge ein kubisches Bezier von pt [0] bis pt [3] hinzu, mit Kontrollpunkten pt [1] und pt [2] [self setNeedsDisplay]; // Punkte ersetzen und mit dem nächsten Segment fertig werden pts [0] = pts [3]; pts [1] = pts [4]; ctr = 1;  - (void) touchesEnded: (NSSet *) berührt withEvent: (UIEvent *) event [self drawBitmap]; [self setNeedsDisplay]; [Pfad removeAllPoints]; ctr = 0;  - (void) touchesCancelled: (NSSet *) berührt withEvent: (UIEvent *) - Ereignis [self touchesEnded: berührt mitEvent: Ereignis];  - (void) drawBitmap UIGraphicsBeginImageContextWithOptions (self.bounds.size, YES, 0.0); if (! incrementalImage) // erstes Mal; Hintergrundfarbe malen UIBezierPath * rectpath = [UIBezierPath bezierPathWithRect: self.bounds]; [[UIColor whiteColor] setFill]; [rectpath fill];  [incrementalImage drawAtPoint: CGPointZero]; [[UIColor blackColor] setStroke]; [Pfadschlag]; incrementalImage = UIGraphicsGetImageFromCurrentImageContext (); UIGraphicsEndImageContext ();  @Ende

Der Kern des oben diskutierten Algorithmus ist in implementiert -touchesMoved: WithEvent: Methode. Die Inline-Kommentare sollten Ihnen helfen, die Diskussion mit dem Code zu verknüpfen.

Wie sind also die Ergebnisse visuell? Denken Sie daran, die Sache mit der XIB zu machen.

Glücklicherweise gibt es diesmal erhebliche Verbesserungen. In Anbetracht der Einfachheit unserer Modifikation sieht es ziemlich gut aus (wenn ich es selbst sage!). Unsere Analyse des Problems mit der vorherigen Iteration und unsere vorgeschlagene Lösung wurden ebenfalls validiert.


Von hier aus weiter

Ich hoffe, Sie fanden dieses Tutorial hilfreich. Hoffentlich entwickeln Sie Ihre eigenen Ideen, wie Sie den Code verbessern können. Eine der wichtigsten (aber einfachen) Verbesserungen, die Sie einbinden können, besteht darin, das Ende der Berührungssequenzen wie zuvor besprochener zu behandeln.

Ein anderer Fall, den ich vernachlässigt habe, ist die Handhabung einer Berührungssequenz, die darin besteht, dass der Benutzer die Ansicht mit dem Finger berührt und dann anhebt, ohne dass er bewegt wurde - effektiv ein Tippen auf den Bildschirm. Der Benutzer würde wahrscheinlich erwarten, auf diese Weise einen Punkt oder ein kleines Kringel auf die Ansicht zu zeichnen, aber bei unserer aktuellen Implementierung passiert nichts, da unser Zeichnungscode erst dann wirksam wird, wenn unsere Ansicht die Ansicht erhält -touchesMoved: WithEvent: Botschaft. Vielleicht möchten Sie einen Blick auf die UIBezierPath Klassendokumentation, um zu sehen, welche anderen Arten von Pfaden Sie erstellen können.

Wenn Ihre App mehr Arbeit erledigt als das, was wir hier gemacht haben (und in einer Zeichnungs-App, die einen Versand wert wäre, würde dies der Fall sein!), So gestaltet, dass der Nicht-UI-Code (insbesondere das Off-Screen-Caching) in einem Hintergrundthread ausgeführt wird auf einem Multicore-Gerät (ab iPad 2) einen signifikanten Unterschied. Selbst bei einem Einzelprozessor-Gerät wie dem iPhone 4 sollte die Leistung verbessert werden, da der Prozessor die Caching-Arbeit verteilen würde, die schließlich nur alle paar Zyklen der Hauptschleife auftritt.

Ich möchte Sie dazu ermutigen, Ihre Codiermuskeln zu trainieren und mit der UIKit-API zu spielen, um einige der in diesem Lernprogramm implementierten Ideen zu entwickeln und zu verbessern. Viel Spaß und danke fürs Lesen!