Smooth Freehand Desen pe iOS

Acest tutorial vă va învăța cum să implementați un algoritm de desenare avansată pentru desenarea netedă și liberă pe dispozitive iOS. Citește mai departe!

Prezentare generală teoretică

Atingerea este modul principal în care un utilizator va interacționa cu dispozitivele iOS. Una dintre funcționalitățile cele mai naturale și evidente pe care aceste dispozitive le furnizează este aceea de a permite utilizatorului să deseneze pe ecran cu degetul. Există numeroase aplicații de desenare și preluare de note în prezent în App Store, iar multe companii solicită chiar clienților să semneze un iDevice atunci când efectuează achiziții. Cum funcționează aceste aplicații? Să ne oprim și să gândim un minut despre ce se întâmplă "sub capotă".

Când un utilizator derulează o vizualizare de tabel, se ciocănește pentru a mări o imagine sau trage o curbă într-o aplicație de pictură, afișajul dispozitivului se actualizează rapid (de ex., De 60 de ori pe secundă) și ciclul de rulare a aplicației este în mod constant eșantionare locația degetului utilizatorului (utilizatorilor). În timpul acestui proces, intrarea "analogică" a unui deget care trage pe ecran trebuie convertită într-un set digital de puncte de pe ecran, iar acest proces de conversie poate reprezenta provocări semnificative. În contextul aplicației noastre de pictură, avem o problemă de "fixare a datelor" pe mâinile noastre. Pe măsură ce utilizatorul scarpină în mod vesel dispozitivul, programatorul trebuie să interpoleze în mod esențial informațiile anonime ("connect-the-dots") care lipsesc între punctele de contact eșantionate pe care iOS le-a raportat. Mai mult, această interpolare trebuie să aibă loc astfel încât rezultatul să fie un accident vascular cerebral care apare continuu, natural și neted pentru utilizatorul final, ca și cum el ar fi schițat cu un stilou pe un registru de hârtie.

Scopul acestui tutorial este de a arăta cum poate fi implementată desenul liber pe iOS, pornind de la un algoritm de bază care efectuează interpolarea liniară dreaptă și avansează într-un algoritm mai sofisticat care abordează calitatea oferită de aplicații bine cunoscute precum Penultimate. Ca și cum crearea unui algoritm care funcționează nu este suficient de greu, trebuie să ne asigurăm că algoritmul funcționează bine. După cum vom vedea, o implementare naivă a desenului poate duce la o aplicație cu probleme semnificative de performanță, care va face ca desenarea să fie greoaie și eventual inutilizabilă.


Noțiuni de bază

Presupun că nu ești absolut nou în dezvoltarea iOS, așa că am depășit etapele de creare a unui nou proiect, adăugarea de fișiere la proiect etc. Sperăm că nu e nimic prea greu aici, oricum, dar, codul de proiect complet este disponibil pentru descărcare și redare cu dvs..

Începeți un nou proiect Xcode iPad bazat pe "Vizualizare individuală"Șablon și numele"FreehandDrawingTut"Asigurați-vă că activați Numărătoarea automată a referințelor (ARC), dar deselectați Storyboards și testele unității. Puteți face acest proiect fie ca iPhone, fie ca aplicație universală, în funcție de dispozitivele pe care le aveți disponibile pentru testare.

Apoi, continuați și selectați proiectul "FreeHandDrawingTut" din Xcode Navigator și asigurați-vă că este acceptată numai orientarea portretului:

Dacă intenționați să implementați iOS 5.x sau o versiune anterioară, puteți modifica suportul de orientare astfel:

 - (BOOL) ar trebui să autorizezeTointerfaceOrientation: (UIInterfaceOrientation) interfaceOrientation return (interfaceOrientation == UIInterfaceOrientationPortrait); 

Fac asta pentru a păstra lucrurile simple, astfel încât să ne concentrăm asupra problemei principale la îndemână.

