Erstellen Sie eine Schaltfläche mit Ripple-Effekt für die XMars-Benutzeroberfläche


Hallo allerseits, heute werde ich euch erzählen, wie ich die Schaltfläche für das XMars-UI- Projekt entwickelt habe. Oh ja, es ist eine Kleinigkeit, aber es gibt etwas zu erzählen. Ich werde die Details weglassen, die mit dem Hinzufügen einer neuen Komponente zum Open Source-Projekt verbunden sind. Im Detail werde ich in einem separaten Artikel über das Projekt sprechen.


Einleitung


Die XMars-Benutzeroberfläche ist eines meiner neuen Open Source-Projekte. Eine einfache Bibliothek von UI-Komponenten für HTML / CSS und React. In Zukunft plane ich, Vue zu unterstützen. Bisher hat es nur einen Button und Icons :)


Das Projekt entstand als Idee im Rahmen des Telegrammwettbewerbs, dessen Kern die Entwicklung einer Webversion des Kunden war. Gemeinsam mit einem Kollegen haben wir beschlossen, warum wir nicht daran teilnehmen. Die Rollen wurden geteilt, sodass ich ein Layout habe. Wenn ein Kollege die Berechtigung versteht, verbinde ich mich, um Komponenten zu schreiben. Alles wäre in Ordnung, aber sich in das Telegramm einzuloggen ist nicht so einfach. Infolgedessen haben wir nichts gesendet, aber ich habe alles eingeholt, und es stellte sich heraus, dass dies vergeblich war. Aber wie Varlamov sagt, ist Ihr Projekt schon etwas wert, da Sie Ihre Zeit damit verbracht haben. Es ist schwierig, dem zu widersprechen, denn wenn Sie auf Stunden und Geld umsteigen, ist es nicht mehr kostenlos, Webpack gleich zu Beginn des Projekts einzurichten. Angesichts all dieser Schande beschloss ich, es irgendwie auf Open Source zu werfen. Welcher einzelne Bootstrap soll verwendet werden? Ich möchte ein eigenes UI-Framework für meine anderen Projekte.


Der Knopf


Die Schaltfläche in der Benutzeroberfläche ist möglicherweise das Hauptelement, mit dem der Benutzer mit der Anwendung interagiert. Daher ist dies eine der ersten Komponenten eines UI-Frameworks / einer UI-Bibliothek.


In der Gestaltung von Telegram gibt es nicht viele Variationen von Schaltflächen:



Ich habe 3 Haupt- (Standard, Akzent, Haupt), runde mit einem Symbol und grün markiert. Es ist immer noch halbtransparent, aber lass es runter. Bei der Entwicklung der XMars-Benutzeroberfläche habe ich größtenteils nicht herausgefunden, wo die transparente Schaltfläche benötigt wird.


Der Bibliotheksbenutzer sollte mit CSS-Klassen vertraut sein. Ich bin kein Fan von solchen Benennungssystemen wie BEM. Ich mag es, wie Bootstrap Klassen benennt. Aber ich würde ein bisschen mehr vereinfachen. Anstelle von .btn .btn-primary nur .btn .primary . Und im Fall der React-Komponente sieht das so aus:


 <Button primary>Hey</Button> 

Die gleiche taste aber welligkeit wirkung:


 <Button primary ripple>Hey</Button> 

HTML / CSS


Die UI-Bibliothek sollte nicht an ein UI-Framework gebunden sein. In Zukunft plane ich, das Layout für Vue Komponenten zu erweitern. Beginnen wir mit einfachem HTML / CSS.


Unter der Haube des Tailwindcss- Projekts handelt es sich um ein Utility-First-CSS-Framework, dh ein Framework, das Ihnen Dienstprogramme statt vollwertiger Komponenten zur Verfügung stellt.



Neben Tailwindcss wird PostCSS für Mixins , Variablen und verschachtelte Stile verwendet.


Ausführlicher über die Verwendung eines solchen Frameworks und wie das Projekt konfiguriert ist, werde ich in einem separaten Artikel erzählen. Zum jetzigen Zeitpunkt reicht es aus, dass wir über ein so leistungsfähiges Toolkit verfügen und die Komponenten, mit denen es arbeitet, voll ausnutzen.


Das <button> verfügt über eine Reihe von Standardstilen, die entweder entfernt oder neu definiert werden müssen.



Im Fall von Tailwindcss hat das Button-Tag diesen Stil:



