Smooth Freehand Drawing op iOS

In deze zelfstudie leer je hoe je een geavanceerd tekenalgoritme kunt implementeren voor een soepele tekening uit de losse hand op iOS-apparaten. Lees verder!

Theoretisch overzicht

Aanraken is de belangrijkste manier waarop een gebruiker zal communiceren met iOS-apparaten. Een van de meest natuurlijke en voor de hand liggende functies die deze apparaten naar verwachting zullen bieden, is dat de gebruiker met zijn vinger op het scherm kan tekenen. Er zijn momenteel veel apps voor het tekenen en noteren van de vrije hand in de App Store en veel bedrijven vragen zelfs klanten een iDevice te ondertekenen bij het doen van aankopen. Hoe werken deze applicaties eigenlijk? Laten we even stilstaan ​​bij wat er gebeurt "onder de motorkap".

Wanneer een gebruiker een tabelweergave scrolt, knijpt om een ​​afbeelding te vergroten of een curve tekent in een schilderij-app, wordt de display van het apparaat snel bijgewerkt (bijvoorbeeld 60 keer per seconde) en wordt de toepassingsloop continu herhaald monsterneming de locatie van de vinger (s) van de gebruiker. Tijdens dit proces moet de "analoge" invoer van een vinger die over het scherm sleept, worden geconverteerd naar een digitale set van punten op het scherm, en dit conversieproces kan aanzienlijke uitdagingen vormen. In het kader van onze schilder-app hebben we een "datapasvormend" probleem bij onze handen. Naarmate de gebruiker vrolijk op het apparaat krabbelt, moet het programmeerapparaat in essentie ontbrekende analoge informatie ("connect-the-dots") interpoleren die verloren is gegaan tussen de gesampelde aanrakingspunten die iOS ons heeft gemeld. Verder moet deze interpolatie zodanig gebeuren dat het resultaat een lijn is die continu, natuurlijk en glad voor de eindgebruiker lijkt, alsof hij met een pen op een notitieblok van papier had getekend.

Het doel van deze zelfstudie is om te laten zien hoe FreeHand-tekeningen kunnen worden geïmplementeerd op iOS, te beginnen met een basisalgoritme dat rechtlijnige interpolatie uitvoert en doorgaat naar een meer geavanceerd algoritme dat de kwaliteit benadert die wordt geboden door bekende toepassingen zoals Penultimate. Alsof het creëren van een algoritme dat werkt niet hard genoeg is, moeten we er ook voor zorgen dat het algoritme goed presteert. Zoals we zullen zien, kan een naïeve tekeningimplementatie leiden tot een app met aanzienlijke prestatieproblemen die tekenen omslachtig maken en uiteindelijk onbruikbaar maken..


Ermee beginnen

Ik ga ervan uit dat je niet helemaal nieuw bent met iOS-ontwikkeling, dus heb ik de stappen van het maken van een nieuw project, het toevoegen van bestanden aan het project, enz. Overrompeld. Hopelijk is er hier niets te moeilijk, maar voor het geval dat, volledige projectcode is beschikbaar om te downloaden en mee te spelen.

Start een nieuw Xcode iPad-project op basis van de "Toepassing enkele weergave"Sjabloon en noem deze"FreehandDrawingTutZorg ervoor dat u automatische referentietelling (ARC) inschakelt, maar dat u de selectie van storyboards en unit-tests opheft. U kunt dit project een iPhone- of Universal-app maken, afhankelijk van welke apparaten u beschikbaar hebt om te testen.

Ga vervolgens door en selecteer het "FreeHandDrawingTut" -project in de Xcode Navigator en zorg ervoor dat alleen de portretoriëntatie wordt ondersteund:

Als je gaat deployen naar iOS 5.x of ouder, kun je de oriëntatiesteun op deze manier wijzigen:

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

Ik doe dit om de zaken eenvoudig te houden, zodat we ons kunnen concentreren op het belangrijkste probleem dat voorhanden is.