Vreau să dezvolt codul în mod iterativ, îmbunătățind-l într-o manieră incrementală - așa cum ați face în mod realist dacă ați porni de la zero - în loc să renunțați la versiunea finală asupra dvs. Sper că această abordare vă va oferi o mai bună manevră asupra diferitelor probleme implicate. Ținând cont de acest lucru, și pentru a salva de la a trebui să ștergeți în mod repetat, să modificați și să adăugați cod în același fișier, care ar putea deveni dezordonat și predispus la erori, voi lua următoarea abordare:

  • Pentru fiecare iterație, vom crea o nouă subclasă UIView. Voi posta tot codul necesar, astfel încât să puteți copia și să lipiți pur și simplu în fișierul .m din noua subclasă UIView pe care o creați. Nu va exista o interfață publică pentru funcția de subclasă de vizualizare, ceea ce înseamnă că nu va trebui să atingeți fișierul .h.
  • Pentru a testa fiecare versiune nouă, va trebui să alocăm subclasa UIView pe care am creat-o pentru a fi afișarea curentă a ecranului. Vă voi arăta cum să faceți acest lucru cu Interface Builder pentru prima dată, trecând prin pași în detaliu, și apoi vă reamintesc acest pas de fiecare dată când codificăm o nouă versiune.

Primul încercare la desen

În Xcode, alegeți Fișier> Nou> Fișier ... , alegeți clasa Obiectiv-C ca șablon, iar în ecranul următor numele fișierului LinearInterpView și să o facă o subclasă de UIView. Salvați-l. Numele "LinearInterp" este scurt pentru "interpolarea liniară" aici. De dragul tutorialului, voi numi fiecare subclasă UIView pe care o creăm pentru a sublinia un concept sau o abordare introdusă în cadrul codului de clasă.

După cum am menționat anterior, puteți lăsa fișierul antet așa cum este. Șterge toate codul prezent în fișierul LinearInterpView.m și îl înlocuiți cu următorul text:

 #import "LinearInterpView.h" @implementare LinearInterpView cale UIBezierPath *; // (3) - (id) initWithCoder: (NSCoder *) aDecoder // (1) if (self = [super initWithCoder: aDecoder]) [self setMultipleTouchEnabled: NO]; // (2) [auto setBackgroundColor: [UICcolor whiteColor]]; cale = [UIBezierPath bezierPath]; [cale setLineWidth: 2.0];  întoarce-te;  - (void) drawRect: (CGRect) rect // (5) [[UICcolor negruColor] setStroke]; [accident vascular cerebral];  - (void) atingeBegan: (NSSet *) atinge cu EventEvent: (UIEvent *) eveniment UITouch * touch = [atinge anyObject]; CGPoint p = [atingeți locațiaInView: auto]; [calea mutaToPoint: p];  - (void) atingeModed: (NSSet *) atinge cu EventEvent: (UIEvent *) eveniment UITouch * touch = [atinge anyObject]; CGPoint p = [atingeți locațiaInView: auto]; [cale addLineToPoint: p]; // (4) [auto setNeedsDisplay];  - (void) atingeEnded: (NSSet *) atinge cu EventEvent: (UIEvent *) eveniment [atinge de sineModed: atinge cuEvent: eveniment];  - (void) touchesCancelled: (NSSet *) atinge cu EventEvent: (UIEvent *) eveniment [touch touchesEnded: touches withEvent: event];  @Sfârșit

În acest cod, lucrăm direct cu evenimentele touch pe care aplicația ne raportează de fiecare dată când avem o secvență de atingere. adică utilizatorul pune un deget pe ecranul de pe ecran, mișcă degetul peste el și, în final, ridică degetul de pe ecran. Pentru fiecare eveniment din această secvență, aplicația ne trimite un mesaj corespunzător (în terminologia iOS, mesajele sunt trimise la "primul răspuns", puteți consulta documentația pentru detalii).