Alle unnötigen standardmäßig entfernt. Sie können gestalten, was Sie möchten, ohne befürchten zu müssen, dass ein Standardrahmen in einem bestimmten Zustand abfällt. Hier ist jedoch eine Einschränkung: Die Standardkontur muss noch festgelegt werden:



Eine Schaltfläche in der XMars-Benutzeroberfläche hat eine .btn Klasse:


 <button class="btn">Button</button> 

Füge diese Klasse zu unseren Styles hinzu:


 .btn { @apply text-black text-base leading-snug; @apply py-3 px-4 border-none rounded-lg; @apply inline-block cursor-pointer no-underline; &:focus { @apply outline-none; } } 

Neben der Tatsache, dass Tailwindcss Klassen zur Verfügung stellt, die Sie verwenden können, bietet es eine Art mixins . @apply ist kein SCSS oder eine Art Plugin für PostCSS. Dies ist die Syntax von Tailwindcss. Angewendete Stile werden im Allgemeinen anhand des Namens semantisch deutlich. py-3 und px-4 alleine können zu Fragen führen. Das erste ist das Auffüllen entlang y, padding-top: 0.75rem; vertikal, nämlich padding-top: 0.75rem; padding-bottom: 0.75rem; . Folglich ist das px-4 horizontal padding-right: 1rem; padding-left: 1rem; .


Das Design, das Telegram zur Verfügung stellte, um es sanft auszudrücken, ist schlecht dokumentiert, und Dinge wie Schaltflächen mit Rahmenradius müssen mit einem Lineal direkt vom Bild aus aufgenommen werden. Haben Sie sich jemals gefragt, was genau die Mittelwerte im border-radius ?



Dies ist buchstäblich der Radius des resultierenden Kreises in der Ecke. Wenn die kollektive Farm, dann können Sie das Lineal wie im Bild oben gezeigt ändern. Also habe ich rechteckige Auswahl in Gimp verwendet.



border-radius Rahmenradius der Buttons im Design ist 10px, leider gibt es keine solche Klasse aus der Box in Tailwindcss, aber ich hatte visuell 8px rounded-lg was 8px für die Standardschriftgröße (rem) ist.


Folgendes ist im Moment passiert: Ich habe den Knopf grau angestrichen, damit die Kanten sichtbar sind:



Als nächstes müssen wir einen Effekt erzielen auf :hover . Dann beschlossen die Designer von Telegram, etwas Licht ins Dunkel zu bringen und gaben die Farbe als 0,08% von #707579 . Ich sehe zwei Möglichkeiten, nehme einfach die Farbe mit der Pipette oder mache es wie dokumentiert. Die erste Option ist einfacher, aber in Zukunft ist sie nicht die beste. Tatsache ist, dass wenn sich der Hintergrund von Weiß unterscheidet: Wenn Sie mit der :hover über eine bestimmte Farbe fahren, verlieren Sie die Helligkeit und Transparenz der Schaltfläche. Dazu ist es besser, der Dokumentation zu folgen und Alpha zu legen männlich der Kanal. Dies kann auf unzählige Arten geschehen, beispielsweise mit den SCSS-Farbfunktionen. Das Projekt enthält jedoch kein SCSS. Aufgrund der gleichen Farbe möchte ich kein Plug-In mit PostCSS verbinden. Wir werden alles sehr einfach gestalten. In Chrome gibt es einen Colopaker, mit dem Sie Farben in verschiedene Systeme umwandeln, HEX-Farben #707579 , in rgba übersetzen und den Alphakanal einstellen können - 0,08%.



Voila! Etwas scharf blitzte mein Bild:



Wir rgba(112, 117, 121, 0.08) - rgba(112, 117, 121, 0.08) .



(: schweben)


Noch langweiliger und ohne viel Aufwand fügte ich den Rest des Staates hinzu:


  &:hover { background-color: var(--grey04); } &.accent { color: var(--blue01); } &.primary { @apply text-white; background-color: var(--blue01); &:hover { background-color: var(--blue02); } } 

Komponente reagieren lassen


Ursprünglich war der Button für den Telegrammwettbewerb vorgesehen und es war unmöglich, ein Framework zu verwenden. Ich musste den Ripple-Effekt für reines JS implementieren. Das möchte ich wirklich so bleiben, aber während Sie das Projekt alleine machen, müssen Sie etwas opfern.


Komponenten, die eine Art Logik erfordern, z. B. der Ripple-Effekt, werden implementiert und sind nur als React-Komponenten verfügbar.


Wickeln Sie die Schaltfläche in React-Komponente ist nicht schwierig:


 import React, { FunctionComponent } from 'react'; export interface ButtonProps { } const Button: FunctionComponent<ButtonProps> = (props) => { return ( <button className="btn">props.children</button> ); } export default Button; 

