Tinder-esque kártya interfész felépítése

Amikor nem dühösen csúszkálok a Tinderre, és kétségbeesetten próbálom megtalálni életem szerelmét egy véletlenszerű ember tengerében, akivel még soha nem találkoztam, szoftvereket és interfészeket építek az iPhone-hoz. Mint kiderült, a Tinder valóban hihetetlenül érdekes és egyedülálló gesztusalapú interakciós mintát kezdett úttörővé tenni, amely napjainkban a szoftveripar számos mobiltelefon-alkalmazására támaszkodott. Gyakran hallhatjuk, hogy a befektetők, a tervezők és a mérnökök az üzleti ötletükre mint „a X finomság” -ra hivatkoznak - utalva arra, hogy a Tinder „ellop-képes” kártyarendszerét alkalmazza, és nem más, mint a többi ember félpszichopatikus osztályozására alkalmazza más frakciókban. egy másodpercre épül, nem csupán egy fénykép pillantására.

Y-Cash App, Eleken

Ezen a hétvégén megbotlottam egy figyelmeztető üzenetre, amely a Tinder-esque kártya interfészének egyszerűsített változatát ábrázolta. Nem terveztem semmi mást a hétvégére, ezért úgy döntöttem, hogy szánok időt arra, hogy Swiftben natív módon megvalósítsam. Mint gondolnád, nyitottam a kódomat a Githubon, és írtam az összetevők, gesztusok és animációk készítésének folyamatáról. Mindig keresek a Core Animationbe merülési módszereket, és többet megértek arról, hogyan lehet dinamikus, gesztus alapú animációkat készíteni. Ez nagyszerű alkalom volt számomra, hogy megismerjek az interfészek készítéséhez rendelkezésre álló eszközöket, amelyek izgalmasak és az emberek szeretik használni.

SwipeableCardViewContainer

A teljes összetevővel való kölcsönhatás az, hogy egy SwipeableCardViewContainer nézetet eldobunk a Storyboard-ba (vagy kódba), és meg kell felelnünk a SwipeableCardViewDataSource, SwipeableCardViewDelegate protokolloknak. Ez a konténer nézet az a nézet, amely felelős az összes kártya elhelyezésén belül és az alapjául szolgáló minden kártya-sorozat nyomon követésének logikájáért. Úgy tervezték, hogy nagyon hasonló legyen az UICollectionView és UITableView alkalmazásokhoz, amelyekkel valószínűleg máris ismersz.

Az Ön adatforrása megad egy numberOfCards-ot, és minden indexhez egy SwipeableCardViewCard-kártyát. Ezenkívül opcionálisan egy nézet is, amely az összes kártya alatt megjelenik, és amely akkor látható, ha minden kártya el van húzva.

Minden alkalommal, amikor a reloadData () meghívásra kerül, eltávolítja a meglévő kártya nézeteket a felülnézetből. Helyezze be az dataSource első 3 kártya nézetét résznézetként. Az overlay / inet effektus elérése érdekében, amikor a kártyák egymás fölé vannak rakva, az egyes kártyák keretét az index alapján manipulálják.

Kiszámolásra kerül egy vízszintes alapállapot, valamint egy vertikális alaphelyzet, és ezeket az értékeket a keret eredetére és szélességére alkalmazzuk, a tároló nézet határainál. Például az első kártya indexe 0, tehát függőlegesen és vízszintesen egy 0 beillesztést alkalmaz, hogy a kártya tökéletesen elférjen a tartályban. A második kártya indexe 1, tehát átnyomja a kártya y eredetét, csökkenti annak szélességét, és lefelé csökken a kártya x eredetét, mindegyikét 1-rel és így tovább, minden látható kártya indexhez 0 és 3 között.

A keretek frissítésének ezzel a megközelítésével szembesültem egy olyan animáció megvalósításával, amelyet kártyaként látsz, amikor a meglévő kártyák felfelé élnek, és alulról új kártyát mutatnak. Minden alkalommal, amikor egy kártyát hozzáadtam a tartályhoz, az eredeti nézetet beillesztném a 0 eredetnél. Ez azt jelentette, hogy a SwipeableCardViewContainer subviews tömbje fordított sorrendben volt a várható tényleges kártyaindexekkel. Vagyis a 0-ból származó részképernyő a nézethierarchiában a legtávolabbi nézet volt, bár a nézethez társított kártyaindex 2 volt (a legmagasabb index).