Pentru a face față acestor mesaje, implementăm metodele -touchesBegan: WithEvent: și companie, care sunt declarate în clasa UIResponder din care UIView moștenește. Putem scrie coduri pentru a face față evenimentelor de atingere indiferent de modul în care ne place. În aplicația noastră vrem să interogăm locația pe ecran a atingerilor, să facem o prelucrare și apoi să tragem linii pe ecran.

Punctele se referă la numerele corespunzătoare din codul de mai sus:

  1. Ne suprasolicităm -initWithCoder: deoarece vederea se naște dintr-un XIB, așa cum vom înființa în curând.
  2. Am dezactivat mai multe atingeri: vom aborda doar o singură secvență de atingere, ceea ce înseamnă că utilizatorul poate desena doar cu un deget la un moment dat; orice alt deget plasat pe ecran în timpul respectiv va fi ignorat. Aceasta este o simplificare, dar nu neapărat una nerezonabilă - oamenii nu scriu de obicei pe hârtie cu două stilouri la un moment dat! În orice caz, ne va împiedica să ne îndepărtăm prea mult, deoarece avem destulă muncă pentru a face deja.
  3. UIBezierPath este o clasă UIKit care ne permite să desenăm forme pe ecran compuse din linii drepte sau anumite tipuri de curbe.
  4. Deoarece facem desene personalizate, trebuie să ignorăm vizualizarea -drawRect: metodă. Facem acest lucru prin străpungerea căii de fiecare dată când se adaugă un nou segment de linie.
  5. Rețineți că, deși lățimea liniei este o proprietate a căii, culoarea liniei în sine este o proprietate a contextului de desen. Dacă nu sunteți familiarizați cu contextele grafice, puteți citi despre ele în documentele Apple. Deocamdată, gândiți-vă la un context grafic ca pe o "pânză" pe care o atrageți atunci când o înlocuiți -drawRect: și rezultatul a ceea ce vedeți este afișarea pe ecran. În curând vom vedea un alt context de desen.

Înainte de a putea să construim aplicația, trebuie să setăm subclasa de vizualizare pe care tocmai am creat-o în vizualizarea pe ecran.

  1. În panoul de navigare, faceți clic pe ViewController.xib (în cazul în care ați creat o aplicație universală, pur și simplu efectuați acest pas pentru ambele ViewController ~ iPhone.xib și ViewController ~ iPad.xib fișiere).
  2. Când vizualizarea apare pe panza constructorului de interfețe, faceți clic pe acesta pentru ao selecta. În panoul de utilități, faceți clic pe "Identities Inspector" (al treilea buton din partea dreaptă din partea de sus a panoului). Secțiunea din partea de sus indică "Clasa personalizată", aici veți seta clasa vizualizării pe care ați făcut clic.
  3. Acum ar trebui să spui "UIView", dar trebuie să o schimbăm (ați ghicit-o) LinearInterpView. Introduceți numele clasei (simpla tastare a literei "L" ar trebui să determine autocompletarea să cronometreze).
  4. Din nou, dacă veți încerca acest lucru ca o aplicație universală, repetați acest pas exact pentru ambele fișiere XIB pe care șablonul le-a făcut pentru dvs..

Acum, construiți aplicația. Ar trebui să obțineți o vedere alb strălucitoare pe care să o puteți trage cu degetul. Având în vedere cele câteva linii de cod pe care le-am scris, rezultatele nu sunt prea ciudate! Desigur, nici ele nu sunt spectaculoase. Aspectul de conectare a punctelor este destul de vizibil (și da, scrisul meu de mână este de asemenea prea prost).

Asigurați-vă că rulați aplicația nu numai pe simulator, ci și pe un dispozitiv real.