Diese Schaltfläche wird im angegebenen Stil angezeigt, ist jedoch in Wirklichkeit wenig hilfreich. Der Benutzer muss in die Lage versetzt werden, die Schaltfläche anzupassen, benutzerdefinierte Stile hinzuzufügen, Ereignishandler zu hängen usw.


Damit der Benutzer alles Notwendige übertragen kann, müssen Sie zuerst Typescript überwinden, da Sie sonst selbst mit onClick keine onClick Übertragung durchführen können. ButtonProps leichtes Bearbeiten der ButtonProps Oberfläche lösen wir das Problem:


 export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> 

Danach können wir sicher props zerstören:


 <button className="btn" {...props}>props.children</button> 

Eine ähnliche Verwendung der Schaltfläche verhält sich wie erwartet:


 <Button onClick={() => alert()}>Hey</Button> 


Als Nächstes fügen wir die Schaltflächenstile und die Möglichkeit hinzu, eine benutzerdefinierte Klasse (die plötzlich jemand benötigt) zu registrieren. Das Paket npm classnames ist perfekt für diese Zwecke.


 export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { primary?: boolean, accent?: boolean, additionalClass?: string, } ... const classNames = classnames( 'btn', { primary }, { accent }, additionalClass ); ... <button className={classNames} {...props}>props.children</button> 

Die btn Klasse ist immer gesetzt, aber primary und accent nur, wenn sie true . Classnames fügt eine Klasse hinzu, wenn sie einen logischen Wert true hat. Mit der Abkürzung ES6 erhalten wir einen einfachen Eintrag { primary } anstelle von { primary: true } .


additionalClass ist ein String, und wenn er leer oder undefiniert ist, spielt er für uns keine besondere Rolle. Es wird lediglich nichts zum Element hinzugefügt.


Zuerst habe ich props wie folgt zugewiesen:


 {...omit(props, ['additionalClass', 'primary'])} 

Alles weglassen, was nicht für die props des Schaltflächenelements gilt, aber dies ist nicht erforderlich, da React nicht zu viel rendert.


Welligkeitseffekt



Eigentlich sieht es so aus, aber es ist wünschenswert, dass die „Welle“ von der Stelle des Klicks abweicht.
Es gibt unzählige Möglichkeiten, eine solche Animation zu erstellen. Es ist wie ein Witz über das blaue Quadrat .


Bei Google wurde jedoch anhand der Beispiele für Codepen deutlich, dass in den meisten Fällen eine „Welle“ durch ein untergeordnetes Element realisiert wird, das sich ausdehnt und verschwindet.


Es wird entsprechend den Koordinaten des Klicks innerhalb der Schaltfläche positioniert. In der XMars-Benutzeroberfläche habe ich mich derzeit entschieden, diesen Effekt onPress wie in der Material-Benutzeroberfläche in onPress zu implementieren, sondern in Zukunft zu onPress . Bisher nur onClick .



Das Bild oben ist alles Magie. Ein Klick erzeugt ein untergeordnetes Schaltflächenelement, es wird absolut in der Mitte des Klicks positioniert und erweitert. Die Eigenschaft overflow: hidden Welle über die Schaltfläche hinausgeht. Das Element muss am Ende der Animation gelöscht werden.


Zunächst definieren wir die Stile, in denen wir sie verwenden können, indem wir Tailwindcss verwenden:


 .with-ripple { @apply relative overflow-hidden; @keyframes ripple { to { @apply opacity-0; transform: scale(2.5); } } .ripple { @apply absolute; z-index: 1; border-radius: 50%; background-color: var(--grey04); transform: scale(0); animation: ripple 0.6s linear; } &.primary { .ripple { background-color: var(--black02); } } } 

Dem für den Effekt verantwortlichen Element wird die Klasse .ripple zugewiesen. border-radius: 50%; entspricht einem Kreis (50% Verrundung in einem Winkel * 2), die Schaltfläche hat eine relative Position, die .ripple Schaltfläche hat eine absolute Position. Die Animation ist sehr einfach, zunehmende Wellen werden in 0,6 Sekunden transparent. Die Hintergrundfarbe ist dieselbe wie :hover und Falten, zwei transparente Wellenfarben und Schaltflächen ergeben das gewünschte Ergebnis. Auf der blauen Schaltfläche .primary ist dies nicht so wichtig, und dort können Sie eine nicht transparente Farbe verwenden.