Ik wil onze code iteratief ontwikkelen en er stap voor stap verbeteringen in aanbrengen - zoals u dat in het begin zou doen als u helemaal opnieuw begon - in plaats van de definitieve versie in één keer te laten vallen. Ik hoop dat deze aanpak je een beter inzicht geeft in de verschillende problemen. Met dit in gedachten, en om te voorkomen dat ik herhaaldelijk code in hetzelfde bestand moet verwijderen, wijzigen en toevoegen, die rommelig en gevoelig voor fouten zou kunnen worden, zal ik de volgende aanpak volgen:

  • Voor elke iteratie maken we een nieuwe UIView-subklasse. Ik zal alle benodigde code publiceren, zodat je eenvoudig kunt kopiëren en plakken in het .m-bestand van de nieuwe UIView-subklasse die je maakt. Er zal geen publieke interface zijn naar de functionaliteit van de view-subklasse, wat betekent dat u het .h-bestand niet hoeft aan te raken.
  • Om elke nieuwe versie te testen, moeten we de UIView-subklasse die we hebben gemaakt toewijzen als de weergave die momenteel op het scherm wordt weergegeven. Ik zal je de eerste keer laten zien hoe je dat doet met Interface Builder, doorloop de stappen in detail en herinner je deze stap telkens als we een nieuwe versie coderen.

Eerste poging tot tekenen

Kies in Xcode Bestand> Nieuw> Bestand ... , kies de klasse Objective-C als sjabloon en geef op het volgende scherm het bestand een naam LinearInterpView en maak er een subklasse van UIView. Bewaar het. De naam "LinearInterp" is hier een afkorting voor "lineaire interpolatie". In het belang van de zelfstudie, noem ik elke UIView-subklasse die we maken om een ​​concept of aanpak te benadrukken die is geïntroduceerd in de klassencode.

Zoals ik eerder al zei, kunt u het header-bestand laten zoals het is. Verwijder allemaal de code in het bestand LinearInterpView.m en vervang deze door het volgende:

 #import "LinearInterpView.h" @implementation LinearInterpView UIBezierPath * path; // (3) - (id) initWithCoder: (NSCoder *) aDecoder // (1) if (self = [super initWithCoder: aDecoder]) [self setMultipleTouchEnabled: NO]; // (2) [self setBackgroundColor: [UIColor whiteColor]]; path = [UIBezierPath bezierPath]; [pad setLineWidth: 2.0];  terugkeer zelf;  - (void) drawRect: (CGRect) rect // (5) [[UIColor blackColor] setStroke]; [path stroke];  - (ongeldig) raaktBegan: (NSSet *) raakt aan metEvent: (UIEvent *) -gebeurtenis UITouch * touch = [raakt anyObject aan]; CGPoint p = [touch locationInView: self]; [pad verplaatsenToPoint: p];  - (void) raakt aanMoved: (NSSet *) raakt aan metEvent: (UIEvent *) -gebeurtenis UITouch * touch = [raakt anyObject aan]; CGPoint p = [touch locationInView: self]; [pad addLineToPoint: p]; // (4) [zelfsetNeedsDisplay];  - (ongeldig) raakt aanEnded: (NSSet *) raakt aan metEvent: (UIEvent *) -gebeurtenis [self touchesMoved: raakt aan metEvent: event];  - (ongeldige) aanrakingenGecannuleerd: (NSSet *) raakt aan metEvent: (UIEvent *) -gebeurtenis [self touchesEnded: touches withEvent: event];  @end

In deze code werken we rechtstreeks met de aanraakgebeurtenissen die de toepassing ons meldt telkens wanneer we een aanraakvolgorde hebben. dat wil zeggen, de gebruiker plaatst een vinger op het scherm, beweegt er met de vinger overheen en tenslotte tilt hij zijn vinger van het scherm. Voor elke gebeurtenis in deze reeks stuurt de applicatie ons een overeenkomstig bericht (in iOS-terminologie worden de berichten naar de "first responder" verzonden; u kunt de documentatie raadplegen voor meer informatie).