Dacă jucați cu aplicația pentru o vreme pe dispozitivul dvs., sunteți obligat să observați ceva: în cele din urmă răspunsul UI începe să rămână în urmă și în loc de ~ 60 de puncte de atingere care au fost achiziționate pe secundă, dintr-un anumit motiv, numărul de puncte UI este capabil să eșantioneze mai mult și mai mult. Deoarece punctele devin din ce în ce mai îndepărtate, interpolarea liniară face desenul chiar mai "blocant" decât înainte. Acest lucru este cu siguranță nedorit. Deci ce se întâmplă?


Menținerea performanței și a receptivității

Să examinăm ce am făcut: pe măsură ce tragem, obținem puncte, le adăugăm pe o cale tot mai în creștere și apoi redăm calea * completă * în fiecare ciclu al bucla principală. Deci, pe măsură ce calea devine mai lungă, în fiecare iterație sistemul de desenare are mai mult de tras și în cele din urmă devine prea mult, ceea ce face dificilă menținerea aplicației. Deoarece totul se întâmplă pe firul principal, codul de desen este în concurență cu codul UI care, printre altele, trebuie să probeze atingerile de pe ecran.

Ați fi iertat că credeți că există o modalitate de a atrage "pe partea de sus" ceea ce era deja pe ecran; din nefericire, aici trebuie să ne despărțim de analogia creionului pe suport de hârtie, deoarece sistemul grafic nu funcționează așa în mod implicit. Deși, în virtutea codului pe care urmează să îl scriem în continuare, vom implementa în mod indirect abordarea "remiză pe partea de sus".

În timp ce există câteva lucruri pe care am putea încerca să le rezolvăm în ceea ce privește performanța codului nostru, vom implementa doar o idee, deoarece se dovedește a fi suficientă pentru nevoile noastre actuale.

Creați o nouă subclasă UIView ca în trecut, denumiți-o CachedLIView (LI este să ne reamintească că încă mai facem Lîn ureche eunterpolation). Ștergeți tot conținutul CachedLIView.m și înlocuiți-l cu următoarele:

 #import "CachedLIView.h" @implementation CachedLIView cale UIBezierPath *; UIImage * incrementalImage; // (1) - (id) initWithCoder: (NSCoder *) aDecoder if (self = [super initWithCoder: aDecoder]) [self setMultipleTouchEnabled: NO]; [auto setBackgroundColor: [UICcolor whiteColor]]; cale = [UIBezierPath bezierPath]; [cale setLineWidth: 2.0];  întoarce-te;  - (void) drawRect: (CGRect) rect [incrementalImage drawInRect: rect]; // (3) [cursa căii];  - (void) atingeBegan: (NSSet *) atinge cu EventEvent: (UIEvent *) eveniment UITouch * touch = [atinge anyObject]; CGPoint p = [atingeți locațiaInView: auto]; [calea mutaToPoint: p];  - (void) atingeModed: (NSSet *) atinge cu EventEvent: (UIEvent *) eveniment UITouch * touch = [atinge anyObject]; CGPoint p = [atingeți locațiaInView: auto]; [cale addLineToPoint: p]; [self setNeedsDisplay];  - (void) atingeEnded: (NSSet *) atinge cu EventEvent: (UIEvent *) eveniment // (2) UITouch * touch = [atinge anyObject]; CGPoint p = [atingeți locațiaInView: auto]; [cale addLineToPoint: p]; [auto drawBitmap]; // (3) [auto setNeedsDisplay]; [path removeAllPoints]; // (4) - (void) touchesCancelled: (NSSet *) atinge cu EventEvent: (UIEvent *) eveniment [touch touchesEnded: touches withEvent: event];  - (void) drawBitmap // (3) UIGraphicsBeginImageContextWithOptions (auto.bounds.size, DA, 0.0); [[UICcolor negruColor] setStroke]; dacă (! incrementalImage) // prima remiză; vopsea de fundal alb de ... UIBezierPath * rectpath = [UIBezierPath bezierPathWithRect: self.bounds]; // enclosing bitmap cu un dreptunghi definit de un alt obiect UIBezierPath [[UICcolor whiteColor] setFill]; [umplutură de rectatură]; // umplerea cu alb [incrementalImage drawAtPoint: CGPointZero]; [accident vascular cerebral]; incrementalImage = UIGraphicsGetImageFromCurrentImageContext (); UIGraphicsEndImageContext ();  @Sfârșit