Beim Klicken müssen Sie das Element "wave" erstellen. Aus diesem Grund erstellen wir einen Status für dieses Unternehmen und fügen der Schaltfläche den entsprechenden Klick-Handler hinzu, jedoch so, dass der benutzerdefinierte onClick nicht beeinträchtigt wird.


 ... const [rippleElements, setRippleElements] = useState<JSX.Element[]>([]); ... function renderRippleElements() { return rippleElements; } return ( <button className={classNames} {...props} onClick={(event) => { if (props.onClick) { props.onClick(event); } if (ripple) { onRippleClick(event); } }} > {children} {renderRippleElements()} </button> ); 

rippleElements - ein Array von JSX-Elementen. Die Render-Funktion hier mag redundant erscheinen, dies ist jedoch eher eine Frage des Stils und der Integration für die Zukunft.


  function onRippleClick(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) { var rect = event.currentTarget.getBoundingClientRect(); const d = Math.max(event.currentTarget.clientWidth, event.currentTarget.clientHeight); const left = event.clientX - rect.left - d/2 + 'px'; const top = event.clientY - rect.top - d/2 + 'px'; const rippleElement = newRippleElement(d, left, top); setRippleElements([...rippleElements, rippleElement]); } function newRippleElement(d: number, left: string, top: string) { const key = uuid(); return ( <div key={key} className="ripple" style={{width: d, height: d, left, top}} onAnimationEnd={() => onAnimationEnd(key)} > </div> ); } 

onRippleClick Handler, der "Wellen" erzeugt. Durch Klicken auf die Schaltfläche ermitteln wir die Größe der Schaltfläche, mit der der Kreis korrekt positioniert wird. newRippleElement alles Notwendige an die Funktion newRippleElement die wiederum einfach ein div Element mit der ripple Klasse erstellt und die für die Positionierung erforderlichen Stile erstellt.


Von den wichtigsten Dingen, die es wert sind, onAnimationEnd hervorgehoben zu onAnimationEnd . Wir benötigen dieses Ereignis, um das DOM von bereits verwendeten Elementen zu löschen.


  function onAnimationEnd(key: string) { setRippleElements(rippleElements => rippleElements.filter(element => element.key !== key)); } 

Es ist sehr wichtig, nicht zu vergessen, die aktuellen rippleElements in Argumente zu übergeben, sonst können Sie ein Array mit alten Werten erhalten, und alles wird nicht wie beabsichtigt funktionieren.


Vollständiger Tastencode:


 import React, { FunctionComponent, ButtonHTMLAttributes, useState } from 'react'; import uuid from 'uuid/v4'; import classnames from 'classnames'; export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { primary?: boolean, accent?: boolean, circle?: boolean, ripple?: boolean, additionalClass?: string, } const Button: FunctionComponent<ButtonProps> = (props) => { const [rippleElements, setRippleElements] = useState<JSX.Element[]>([]); const {primary, accent, circle, ripple, additionalClass, children} = props; const classNames = classnames( 'btn', { primary }, { 'with-ripple': ripple }, { circle }, { accent }, additionalClass ); function onAnimationEnd(key: string) { setRippleElements(rippleElements => rippleElements.filter(element => element.key !== key)); } function onRippleClick(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) { var rect = event.currentTarget.getBoundingClientRect(); const d = Math.max(event.currentTarget.clientWidth, event.currentTarget.clientHeight); const left = event.clientX - rect.left - d/2 + 'px'; const top = event.clientY - rect.top - d/2 + 'px'; const rippleElement = newRippleElement(d, left, top); setRippleElements([...rippleElements, rippleElement]); } function newRippleElement(d: number, left: string, top: string) { const key = uuid(); return ( <div key={key} className="ripple" style={{width: d, height: d, left, top}} onAnimationEnd={() => onAnimationEnd(key)} > </div> ); } function renderRippleElements() { return rippleElements; } return ( <button className={classNames} {...props} onClick={(event) => { if (props.onClick) { props.onClick(event); } if (ripple) { onRippleClick(event); } }} > {children} {renderRippleElements()} </button> ); } export default Button; 

Das Endergebnis kann hier gemeldet werden.


Fazit


Es wurde eine Menge weggelassen, zum Beispiel, wie das Projekt aufgebaut wurde, wie die Dokumentation geschrieben wurde und wie eine neue Komponente im Projekt getestet wurde. Ich werde versuchen, diese Themen in separaten Publikationen zu behandeln.


XMars UI Github-Repository

Source: https://habr.com/ru/post/de484554/


All Articles