Om deze berichten te verwerken, implementeren we de methoden -touchesBegan: withEvent: en bedrijf, die worden gedeclareerd in de UIResponderklasse waarvan UIView erft. We kunnen code schrijven om de aanraakgebeurtenissen te behandelen op elke manier die we willen. In onze app willen we de locatie van de aanrakingen op het scherm opvragen, een beetje verwerken en vervolgens lijnen op het scherm tekenen.

De punten verwijzen naar de corresponderende opmerkingen uit de bovenstaande code:

  1. We negeren -initWithCoder: omdat het beeld is geboren uit een XIB, zoals we binnenkort zullen instellen.
  2. We hebben meerdere aanrakingen uitgeschakeld: we gaan slechts één aanraakvolgorde aan, wat betekent dat de gebruiker slechts met één vinger tegelijk kan tekenen; elke andere vinger die gedurende die tijd op het scherm wordt geplaatst, zal worden genegeerd. Dit is een vereenvoudiging, maar niet per se een onredelijke - mensen schrijven gewoonlijk ook niet op papier met twee pennen tegelijk! Het zorgt er in elk geval voor dat we niet te ver afdwalen, want we hebben al genoeg werk te doen.
  3. De UIBezierPath is een UIKit-klasse waarmee we vormen op het scherm kunnen tekenen die zijn samengesteld uit rechte lijnen of bepaalde typen curven.
  4. Aangezien we een aangepaste tekening maken, moeten we de weergave overschrijven -drawRect: methode. We doen dit door het pad te aaien telkens wanneer een nieuw lijnsegment wordt toegevoegd.
  5. Merk ook op dat terwijl de lijndikte een eigenschap van het pad is, de kleur van de lijn zelf een eigenschap is van de tekencontext. Als u onbekend bent met grafische contexten, kunt u erover lezen in de Apple-documenten. Denk voor nu aan een grafische context als een "canvas" waar je naar toe trekt wanneer je de -drawRect: methode, en het resultaat van wat u ziet, is de weergave op het scherm. We komen binnenkort een ander soort tekencontext tegen.

Voordat we de toepassing kunnen bouwen, moeten we de subklasse die we zojuist hebben gemaakt instellen op de weergave op het scherm.

  1. Klik in het navigatievenster op ViewController.xib (in het geval u een universele app hebt gemaakt, voert u deze stap eenvoudig uit voor beide ViewController ~ iPhone.xib en ViewController ~ iPad.xib -bestanden).
  2. Wanneer de weergave wordt weergegeven op het canvas van de interfacebuilder, klikt u erop om het te selecteren. Klik in het deelvenster met hulpprogramma's op 'Identiteitencontrole' (derde knop van rechts boven in het deelvenster). Het bovenste gedeelte zegt "Aangepaste klasse", hier kunt u de klasse instellen van de weergave waarop u hebt geklikt.
  3. Op dit moment zou het "UIView" moeten zijn, maar we moeten het veranderen naar (je raadt het al) LinearInterpView. Type de naam van de klasse in (door simpelweg "L" in te tikken, zou autocomplete geruststellend moeten klinken).
  4. Nogmaals, als u dit gaat testen als een universele app, herhaalt u deze exacte stap voor beide XIB-bestanden die de sjabloon voor u heeft gemaakt.

Bouw nu de applicatie. Je zou een glanzend wit beeld moeten krijgen dat je met je vinger kunt tekenen. Gezien de paar regels code die we hebben geschreven, zijn de resultaten niet al te arm! Natuurlijk zijn ze ook niet spectaculair. Het connect-the-dots uiterlijk is nogal merkbaar (en ja, mijn handschrift zuigt ook).

Zorg ervoor dat u de app niet alleen op de simulator uitvoert, maar ook op een echt apparaat.