După salvare, nu uitați să schimbați clasa obiectului de vizualizare în XIB-urile dvs. în CachedLIView!

Când utilizatorul își plasează degetul pe ecran pentru a desena, începem cu o cale nouă, fără puncte sau linii în ea și adăugăm segmente de linie la ea la fel ca înainte.

Din nou, referindu-se la numerele din comentarii:

  1. În plus, menținem în memorie o imagine bitmap (de pe ecran) de aceeași dimensiune ca cea a panzei (adică pe ecran), în care putem stoca ceea ce am tras până acum.
  2. Desenăm conținutul pe ecran în acest tampon de fiecare dată când utilizatorul își ridică degetul (semnalat de -touchesEnded: WithEvent).
  3. Metoda drawBitmap creează un context bitmap - metodele UIKit au nevoie de un "context actual" (o pânză) pentru a atrage. Când suntem înăuntru -drawRect: acest context este automat pus la dispoziția noastră și reflectă ceea ce facem în viziunea noastră de pe ecran. În schimb, contextul bitmap trebuie să fie creat și distrus în mod explicit, iar conținutul desenat se află în memorie.
  4. Prin cachearea desenului anterior în acest mod, putem să scăpăm de conținutul anterior al căii și, în acest fel, să ne menținem drumul prea lung.
  5. Acum, de fiecare dată drawRect: se numeste, mai intai tragem continutul tamponului de memorie in viziunea noastra care (prin design) are exact aceeasi dimensiune, si astfel pentru utilizatorul pe care il pastram iluzia desenului continuu, numai intr-un mod diferit decat inainte.

În timp ce acest lucru nu este perfect (ce se întâmplă dacă utilizatorul nostru continuă să deseneze fără a-și ridica degetul, vreodată?), Va fi suficient de bun pentru scopul acestui tutorial. Sunteți încurajați să experimentați pe cont propriu pentru a găsi o metodă mai bună. De exemplu, puteți încerca să salvați desenul în mod periodic în loc de numai când utilizatorul ridică degetul. Așa cum se întâmplă, această procedură de cache de pe ecran ne oferă posibilitatea de procesare de fundal, dacă alegem să o implementăm. Dar nu vom face asta în acest tutorial. Sunteți invitați să încercați pe cont propriu!


Îmbunătățirea calității stroke vizuale

Acum, să ne îndreptăm atenția spre a face ca desenul să "arate mai bine". Până acum, ne-am alăturat punctelor de atingere adiacente cu segmente de linie drepte. Dar, în mod normal, atunci când tragem mâna liberă, cursa noastră naturală are un aspect curat și curbil (mai degrabă decât blocat și rigid). Are sens că încercăm să interpolăm punctele noastre cu curbe mai degrabă decât segmente de linie. Din fericire, clasa UIBezierPath ne permite să-i desenăm același nume: curbe Bezier.

Care sunt curbele lui Bezier? Fără a invoca definirea matematică, o curbă Bezier este definită de patru puncte: două puncte finale prin care trece o curbă și două "puncte de control" care ajută la definirea tangentelor pe care curba trebuie să le atingă la punctele sale finale (aceasta este o curbă Bezier cubică, pentru simplitate o voi referi la ea ca pe o "curbă Bezier").

Bezier curbe ne permit să atragă tot felul de forme interesante.

Ceea ce vom încerca acum este de a grupa secvențe de patru puncte de atingere adiacente și de a interpola secvența de puncte într-un segment de curbă Bezier. Fiecare pereche de segmente Bezier adiacente va împărți un punct final în comun pentru a menține continuitatea accidentului.