Amikor az új kártyát a verem aljára helyezi be, a nézetet a 0-s indexbe helyezem az alnézetek tömbjében, ami indexelt eltérést okoz. Akkor, amikor frissítettem az összes nézet keretét az új indexük alapján az új pozíciójukra, az megfordítaná a kártya összes pozícióját. Ezt a problémát úgy oldottam meg, hogy az .reversed () használatával az alnézetekben iteráltam, így biztosítva, hogy a kereteket a tényleges indexük alapján frissítsék, nem pedig az indexek az indexben.

Nézetek ellenőrzése a Revealban

SwipeableView

Mint gondolnád, hogy a komponens megvalósításának legbonyolultabb és időigényesebb része a húzható kártyák volt. Ehhez sok összetett matematika alkalmazására volt szükség (ezek közül néhányat még mindig nem értek teljesen). Ennek nagy része a SwipeableView nevű UIView alosztályban található.

Minden kártya alosztály, a SwipeableView, amely egy UIPanGestureRecognizer alkalmazást használ, hogy meghallgassa a Pan mozdulatokat, például amikor a felhasználó „megragadja” egy kártyát az ujjával, mozgatja a képernyőn, majd rázza meg vagy felemeli az ujját. A Gesztusfelismerők nagyon alulértékelt API-k, amelyek hihetetlenül egyszerűen és egyszerűen működnek, figyelembe véve, hogy mekkora funkcionalitást és teljesítményt nyújtanak.

Minden UIGestureRecognizer-nek van egy állapota, amely bizonyos információkat nyújt arról, hogy mi történt vagy nem történt. Figyelemre méltó ez az összetevő a Kezdő, Megváltozott, Végleges állapotok, amelyek meghatározzák, hogy a felhasználó elindított-e egy panelt, egy edény folyamatban van, vagy az edény kész. Vannak más olyan állapotok is, mint például Lehetséges, Megszakított, Sikertelen, amelyek akkor válnak kiváltásra, ha egy gesztus még nincs regisztrálva pásztázó gesztusként, vagy a felhasználó törölte a pásztázást gesztus megfordításával, vagy ha valami nem sikerült. Ez az egyszerű enum nagyon sok bonyolult logikát kezel az UIKit belsejében a motorháztető alatt, annak meghatározására, hogy a felhasználó hogyan működik együtt a szoftverrel.

Meghallgattam egy hihetetlen beszédet Andy Matuschak, aki nemrégiben dolgozott az UIKit mellett, és elmagyarázza, hogy az UIKit csapata miért használta ezt a sajátos megközelítést a gesztusok kezelésére más hagyományos megközelítésekkel szemben, mint például a React.js. Azt javaslom, töltsön időt ennek a beszélgetésnek a meghallgatására vagy nézésére.

Amikor a pánik gesztusa megkezdődik, néhány különböző dolognak kell történnie. Mint például az InitTouchPoint kiszámítása, egy CGPoint, amely pontosan képviseli azt a helyet, ahol a felhasználó először elindította a képernyőt a képernyőn a SwipeableView koordinátarendszerében. Ezzel kiszámítják egy új rögzítési pontot, amelyet hamarosan a SwipeableView rétegének horgonypontjaként állítanak be.

Az UIKit az anchorPoint pontot írja le, amelyben az összes geometriai transzformációt alkalmazzák. Alapértelmezés szerint az összes UIView anchorPoint a CGPoint nézet pontos középpontja (x: 0,5, y: 0,5).

A nézet minden geometriai manipulációja a megadott pont körül zajlik. Például, ha egy rotációs transzformációt alkalmazunk egy rétegre az alapértelmezett rögzítési ponttal, a réteg forog a közepén. Ha a rögzítési pontot más helyre változtatja, a réteg elfordul az új pont körül.

Annak beállításával, hogy a rögzítési pontot arra a pontra állítjuk, amelyen a felhasználó megkezdi a gesztusát, biztosítjuk, hogy minden fordítás és elforgatás a felhasználó ujjával szemben történjen, ami sokkal természetesebb animációt biztosít, és azt az érzést kelti, hogy a felhasználó valóban megragadta a a kártya.

A rögzítési pont kiszámítása és beállítása után a réteg helyzetét frissítik, a meglévő animációkat (amelyek esetleg másutt elindultak) eltávolítják, és a SwipeableView réteg rasterizationScale-jét az eszköz méretére állítják úgy, hogy a raszterálás az eszközhöz képest történjen. a tényleges méret és tartalom nem zsugorodik, vagy nem növekszik.