Als je een tijdje met de app op je apparaat speelt, zul je zeker iets merken: uiteindelijk begint de UI-reactie te vertragen, en in plaats van de ~ 60 aanraakpunten die per seconde werden verkregen, om wat voor reden dan ook het aantal punten de gebruikersinterface kan druppels steeds verder bemonsteren. Aangezien de punten verder uit elkaar raken, maakt de interpolatie van de rechte lijn de tekening zelfs "blockier" dan voorheen. Dit is zeker onwenselijk. Dus wat is er aan de hand?


Prestaties en reactievermogen behouden

Laten we eens bekijken wat we aan het doen zijn: terwijl we tekenen, krijgen we punten, voegen ze toe aan een steeds groter wordend pad en renden dan het * complete * pad in elke cyclus van de hoofdlus. Dus naarmate het pad langer wordt, heeft het tekeningssysteem in elke iteratie meer te tekenen en uiteindelijk wordt het te veel, waardoor het moeilijk wordt voor de app om bij te blijven. Aangezien alles gebeurt op de hoofddraad, concurreert onze tekencode met de UI-code die, onder andere, de aanrakingen op het scherm moet bemonsteren.

Het zou je vergeven zijn te denken dat er een manier was om 'bovenop' te tekenen wat al op het scherm was; helaas is dit waar we ons moeten losmaken van de pen-op-papier-analogie, omdat het grafische systeem standaard niet zo werkt. Hoewel we, op basis van de code die we nu gaan schrijven, indirect de "draw-on-top" -aanpak zullen implementeren.

Hoewel we een paar dingen proberen om de prestaties van onze code te verbeteren, zullen we slechts één idee implementeren, omdat het voldoende blijkt te zijn voor onze huidige behoeften.

Maak een nieuwe UIView-subklasse zoals u eerder deed en noem deze CachedLIView (de LI is om ons eraan te herinneren dat we het nog steeds doen LInEar iknterpolation). Verwijder alle inhoud van CachedLIView.m en vervang het door het volgende:

 #import "CachedLIView.h" @implementation CachedLIView UIBezierPath * path; UIImage * incrementalImage; // (1) - (id) initWithCoder: (NSCoder *) aDecoder if (self = [super initWithCoder: aDecoder]) [self setMultipleTouchEnabled: NO]; [self setBackgroundColor: [UIColor whiteColor]]; path = [UIBezierPath bezierPath]; [pad setLineWidth: 2.0];  terugkeer zelf;  - (void) drawRect: (CGRect) rect [incrementalImage drawInRect: rect]; // (3) [wegbeweging];  - (ongeldig) raaktBegan: (NSSet *) raakt aan metEvent: (UIEvent *) -gebeurtenis UITouch * touch = [raakt anyObject aan]; CGPoint p = [touch locationInView: self]; [pad verplaatsenToPoint: p];  - (void) raakt aanMoved: (NSSet *) raakt aan metEvent: (UIEvent *) -gebeurtenis UITouch * touch = [raakt anyObject aan]; CGPoint p = [touch locationInView: self]; [pad addLineToPoint: p]; [zelfsetNeedsDisplay];  - (void) touchesEnded: (NSSet *) raakt aan metEvent: (UIEvent *) -gebeurtenis // (2) UITouch * touch = [raakt anyObject aan]; CGPoint p = [touch locationInView: self]; [pad addLineToPoint: p]; [zelf drawBitmap]; // (3) [zelfsetNeedsDisplay]; [pad removeAllPoints]; // (4) - (ongeldige) aanrakingenGecannuleerd: (NSSet *) raakt aan metEvent: (UIEvent *) -gebeurtenis [self touchesEnded: touches withEvent: event];  - (void) drawBitmap // (3) UIGraphicsBeginImageContextWithOptions (self.bounds.size, YES, 0.0); [[UIColor blackColor] setStroke]; if (! incrementalImage) // eerste trekking; verf achtergrond wit door ... UIBezierPath * rectpath = [UIBezierPath bezierPathWithRect: self.bounds]; // bitmap insluiten door een rechthoek gedefinieerd door een ander UIBezierPath-object [[UIColor whiteColor] setFill]; [rectpath fill]; // vullen met wit [incremental Image drawPoint: CGPointZero]; [path stroke]; incrementalImage = UIGraphicsGetImageFromCurrentImageContext (); UIGraphicsEndImageContext ();  @end