Știți deja exercițiul. Creați o nouă subclasă UIView și denumiți-o BezierInterpView. Inserați următorul cod în fișierul .m:

 #import "BezierInterpView.h" @implementation BezierInterpView cale UIBezierPath *; UIImage * incrementalImage; Punctele CGPoint [4]; // pentru a urmări cele patru puncte ale segmentului Bezier uint ctr; // o variabilă contor pentru a ține evidența indexului de puncte - (id) initWithCoder: (NSCoder *) aDecoder if (self = [super initWithCoder: aDecoder]) [self setMultipleTouchEnabled: NO]; [auto setBackgroundColor: [UICcolor whiteColor]]; cale = [UIBezierPath bezierPath]; [cale setLineWidth: 2.0];  întoarce-te;  - (void) drawRect: (CGRect) rect [incrementalImage drawInRect: rect]; [accident vascular cerebral];  - (void) atingeBegan: (NSSet *) atinge cuEvent: (UIEvent *) eveniment ctr = 0; UITouch * touch = [atinge oriceObject]; pts [0] = [atingeți locațiaInView: auto];  - (void) atingeModed: (NSSet *) atinge cu EventEvent: (UIEvent *) eveniment UITouch * touch = [atinge anyObject]; CGPoint p = [atingeți locațiaInView: auto]; ctr ++; pct [ctr] = p; dacă (ctr == 3) // punctul 4 moveToPoint: pts [0]]; [cale addCurveToPoint: pct. [3] controlPoint1: pts [1] controlPoint2: pts [2]]; // așa este adăugată o curbă Bezier pe o cale. Adăugăm un Bezier cubic de la pt [0] la pt [3], cu puncte de control pt [1] și pt [2] [self setNeedsDisplay]; pts [0] = [cale curentă]; ctr = 0;  - (void) atingeEnded: (NSSet *) atinge cuEvent: (UIEvent *) eveniment [self drawBitmap]; [self setNeedsDisplay]; pts [0] = [cale curentă]; // lăsați cel de-al doilea punct final al segmentului actual Bezier să fie primul pentru următorul segment Bezier [path removeAllPoints]; ctr = 0;  - (void) touchesCancelled: (NSSet *) atinge cu EventEvent: (UIEvent *) eveniment [touch touchesEnded: touches withEvent: event];  - (void) drawBitmap UIGraphicsBeginImageContextWithOptions (auto.bounds.size, DA, 0.0); [[UICcolor negruColor] setStroke]; dacă (! incrementalImage) // prima dată; vopsea de fundal alb UIBezierPath * rectpath = [UIBezierPath bezierPathWithRect: self.bounds]; [[UICcolor whiteColor] setFill]; [umplutură de rectatură];  [incrementalImage drawAtPoint: CGPointZero]; [accident vascular cerebral]; incrementalImage = UIGraphicsGetImageFromCurrentImageContext (); UIGraphicsEndImageContext ();  @Sfârșit

După cum indică comentariile inline, schimbarea principală este introducerea a două variabile noi pentru a urmări punctele din segmentele noastre Bezier și o modificare a -(Void) touchesMoved: withEvent: metoda de a desena un segment Bezier pentru fiecare patru puncte (de fapt, la fiecare trei puncte, în ceea ce privește atingerile pe care aplicația ne raportează, deoarece împărtășim un punct final pentru fiecare pereche de segmente Bezier adiacente).

S-ar putea să subliniați aici că am neglijat cazul în care utilizatorul își ridică degetul și încheie secvența de atingere înainte de a avea suficiente puncte pentru a finaliza ultimul nostru segment Bezier. Dacă da, ați avea dreptate! În timp ce din punct de vedere vizual acest lucru nu face prea mare importanță, în anumite cazuri importante. De exemplu, încercați să desenați un cerc mic. S-ar putea să nu se închidă complet, și într-o aplicație reală ați dori să o ocupați în mod corespunzător de acest lucru -touchesEnded: WithEvent metodă. În timp ce ne aflăm, nu am acordat o atenție deosebită cazului de anulare a atingerii. touchesCancelled: WithEvent metoda de exemplu se ocupă de acest lucru. Aruncați o privire la documentația oficială și vedeți dacă există cazuri speciale pe care ar trebui să le rezolvați aici.