Ha a pásztázó gesztus állapota megváltozik, például amikor a felhasználó az ujját (és a kártyát) a képernyő körül húzza, átalakítást kell végrehajtani a kártyán, hogy a kártya kövesse az ujját, és azt az érzést kelti, hogy a felhasználó húzza a kártya. Azt is szeretnénk elvégezni, hogy ezt a fordítást kissé elforgatva nézzük, hogy minél több „megrázható” ív váljon a kártyára, és inkább olyannak viselkedjen, mint egy kártya, amelyet a felhasználónak egy bizonyos irányba kell ráznia, szemben a szabadon mozgatni. Tinder itt hangsúlyozza a „Balra vagy a jobbra húzást” annak a paradigma elfogadásának / elutasításának hangsúlyozására, amely miatt ez az interfész jobban ellenőrzöttnek és lekerekítettnek érzi magát.

Ennek a transzformációnak a alkalmazása viszonylag egyszerű, a CATransform3D létrejön, és mind a CATransform3DRotate, mind a CATransform3DTranslate alkalmazandó. A forgatást a rotationAngle alapján kell kiszámítani néhány beállítás, például animationDirectionY, rotationAngle és rotationStrength szorzásával. Forgatási szög határozza meg a kártya elforgatásának mértékét, amint elmozdul, vagy a görbe „intenzitását”. Alapértelmezés szerint ez a görbe π / 10. A forgási szilárdságot a fordítási pont és a maximális fordítási beállítás 1,0 alapján kell kiszámítani.

Miután a pásztázó gesztus befejeződött, egy befejezett animációt kell alkalmazni a kártyára úgy, hogy az a képernyőről lepattanjon, vagy ha a felhasználó egy bizonyos küszöbértéknél nem teljes mértékben átpördítette a kártyát, vissza kell animálni azt az eredeti helyére. a veremben.

Először kiszámoljuk az ablaktábla dragDirection-jét, amely sok bonyolult matematikát igényel, amely normalizálja a pásztázó gesztus fordítási pontját, és elvégzi a redukciót a legközelebbi ellop-irány kiszámításához az egyes ellop-irányokhoz megadott statikus értékek alapján. Míg én nem valósítottam meg teljesen ezt a logikát, képes voltam megtervezni egy hasonló komponens - https://github.com/Yalantis/Koloda - meglévő nyílt forráskódú megvalósítását, és beágyazhattam ezt a logikát a SwipeDirection nevű enumba. Mindegyik ellop-iránynak van egy vízszintes pozíciója és vertikális pozíciója ahhoz képest, hogy hol van egy általános geometriai rendszeren (például a bal felső rész vízszintesen van balra és függőlegesen a felső). Ezen információk felhasználásával kiszámolhatom egy adott ellop irányát balra, balra, jobbra, vagy balra stb.

Hasonlóképpen egy normalizált húzási pont és a gesztus húzási fordítási pontjának felhasználásával kiszámolhatom a húzási százalékot a Gesztus végpontjától a „célpontig” mért távolság hányadának, ahol a kártya teljesen „megpörög”. Ezt a célpontot egy elcsúszott százalékos különbség alapján számítják ki, amely meghatározza azt a küszöböt, hogy mekkora legyen ennek a százaléknak, mielőtt egy mozdulatot úgy kell tekinteni, hogy az elegendő „megrázásnak” minősüljön a kártya eltávolításához. Alapértelmezés szerint ez a küszöbérték 0,6, azaz ha a pásztázó gesztus nagyobb vagy egyenlő a célponttól való távolság 60% -ával, akkor a kártya megrázkódottnak tekinthető, és eltávolítható a kötegből, különben vissza kell térnie eredeti helyzetébe a kötegben. .

A Facebook POP animációs keretrendszerével a dolgok egyszerűsítése és a dobozból való dinamikus animáció biztosítása érdekében a POPBasicAnimation kártyát alkalmazom, amely az X és Y eredetét a képernyőn kívüli értékre fordítja. Miután ez az animáció befejeződött, felhívom a meghatalmazott funkciót self.delegate? .DidEndSwipe-re (onView: self), és támaszkodom megbízottomra (amely ezt a SwipeableView-t alnézetté tette), hogy eltávolítsam ezt a SwipeableView-t al-nézetként. Ezt a kártyát mostantól teljesen eltávolítottuk a veremből, és elvégeztük a munkát.