Vergeet na het opslaan niet om de klasse van het view-object in uw XIB ('s) in CachedLIView te veranderen!

Wanneer de gebruiker zijn vinger op het scherm legt om te tekenen, beginnen we met een nieuw pad zonder punten of lijnen, en we voegen lijnsegmenten eraan toe net zoals we eerder deden.

Nogmaals, verwijzend naar de cijfers in de reacties:

  1. We onderhouden bovendien een (offscreen) bitmapafbeelding van hetzelfde formaat als ons canvas (dus op het scherm) in het geheugen, waarin we kunnen opslaan wat we tot nu toe hebben getekend.
  2. We tekenen de inhoud op het scherm in deze buffer telkens wanneer de gebruiker zijn vinger opheft (gesignaleerd door -touchesEnded: WithEvent).
  3. De drawBitmap-methode maakt een bitmapcontext - UIKit-methoden hebben een "huidige context" (een canvas) nodig om in te tekenen. Wanneer we binnen zijn -drawRect: deze context wordt automatisch aan ons beschikbaar gesteld en geeft weer wat we naar ons onzichtbare scherm trekken. Daarentegen moet de bitmapcontext expliciet worden gemaakt en vernietigd en bevindt de getekende inhoud zich in het geheugen.
  4. Door de vorige tekening op deze manier in cache te plaatsen, kunnen we de vorige inhoud van het pad verwijderen en op deze manier voorkomen dat het pad te lang wordt.
  5. Nu elke keer drawRect: wordt genoemd, we tekenen eerst de inhoud van de geheugenbuffer naar onze mening die (door ontwerp) exact dezelfde grootte heeft, en dus behouden we voor de gebruiker de illusie van continu tekenen, alleen op een andere manier dan voorheen.

Hoewel dit niet perfect is (wat als onze gebruiker blijft tekenen zonder ooit zijn vinger op te steken?), Is het goed genoeg voor de reikwijdte van deze zelfstudie. U wordt aangemoedigd om zelf te experimenteren om een ​​betere methode te vinden. U kunt bijvoorbeeld proberen de tekening periodiek in de cache op te slaan in plaats van alleen wanneer de gebruiker zijn vinger ophaalt. Het is namelijk zo dat deze off-screen caching-procedure ons de mogelijkheid biedt om op de achtergrond te verwerken, mochten we ervoor kiezen om het te implementeren. Maar dat gaan we niet doen in deze tutorial. U bent echter van harte uitgenodigd om het zelf te proberen!


Verbetering van de visuele slagkwaliteit

Laten we nu onze aandacht richten op het "er beter uitzien" van de tekening. Tot nu toe hebben we aangrenzende aanraakpunten met rechte lijnsegmenten verbonden. Maar normaal gesproken, wanneer we uit de vrije hand tekenen, heeft onze natuurlijke streek een vrij stromende en bochtige (in plaats van een blokachtige en stijve) uitstraling. Het is logisch dat we onze punten proberen te interpoleren met curves in plaats van lijnsegmenten. Gelukkig laat de UIBezierPath-klasse ons zijn naamgenoot tekenen: Bezier-curven.

Wat zijn Bezier-curven? Zonder een beroep te doen op de wiskundige definitie, wordt een Bézier-curve gedefinieerd door vier punten: twee eindpunten waardoor een curve passeert en twee 'controlepunten' die helpen bij het definiëren van tangenten die de curve moet aanraken bij zijn eindpunten (dit is technisch een kubieke Bezier-curve, maar voor de eenvoud zal ik het gewoon een "Bezier-curve" noemen).

Met Bezier-curven kunnen we allerlei interessante vormen tekenen.

Wat we nu gaan proberen, is om reeksen van vier aangrenzende aanraakpunten te groeperen en de puntsequentie in een Bezier-curvesegment te interpoleren. Elk aangrenzend paar Bezier-segmenten delen een gemeenschappelijk eindpunt om de continuïteit van de streek te behouden.

Je kent de boor nu wel. Maak een nieuwe UIView-subklasse en geef deze een naam BezierInterpView. Plak de volgende code in het .m-bestand:

 #import "BezierInterpView.h" @implementation BezierInterpView UIBezierPath * path; UIImage * incrementalImage; CGPoint-punten [4]; // om de vier punten van ons Beint-segment uint ctr bij te houden; // een tellervariabele om de puntindex bij te houden - (id) initWithCoder: (NSCoder *) aDecoder if (self = [super initWithCoder: aDecoder]) [self setMultipleTouchEnabled: NO]; [self setBackgroundColor: [UIColor whiteColor]]; path = [UIBezierPath bezierPath]; [pad setLineWidth: 2.0];  terugkeer zelf;  - (void) drawRect: (CGRect) rect [incrementalImage drawInRect: rect]; [path stroke];  - (ongeldig) raaktBegan: (NSSet *) raakt aan metEvent: (UIEvent *) -gebeurtenis ctr = 0; UITouch * touch = [raakt anyObject aan]; pts [0] = [touch locationInView: self];  - (void) raakt aanMoved: (NSSet *) raakt aan metEvent: (UIEvent *) -gebeurtenis UITouch * touch = [raakt anyObject aan]; CGPoint p = [touch locationInView: self]; ctr ++; pts [ctr] = p; if (ctr == 3) // 4e punt [pad verplaatsenToPoint: pts [0]]; [pad addCurveToPoint: pts [3] controlPoint1: pts [1] controlPoint2: pts [2]]; // dit is hoe een Bezier-curve aan een pad wordt toegevoegd. We voegen een kubieke Bezier toe van pt [0] naar pt [3], met controlepunten pt [1] en pt [2] [self setNeedsDisplay]; pts [0] = [pad currentPoint]; ctr = 0;  - (ongeldig) raakt aanEnded: (NSSet *) raakt aan metEvent: (UIEvent *) event [self drawBitmap]; [zelfsetNeedsDisplay]; pts [0] = [pad currentPoint]; // laat het tweede eindpunt van het huidige Bezier-segment het eerste zijn van het volgende Bezier-segment [pad removeAllPoints]; ctr = 0;  - (ongeldige) aanrakingenGecannuleerd: (NSSet *) raakt aan metEvent: (UIEvent *) -gebeurtenis [self touchesEnded: touches withEvent: event];  - (void) drawBitmap UIGraphicsBeginImageContextWithOptions (self.bounds.size, YES, 0.0); [[UIColor blackColor] setStroke]; if (! incrementalImage) // eerste keer; verf achtergrond wit UIBezierPath * rectpath = [UIBezierPath bezierPathWithRect: self.bounds]; [[UIColor whiteColor] setFill]; [rectpath fill];  [incrementalImage drawAtPoint: CGPointZero]; [path stroke]; incrementalImage = UIGraphicsGetImageFromCurrentImageContext (); UIGraphicsEndImageContext ();  @end

Zoals de inline opmerkingen aangeven, is de belangrijkste verandering de introductie van een aantal nieuwe variabelen om de punten in onze Bezier-segmenten bij te houden, en een aanpassing van de -(Void) touchesMoved: withEvent: methode om een ​​Bezier-segment te tekenen voor elke vier punten (eigenlijk om de drie punten, in termen van de aanrakingen die de app ons rapporteert, omdat we één eindpunt delen voor elk paar aangrenzende Bézier-segmenten).

U kunt er hier op wijzen dat we het geval hebben verwaarloosd van het feit dat de gebruiker zijn vinger opheft en de aanraakvolgorde beëindigt voordat we genoeg punten hebben om ons laatste Bezier-segment te voltooien. Als dat zo is, hebt u gelijk! Hoewel dit visueel gezien niet veel uitmaakt, doet het dat in bepaalde belangrijke gevallen wel. Probeer bijvoorbeeld een kleine cirkel te tekenen. Het sluit misschien niet helemaal, en in een echte app zou je dit op de juiste manier willen aanpakken in de -touchesEnded: withEvent methode. Terwijl we bezig zijn, hebben we ook geen speciale aandacht geschonken aan het geval van een annulering van de aanraakbediening. De touchesCancelled: withEvent instantie methode verwerkt dit. Bekijk de officiële documentatie en kijk of er speciale gevallen zijn die u hier misschien moet behandelen.

Hoe zien de resultaten eruit? Nogmaals, ik herinner u eraan om de juiste klasse in de XIB in te stellen voordat u gaat bouwen.

Huh. Het lijkt niet veel verbetering, of wel? Ik denk dat het misschien zo is licht beter dan interpolatie met een rechte lijn, of misschien is dat gewoon wishful thinking. In elk geval is het niets om over op te scheppen.


Verdere verbetering van de slagkwaliteit

Dit is wat ik denk dat er gebeurt: terwijl we de moeite nemen om elke reeks van vier punten te interpoleren met een glad curve-segment, we doen geen moeite om een ​​curvesegment soepel over te laten schakelen naar de volgende, zo effectief dat we nog steeds een probleem hebben met het eindresultaat.

Wat kunnen we eraan doen? Als we ons houden aan de aanpak die we in de laatste versie hebben gestart (dat wil zeggen met Bezier-curven), moeten we zorgen voor de continuïteit en vloeiendheid op het 'knooppunt' van twee aangrenzende Bezier-segmenten. De twee raaklijnen aan het eindpunt met de bijbehorende bedieningspunten (tweede besturingspunt van het eerste segment en eerste besturingspunt van het tweede segment) lijken de sleutel te zijn; als beide raaklijnen dezelfde richting hadden, dan zou de curve vloeiender zijn op de kruising.

Wat als we het gemeenschappelijke eindpunt ergens op de lijn van de twee controlepunten verplaatsen? Zonder gebruik te maken van aanvullende gegevens over de aanrakingspunten lijkt het beste punt het middelpunt te zijn van de lijn tussen de twee controlepunten in overweging, en zou aan onze opgelegde eis voor de richting van de twee raaklijnen worden voldaan. Laten we dit proberen!

Maak een UIView-subklasse (nog een keer) en noem deze SmoothedBIView. Vervang de volledige code in het .m-bestand door het volgende:

 #import "SmoothedBIView.h" @implementation SmoothedBIView UIBezierPath * path; UIImage * incrementalImage; CGPoint-punten [5]; // we moeten nu de vier punten van een Bezier-segment en het eerste besturingspunt van het volgende segment uint ctr bijhouden;  - (id) initWithCoder: (NSCoder *) aDecoder if (self = [super initWithCoder: aDecoder]) [self setMultipleTouchEnabled: NO]; [self setBackgroundColor: [UIColor whiteColor]]; path = [UIBezierPath bezierPath]; [pad setLineWidth: 2.0];  terugkeer zelf;  - (id) initWithFrame: (CGRect) -frame self = [super initWithFrame: frame]; if (self) [self setMultipleTouchEnabled: NO]; path = [UIBezierPath bezierPath]; [pad setLineWidth: 2.0];  terugkeer zelf;  // Alleen overschrijven drawRect: als u een aangepaste tekening uitvoert. // Een lege implementatie beïnvloedt de prestaties tijdens de animatie nadelig. - (void) drawRect: (CGRect) rect [incrementalImage drawInRect: rect]; [path stroke];  - (ongeldig) raaktBegan: (NSSet *) raakt aan metEvent: (UIEvent *) -gebeurtenis ctr = 0; UITouch * touch = [raakt anyObject aan]; pts [0] = [touch locationInView: self];  - (void) raakt aanMoved: (NSSet *) raakt aan metEvent: (UIEvent *) -gebeurtenis UITouch * touch = [raakt anyObject aan]; 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 ); // verplaats het eindpunt naar het midden van de lijn die samenkomt met het tweede besturingspunt van het eerste Bezier-segment en het eerste besturingspunt van het tweede Bezier-segment [pad verplaatsenToPoint: pts [0]]; [pad addCurveToPoint: pts [3] controlPoint1: pts [1] controlPoint2: pts [2]]; // voeg een kubieke Bezier toe van pt [0] naar pt [3], met controlepunten pt [1] en pt [2] [self setNeedsDisplay]; // vervang punten en bereid je voor op het volgende segment pts [0] = pts [3]; pts [1] = pts [4]; ctr = 1;  - (ongeldig) raakt aanEnded: (NSSet *) raakt aan metEvent: (UIEvent *) event [self drawBitmap]; [zelfsetNeedsDisplay]; [pad removeAllPoints]; ctr = 0;  - (ongeldige) aanrakingenGecannuleerd: (NSSet *) raakt aan metEvent: (UIEvent *) -gebeurtenis [self touchesEnded: touches withEvent: event];  - (void) drawBitmap UIGraphicsBeginImageContextWithOptions (self.bounds.size, YES, 0.0); if (! incrementalImage) // eerste keer; verf achtergrond wit UIBezierPath * rectpath = [UIBezierPath bezierPathWithRect: self.bounds]; [[UIColor whiteColor] setFill]; [rectpath fill];  [incrementalImage drawAtPoint: CGPointZero]; [[UIColor blackColor] setStroke]; [path stroke]; incrementalImage = UIGraphicsGetImageFromCurrentImageContext (); UIGraphicsEndImageContext ();  @end

De essentie van het algoritme dat we hierboven hebben besproken, is geïmplementeerd in de -touchesMoved: withEvent: methode. De inline opmerkingen moeten u helpen de discussie te koppelen aan de code.

Dus, hoe zijn de resultaten, visueel gesproken? Vergeet niet om het ding met de XIB te doen.

Gelukkig is er deze keer een substantiële verbetering. Gezien de eenvoud van onze aanpassing, ziet het er best goed uit (als ik het zelf zeg!). Onze analyse van het probleem met de vorige iteratie en onze voorgestelde oplossing is ook gevalideerd.


Waar te gaan vanaf hier

Ik hoop dat je deze tutorial nuttig hebt gevonden. Hopelijk ontwikkel je je eigen ideeën over het verbeteren van de code. Een van de belangrijkste (maar gemakkelijke) verbeteringen die u kunt aanbrengen, is het eleganter hanteren van het einde van de aanraakvolgorde, zoals eerder besproken.

Een ander geval dat ik verwaarloosde, is het hanteren van een aanraakvolgorde die bestaat uit het feit dat de gebruiker het beeld met zijn vinger aanraakt en het vervolgens optilt zonder het te verplaatsen - effectief een tik op het scherm. De gebruiker verwacht waarschijnlijk op deze manier een punt of een klein gekreukeltje te tekenen, maar met onze huidige implementatie gebeurt er niets omdat onze tekencode niet wordt geactiveerd tenzij ons beeld de -touchesMoved: withEvent: bericht. Misschien wilt u eens kijken naar de UIBezierPath klassedocumentatie om te zien welke andere soorten paden je kunt bouwen.

Als uw app meer werk doet dan wat we hier hebben gedaan (en in een teken-app die de verzendkosten waard is, zou het!), Zodanig ontwerpen dat de niet-UI-code (met name de off-screen caching) in een achtergrondthread wordt uitgevoerd, aanzienlijk verschil maken op een multicore-apparaat (vanaf iPad 2). Zelfs op een apparaat met één processor, zoals de iPhone 4, zou de performance moeten verbeteren, omdat ik verwacht dat de processor het caching-werk zou delen, wat per slot van rekening maar eenmaal in de paar cycli van de hoofdlus gebeurt.

Ik moedig je aan om je coderingsspieren te buigen en te spelen met de UIKit API om enkele van de ideeën die in deze tutorial zijn geïmplementeerd te ontwikkelen en te verbeteren. Veel plezier en bedankt voor het lezen!