Deci, cum arată rezultatele? Încă o dată, vă reamintesc să setați clasa corectă în XIB înainte de a construi.

Huh. Nu pare o mulțime de îmbunătățiri, nu-i așa? Cred că ar putea fi puțin mai bine decât interpolarea liniei drepte, sau poate că asta e doar o dorință de gândire. În orice caz, nimic nu merită să se laude.


Îmbunătățirea în continuare a calității accidentului

Iată ce cred că se întâmplă: în timp ce facem dificultatea de a interpola fiecare secvență de patru puncte cu un segment curbat neted, nu facem nici un efort pentru a face ca un segment curbat sa treaca usor in urmatorul, atât de eficient avem încă o problemă cu rezultatul final.

Deci, ce putem face? Dacă vom continua să abordăm abordarea pe care am pornit-o în ultima versiune (adică folosind curbele Bezier), trebuie să avem grijă de continuitatea și neteda la "punctul de joncțiune" al două segmente Bezier adiacente. Cele două tangente la punctul final cu punctele de control corespunzătoare (al doilea punct de control al primului segment și primul punct de control al celui de-al doilea segment) par a fi cheia; dacă ambele aceste tangente aveau aceeași direcție, curba ar fi mai netedă la joncțiune.

Ce se întâmplă dacă am mișcat punctul final comun undeva pe linia care unește cele două puncte de control? Fără a utiliza date suplimentare despre punctele de atingere, cel mai bun punct pare să fie punctul de mijloc al liniei care unește cele două puncte de control în considerare și cerința noastră impusă în direcția celor două tangente ar fi satisfăcute. Să încercăm asta!

Creați o subclasă UIView (încă o dată) și denumiți-o SmoothedBIView. Înlocuiți întregul cod în fișierul .m cu următoarele:

 #import "SmoothedBIView.h" @implementation SmoothedBIView cale UIBezierPath *; UIImage * incrementalImage; Punctele CGPoint [5]; // acum trebuie să ținem evidența celor patru puncte ale unui segment Bezier și a primului punct de control al segmentului uint ctr;  - (id) initWithCoder: (NSCoder *) aDecoder if (self = [super initWithCoder: aDecoder]) [auto setMultipleTouchEnabled: NO]; [auto setBackgroundColor: [UICcolor whiteColor]]; cale = [UIBezierPath bezierPath]; [cale setLineWidth: 2.0];  întoarce-te;  - (id) initWithFrame: Cadrul (CGRect) auto = [super initWithFrame: cadru]; dacă (auto) [self setMultipleTouchEnabled: NO]; cale = [UIBezierPath bezierPath]; [cale setLineWidth: 2.0];  întoarce-te;  // Numai suprascrie drawRect: dacă executați desenul personalizat. // O implementare goală afectează în mod negativ performanța în timpul animației. - (void) drawRect: (CGRect) rect [incrementalImage drawInRect: rect]; [accident vascular cerebral];  - (void) atingeBegan: (NSSet *) atinge cuEvent: (UIEvent *) eveniment ctr = 0; UITouch * touch = [atinge oriceObject]; pts [0] = [atingeți locațiaInView: auto];  - (void) atingeModed: (NSSet *) atinge cu EventEvent: (UIEvent *) eveniment UITouch * touch = [atinge anyObject]; CGPoint p = [atingeți locațiaInView: auto]; ctr ++; pct [ctr] = p; dacă (ctr == 4) pts [3] = CGPointMake ((pct. [2] .x + pts [4] .x) /2.0. ); // mutați punctul final la mijlocul liniei care unește al doilea punct de control al primului segment Bezier și primul punct de control al celui de-al doilea segment Bezier [calea moveToPoint: pts [0]]; [cale addCurveToPoint: pct. [3] controlPoint1: pts [1] controlPoint2: pts [2]]; // adăugați un Bezier cubic de la pt [0] la pt [3], cu puncte de control pt [1] și pt [2] [self setNeedsDisplay]; // înlocuiți punctele și pregătiți-vă pentru a ocupa următorul segment pts [0] = pts [3]; pts [1] = puncte [4]; ctr = 1;  - (void) atingeEnded: (NSSet *) atinge cuEvent: (UIEvent *) eveniment [self drawBitmap]; [self setNeedsDisplay]; [path removeAllPoints]; ctr = 0;  - (void) touchesCancelled: (NSSet *) atinge cu EventEvent: (UIEvent *) eveniment [touch touchesEnded: touches withEvent: event];  - (void) drawBitmap UIGraphicsBeginImageContextWithOptions (auto.bounds.size, DA, 0.0); dacă (! incrementalImage) // prima dată; vopsea de fundal alb UIBezierPath * rectpath = [UIBezierPath bezierPathWithRect: self.bounds]; [[UICcolor whiteColor] setFill]; [umplutură de rectatură];  [incrementalImage drawAtPoint: CGPointZero]; [[UICcolor negruColor] setStroke]; [accident vascular cerebral]; incrementalImage = UIGraphicsGetImageFromCurrentImageContext (); UIGraphicsEndImageContext ();  @Sfârșit