Ha a dragPercentage nem haladja meg a 60% -ot vagy annál több, vagy ha a pásztázás gesztusának állapota törlődik vagy meghiúsul, vissza kell animálnom a kötegbe. Ez az animáció általában egy kicsit gumiszalagszerű, rugós visszapattanási animáció, amely azt jelzi, hogy az ellop sikertelen volt, és a felhasználó „elengedte” a kártyát, lehetővé téve, hogy visszatérjen az eredeti helyére, pontosan úgy, ahogy azt a valós fizika tér.

Az animáció végrehajtása ugyanolyan egyszerű, mint a POPSpringAnimation alkalmazása a már alkalmazott forgatásnál, hogy a jelenlegi értékeiről az eredeti értékre fordítsa. Csakúgy, mint a fordítás POPSpringAnimation elemzését, amelyet a jelenlegi értékeinkről az eredeti értékekre alkalmaztunk. A POPSpringAnimation biztosítja, hogy a tényleges értékek kissé túllépjenek előre-hátra, ami a kívánt rugóhatást eredményezheti.

SampleSwipeableCard

Most, hogy megvalósítottuk a SwipeableView alkalmazást, olyan egyedi kártyákat készítünk, amelyek különböznek egymástól, és tartalmazzák a saját tartalmukat, például UILabels, UIImages és UIButtons, mivel az alnézetek ugyanolyan egyszerűek, mint a SwipeableView örökölése. Maga az alosztály szigorúan felel a saját tartalmának kezeléséért, és arra támaszkodik, hogy a szuperosztály gondoskodjon az éppen végrehajtott ellop-logikáról.

Létrehoztam egy SampleSwipeableCard alosztályt, amely egy címhez és egy felirathoz Uaabel, valamint egy piros UIBgombot plusz ikonnal és egy különálló háttérszínű UIView-t tartalmaz, amely egy belső UIImageView-t tartalmaz. Az összes szabványos, egyszerű és alapvető UIKit elem pontosan úgy dobódik az Xib-re, mint amire számíthat.

A ViewController-ben biztosítom, hogy minden kártya számára ViewModels sorozatot készítsek, amivel a SampleSwipeableCard képes konfigurálni.

És visszaadok egy SampleSwipeableCard kártyát, amely az adott index nézetmodellel van konfigurálva. Pontosan úgy, ahogy egy adott ViewModel UICollectionView elemén belül egy cellát konfigurálnék.

Olyan kód felhasználásával, amelyet korábban lekerekített sarkok és árnyék felvitelére használtam (meglepően nem olyan egyszerű feat) az iOS 11 App Store új bejegyzésének újjáépítéséhez - hasonló lekerekített sarkot alkalmaztam. és árnyékolom a SampleSwipeableCard nézeteimhez.

Valami, amit utóbbi időben sokkal csináltam, megbotlik egy olyan dologon, ami felhívja a figyelmemet, vagy érdekli az érdeklődésem, majd nagyon gyorsan a nyakam mélyén találom magam a gyomnövényekben, amelyek elrejtik. Az animáció valószínűleg nem az én erõs alkalmazkodásom, amikor a felhasználói felületeket építjük. Az animációt az UIKit és a Core Animation segítségével végeztem korábban, nagyrészt sikerrel, bár nem vagyok benne annyira magabiztos, mint más dolgok megvalósításában. Leginkább azért, mert soha nincs helyes vagy rossz válasz arra, hogyan lehet valamit animálni, hanem sokféleképpen érhető el ugyanaz az eredmény.

Nagyon rajongom a Tinder kártya stílusú gesztus felületét, és azt hiszem, hogy ez egy igazán egyedi módszer a kis-közepes adathalmazok ellopására, szortírozására vagy manipulálására, legyen szó akár véletlenszerű potenciális szerelmi érdekről, akár valami másról. Nagyon tartozom a fenti megvalósításnak ennek a kártya stílusú felületnek a sok nyílt forráskódú implementációjához, amelyet online fedeztem fel, ám azt hiszem, nagyszerű, hogy a mérnökök építhetnek valamit, és nyílt forráskódú kódot adhatnak mások számára, hogy fordított mérnököt készítsenek, vagy vonatkozzon valami másra.

Ezt a kódot közzétettem a Githubon. Ha érdekli, bátran eldönteni, vagy elküldhetsz húzási kérelmet. Tudassa velem, ha kérdése van, vagy ha tetszett ez a üzenet, és szeretné, ha részletesebben írnék valamiről.

Köszönöm, hogy elolvasta! Nyugodtan kövessen engem a twitteren @phillfarrugia

http://www.phillfarrugia.com/2017/10/22/building-a-tinder-esque-card-interface/