Principiul algoritmului discutat mai sus este implementat în -touchesMoved: WithEvent: metodă. Comentariile inline vă vor ajuta să conectați discuția cu codul.

Deci, cum sunt rezultatele, din punct de vedere vizual? Nu uitați să faceți acest lucru cu XIB.

Din fericire, de această dată există îmbunătățiri substanțiale. Având în vedere simplitatea modificării noastre, pare destul de bună (dacă o spun și eu!). Analiza noastră a problemei cu iterația anterioară și soluția propusă au fost validate.


Unde să mergeți de aici

Sper că ați găsit acest tutorial benefic. Sperăm că veți dezvolta propriile idei despre îmbunătățirea codului. Una dintre cele mai importante îmbunătățiri (dar ușoare) pe care le puteți încorpora este manipularea mai fină a secvențelor de atingere, așa cum sa discutat anterior.

Un alt caz pe care l-am neglijat se ocupă de o secvență de atingere care constă în faptul că utilizatorul atinge vederea cu degetul și apoi o ridică fără a fi mutat - în mod efectiv un robinet pe ecran. Utilizatorul s-ar aștepta probabil să deseneze un punct sau o micșorare a punctului de vedere în acest fel, dar cu implementarea noastră actuală nu se întâmplă nimic deoarece codul de desen nu se execută decât dacă vederea noastră primește -touchesMoved: WithEvent: mesaj. S-ar putea să doriți să aruncați o privire la UIBezierPath pentru a vedea ce alte tipuri de căi puteți construi.

Dacă aplicația dvs. funcționează mai mult decât ceea ce am făcut aici (și într-o aplicație de desen care merită transportată, ar fi!), Proiectând-o astfel încât codul non-UI (în special cache-ul de pe ecran) face diferența semnificativă pe un dispozitiv multicore (iPad 2). Chiar și pe un dispozitiv cu un singur procesor, cum ar fi iPhone 4, performanța ar trebui să se îmbunătățească, deoarece mă aștept ca procesorul să anuleze munca de caching care, la urma urmei, se întâmplă doar o dată la fiecare ciclu al ciclului principal.

Vă încurajez să vă flexați mușchii de codificare și să jucați cu UIKit API pentru a dezvolta și îmbunătăți unele dintre ideile implementate în acest tutorial. Distrează-te și mulțumesc pentru lectură!

Cod