So haben wir mit Flutter benutzerdefinierte Widgets für Überprüfungsseiten erstellt

Bei Mindinventory haben wir einige unglaublich kreative Köpfe, die in freier Wildbahn leben und wildere Ideen entwickeln. Einer von ihnen ist Ketan. Eines Tages kam er mit dieser Interaktion auf der Überprüfungsseite zu mir. Ich sah die Tweens zwischen den verschiedenen Animationen der Überprüfungsausdrücke war eine der großartig aussehenden Interaktionen, die ich je gesehen habe. Sofort haben wir uns entschlossen, sie real zu machen.

Beschlossen, es mit Flutter zu tun

Als dies passierte, habe ich mir gerade die Hände mit Flutter schmutzig gemacht. Ich habe in Flutter gesehen, dass sie buchstäblich alle Standard-UI-Steuerelemente von Android und iOS mit Dart (die Sprache, die Flutter verwendet) neu erstellt haben. Diese Widgets werden mit äußerster Präzision und Details erstellt. Trotzdem war die Leistung mit den Standard-UI-Kits vergleichbar. Obwohl es eine Reihe von maßgeschneiderten Widgets von Erstanbietern gibt, aber die Anpassung das war, was wir als unser Ziel erreichen mussten, war Dart eine neue Sprache für uns bei Mindinventory.

Es war vielen weitgehend unbekannt, bis sie Flutter einführten. Mit Flutter ist das Gute, dass flüssige Animationen mit 60 FPS leicht erreichbar sind, und genau das brauchen wir. Ich habe mich darauf gestürzt und beschlossen, die Fähigkeit von Flutter zu überprüfen, indem ich diese Widgets für die Überprüfungsseite mit erstellte Flattern. In der Zwischenzeit können Sie auch die Animation aktivieren Dribbble und Behance.

Ein Widget ist das, was wir brauchen

Eine Sache über Flutter, dass Sie eine Hassliebe haben werden, ist: Alles ist ein Widget. Von Ihrer App bis zu diesem glänzenden kleinen Kontrollkästchen und von der Handhabungsorientierung bis zur Reaktion auf Klickereignisse. Eine Art Widget ist das, was Sie brauchen. Was ich getan habe, ist Erstellen Sie in Flutter zwei benutzerdefinierte Widgets für dieses Projekt, eines ist der Smiley mit Ausdrücken und eines ist das Arc-Menü.

Lassen Sie uns ausführlich darüber sprechen, was es braucht, um dieses Meisterwerk zu schaffen.

Ausdrücke auf Leinwand rendern

Segeltuch

Benutzerdefinierten Maler erstellen

Es war einfach, eine Klasse zu definieren, die CustomPainter erweitert und uns direkten Zugriff auf Canvas ermöglicht, um nach Belieben alles darin zu zeichnen.

class SmilePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
        …rendering logic
  }
    @override
  bool shouldRepaint(CustomPainter oldDelegate) {  
    …logic to determine if we need to repaint or not
    return true;
  }
}

Und unten sehen Sie, wie es dem Layout hinzugefügt werden kann.

CustomPaint(
  size: Size(MediaQuery.of(context).size.width, (MediaQuery.of(context).size.width / 2) + 80),
  painter: SmilePainter(slideValue),
)

Status der Ausdrücke halten

Wir müssen einen Smiley mit 4 verschiedenen Ausdrücken zeichnen. Okay, Gut, Schlecht und Ugh. Hier werden Ausdrücke erstellt, um darzustellen, was der Benutzer fühlt, wenn er eine bestimmte Bewertung auswählt. Wir müssen die Augen und den Mund gemäß dem ausgewählten Ausdruck animieren.

Ausdrücke

Das erste, was mir in den Sinn kam, waren SVG-Dateien und VectorDrawables, an die ich in Android gewöhnt bin. Leider unterstützt Flutter diese Art von SVG-Animation noch nicht. Das Paket flutter_svg gab einige Hoffnung, aber es war nutzlos, da es nicht animiert werden kann Mit diesen Ausdrücken musste ich tiefer graben und fand dann diese CustomPainter-Klasse. Sie gibt uns eine Art totale Kontrolle darüber, was auf dem Bildschirm angezeigt wird (als Widget), und ich war wie Hale Luya, das ist was wir brauchen :),
Zuerst habe ich mit dem Zeichnen der Ausdrücke von vier Zuständen begonnen. Als Teil davon habe ich die ReviewState-Klasse wie folgt definiert:

class ReviewState {
  //smile end points
  Offset leftOffset;
  Offset centerOffset;
  Offset rightOffset;

  //smile handle points
  Offset leftHandle;
  Offset rightHandle;

  String title;
  Color titleColor;

  //gradient colors
  Color startColor;
  Color endColor;
}

Es enthält den Status des Renderns des Ausdrucks. In dieser Klasse habe ich behandelt

  1. Startfarbe und Endfarbe des Verlaufs
  2. Drei Mundpunkte und zwei Ankerpunkte, die die Krümmung des Mundes definieren
  3. der Titeltext und seine Farbe.

Das Definieren war der einfache Teil. Wir haben die vier Zustände definiert, einen für jeden Ausdruck. Lassen Sie uns nun überprüfen, wie wir zwischen diesen Zuständen animieren.

Tweening zwischen Staaten

Um zwischen jedem dieser Zustände zu wechseln, hatte ich einen Bereich definiert, der alle Zustände abdeckt. Ich definierte 0 bis 400 als einen Bereich, innerhalb dessen ich den Ort jedes Zustands definierte.
dh 0 wäre SCHLECHT, 100 wäre UGH, 200 wäre OK, 300 wäre GUT und schließlich wären 400 wieder SCHLECHT.

Erstellen Sie im Wesentlichen eine Schleife nach Bedarf mit dem Bogenmenü, das wir später in diesem Beitrag sehen werden.
Sie können diese Logik überprüfen, indem Sie Slider in main.dart auskommentieren

So wechseln wir zwischen den einzelnen Formen:

//create new state between given two states.
static ReviewState lerp(ReviewState start, ReviewState end, double ratio) {
  var startColor = Color.lerp(start.startColor, end.startColor, ratio);
  var endColor = Color.lerp(start.endColor, end.endColor, ratio);

  return ReviewState(
    Offset.lerp(start.leftOffset,end.leftOffset, ratio),
    Offset.lerp(start.leftHandle, end.leftHandle, ratio),
    Offset.lerp(start.centerOffset, end.centerOffset, ratio),
    Offset.lerp(start.rightHandle, end.rightHandle, ratio),
    Offset.lerp(start.rightOffset, end.rightOffset, ratio),
    startColor,
    endColor,
    start.titleColor,
    start.title);
}

Hier haben wir unsere eigene Lerp-Methode (lineare Interpolation) für unseren ReviewState unter Verwendung von Offset.lerp und Color.lerp erstellt. Diese Methode wechselt zwischen den beiden ReviewState-Instanzen. Sie erstellt Zwischenwerte für die Verlaufsfarben und die Mundkurve gemäß dem Verhältnis, das wir zur Verfügung stellen, ganz praktisch, nicht wahr? So sieht es aus, nachdem das Ereignis slide onChanged mit unserem Rendering verknüpft wurde:

Tweening des Textes

Neben den Ausdrücken, die wir auch zwischen den Texten tweeten, werden sowohl die Textposition als auch die Deckkraft übersetzt. Dazu bestimmen wir zunächst drei Positionen, die mittlere, linke und rechte Textposition, und animieren relativ zu dieser Position die Textanimation, wie Sie hier sehen können ::

tweenText(ReviewState centerReview, ReviewState rightReview, double diff,
Canvas canvas) {

  currentState = ReviewState.lerp(centerReview, rightReview, diff);

  TextSpan spanCenter = new TextSpan(
    style: new TextStyle(
      fontWeight: FontWeight.bold,
      fontSize: 52.0,
      color: centerReview.titleColor.withAlpha(255 - (255 * diff).round())
    ),
    text: centerReview.title
  );
  TextPainter tpCenter =
new TextPainter(text: spanCenter, textDirection: TextDirection.ltr);

TextSpan spanRight = new TextSpan(
  style: new TextStyle(
    fontWeight: FontWeight.bold,
    fontSize: 52.0,
    color: rightReview.titleColor.withAlpha((255 * diff).round())),
    text: rightReview.title);
    TextPainter tpRight = new TextPainter(text: spanRight, textDirection: TextDirection.ltr);
    tpCenter.layout();
    tpRight.layout();
    Offset centerOffset = new Offset(centerCenter - (tpCenter.width / 2), smileHeight);
    Offset centerToLeftOffset = new Offset(leftCenter - (tpCenter.width / 2), smileHeight);
    Offset rightOffset = new Offset(rightCenter - (tpRight.width / 2), smileHeight);
    Offset rightToCenterOffset = new Offset(centerCenter - (tpRight.width / 2), smileHeight);
    tpCenter.paint(canvas, Offset.lerp(centerOffset, centerToLeftOffset, diff));
    tpRight.paint(canvas, Offset.lerp(rightOffset, rightToCenterOffset, diff));
}

Wir berechnen den Abstand zwischen den drei Textpositionen unter Berücksichtigung des längsten Textes unter allen, der “GUT” ist. Wir führen dann das Lerp-Verfahren zwischen diesen Positionen durch und ändern gleichzeitig die Deckkraft (Alpha) so, dass die Text, der in der Mitte animiert wird, erhöht die Deckkraft, während derjenige, der die mittlere Position verlässt, ausgeblendet wird.

Animierender Ausdruck der Augen

Der aufregendste Teil dieser Ausdrücke war die Animation mit den Augen, insbesondere in BAD- und UGH-Staaten. Anfangs dachte ich über eine Pfadtransformation nach, aber das wird einen komplexen Pfaderstellungsprozess beinhalten. Später entschied ich mich für einen einfachen und praktischen Weg, der das Zeichnen war diese Ausschnitte über den Augen selbst, die im Wesentlichen den gewünschten Effekt erzeugen:
Für die Augenanimation des BAD-Ausdrucks haben wir ein Dreieck über die Augen gezeichnet, dessen ein Punkt je nach aktuellem Status verkleinert und erweitert wird. Schauen wir uns den Code für denselben an:

// zeichne die Kanten von BAD Review

if (slideValue <= 100 || slideValue > 300) {
  double diff = -1.0;
  double tween = -1.0;
  if (slideValue <= 100) {
    diff = slideValue / 100;
    tween = lerpDouble(eyeY-(eyeRadiusbythree*0.6), eyeY-eyeRadius, diff);
  } else if (slideValue > 300) {
    diff = (slideValue - 300) / 100;
    tween = lerpDouble(eyeY-eyeRadius, eyeY-(eyeRadiusbythree*0.6), diff);
  }
  List<Offset> polygonPath = List<Offset>();
  polygonPath.add(Offset(leftEyeX-eyeRadiusbytwo, eyeY-eyeRadius));
  polygonPath.add(Offset(leftEyeX+eyeRadius, tween));
  polygonPath.add(Offset(leftEyeX+eyeRadius, eyeY-eyeRadius));
  Path clipPath = new Path();
  clipPath.addPolygon(polygonPath, true);
  canvas.drawPath(clipPath, whitePaint);
  List<Offset> polygonPath2 = List<Offset>();
  polygonPath2.add(Offset(rightEyeX+eyeRadiusbytwo, eyeY-eyeRadius));
  polygonPath2.add(Offset(rightEyeX-eyeRadius, tween));
  polygonPath2.add(Offset(rightEyeX-eyeRadius, eyeY-eyeRadius));
  Path clipPath2 = new Path();
  clipPath2.addPolygon(polygonPath2, true);
  canvas.drawPath(clipPath2, whitePaint);
}

Um die Augäpfel für den UGH-Zustand zu zeichnen, zeichnen wir Kreise über die Augen. Um den Augapfel-Expansions- und Schrumpfeffekt zu erzielen, ändern wir ein Ende des Augapfels Rect Wir zeichnen Augapfelkreise. So machen wir das:

//draw the balls of UGH Review
if (slideValue > 0 && slideValue < 200) {
  double diff = -1.0;
  double leftTweenX = -1.0;
  double leftTweenY = -1.0;
  double rightTweenX = -1.0;
  double rightTweenY = -1.0;
  if (slideValue <= 100) {
    // bad to ugh
    diff = slideValue / 100;
    leftTweenX = lerpDouble(leftEyeX-eyeRadius, leftEyeX, diff);
    leftTweenY = lerpDouble(eyeY-eyeRadius, eyeY, diff);
    rightTweenX = lerpDouble(rightEyeX+eyeRadius, rightEyeX, diff);
    rightTweenY = lerpDouble(eyeY, eyeY-(eyeRadius+eyeRadiusbythree), diff);
  } else {
    // ugh to ok
    diff = (slideValue - 100) / 100;
    leftTweenX = lerpDouble(leftEyeX, leftEyeX-eyeRadius, diff);
    leftTweenY = lerpDouble(eyeY, eyeY-eyeRadius, diff);
    rightTweenX = lerpDouble(rightEyeX, rightEyeX+eyeRadius, diff);
    rightTweenY = lerpDouble(eyeY-(eyeRadius+eyeRadiusbythree), eyeY, diff);
  }
  canvas.drawOval(Rect.fromLTRB(leftEyeX-(eyeRadius+eyeRadiusbythree),
  eyeY-(eyeRadius+eyeRadiusbythree), leftTweenX, leftTweenY), whitePaint);
  canvas.drawOval(Rect.fromLTRB(rightTweenX, eyeY, rightEyeX+(eyeRadius+eyeRadiusbythree),eyeY-(eyeRadius+eyeRadiusbythree)), whitePaint);
}

Erstellen eines Widgets für das interaktive Bogenmenü

Zu Beginn dieses Beitrags haben wir gesehen, wie wir die Ausdrücke zeichnen. Es war nicht interaktiv und muss von einem anderen Steuerelement ausgelöst werden, entweder vom Slider-Widget oder von diesem Bogen-Widget, das wir sehen werden.

Für dieses Steuerelement habe ich beschlossen, das bisher Gelernte zusammenzufassen. In diesem Widget müssen wir auch einen CustomPainter erstellen, diesmal jedoch in ein Widget namens ArcChooser einbinden, um die Abstraktion und Struktur zu verbessern. Dies muss StatefulWidget sein, wie wir es benötigen Um den Status zu ändern, wenn die Interaktion vom Benutzer ausgeführt wird, haben wir die Statusklasse ChooserState und einen Painter namens ChooserPainter. Mit dieser Implementierung definieren wir die gesamte Logikberechnung innerhalb der Statusklasse, während die Rendering-bezogenen Elemente in Painter wie erwartet beibehalten werden.

Definieren und Rendern von Arc-Elementen

In unserem Fall müssen wir einen unendlichen Bogen erstellen, der weitergeht, unabhängig davon, in welche Richtung der Benutzer scrollt. Er wird in kreisenden Bewegungen weiter scrollen. Um dies zu erreichen, definieren wir 8 Bogenelemente, die jeweils 45 ° als offensichtliche Handlung benötigen Die folgende Klasse wurde erstellt, um ein Arc-Menüelement programmgesteuert darzustellen.

class ArcItem {
  String text;
  List<Color> colors;
  double startAngle;
  ArcItem(this.text, this.colors, this.startAngle);
}

Wir übergeben diese Elemente an unseren ChooserPainter, um sie weiter auf dem Bildschirm zu zeichnen. Mal sehen, was wir darin tun:

var dummyRect = Rect.fromLTRB(0.0, 0.0, size.width, size.height);
for (int i = 0; i < arcItems.length; i++) {
  canvas.drawArc(
    arcRect,
    arcItems[i].startAngle,
    angleInRadians,
    true,
    new Paint()
    ..style = PaintingStyle.fill
    ..shader = new LinearGradient(
    colors: arcItems[i].colors,
  ).createShader(dummyRect));
  //Draw text
  TextSpan span = new TextSpan(style: new TextStyle(fontWeight: FontWeight.normal, fontSize: 32.0, color: Colors.white), text: arcItems[i].text);
  TextPainter tp = new TextPainter(text: span, textAlign: TextAlign.center, textDirection: TextDirection.ltr,);
  tp.layout();
  //find additional angle to make text in center
  double f = tp.width/2;
  double t = sqrt((radiusText*radiusText) + (f*f));
  double additionalAngle = acos(((t*t) + (radiusText*radiusText)-(f*f))/(2*t*radiusText));
  double tX = center.dx + radiusText*cos(arcItems[i].startAngle+angleInRadiansByTwo - additionalAngle);// - (tp.width/2);
  double tY = center.dy + radiusText*sin(arcItems[i].startAngle+angleInRadiansByTwo - additionalAngle);// - (tp.height/2);
  canvas.save();
  canvas.translate(tX,tY);
  // canvas.rotate(arcItems[i].startAngle + angleInRadiansByTwo);
  canvas.rotate(arcItems[i].startAngle+angleInRadians+angleInRadians+angleInRadiansByTwo);
  tp.paint(canvas, new Offset(0.0,0.0));
  canvas.restore();
  //big lines
  canvas.drawLine( new Offset(center.dx + radius4*cos(arcItems[i].startAngle), center.dy + radius4*sin(arcItems[i].startAngle)), center, linePaint);
  canvas.drawLine( new Offset(center.dx + radius4*cos(arcItems[i].startAngle+angleInRadiansByTwo), center.dy + radius4*sin(arcItems[i].startAngle+angleInRadiansByTwo)), center, linePaint);
  //small lines
  canvas.drawLine( new Offset(center.dx + radius5*cos(arcItems[i].startAngle+angleInRadians1), center.dy + radius5*sin(arcItems[i].startAngle+angleInRadians1)), center, linePaint);
  canvas.drawLine(
new Offset(center.dx + radius5*cos(arcItems[i].startAngle+angleInRadians2), center.dy + radius5*sin(arcItems[i].startAngle+angleInRadians2)), center, linePaint);
  canvas.drawLine( new Offset(center.dx + radius5*cos(arcItems[i].startAngle+angleInRadians3), center.dy + radius5*sin(arcItems[i].startAngle+angleInRadians3)), center, linePaint);
  canvas.drawLine( new Offset(center.dx + radius5*cos(arcItems[i].startAngle+angleInRadians4), center.dy + radius5*sin(arcItems[i].startAngle+angleInRadians4)), center, linePaint);
}

Wie Sie in diesem Code sehen können, war das Zeichnen von Bögen unkompliziert. Für das Malen von Farbverläufen innerhalb der Bogenelemente mussten wir für jedes Element ein Farbverlaufsobjekt erstellen.

Abgesehen davon zeichnen wir viele andere Dinge. Wir müssen die Beschriftung des Bogenelements in einem bestimmten Winkel rendern, da der Benutzer mit unserem Bogenmenü interagiert. Das Zeichnen von Text in einem bestimmten Winkel erfordert die folgenden Schritte:

  1. Finden Sie die genaue x, y-Position, an der Text in der Mitte des Bogens gezeichnet wird, und berücksichtigen Sie dabei die Breite des Texts
  2. Zeichnen Sie es im rechten Winkel, was erfordert, dass wir die Leinwand in einem bestimmten Winkel drehen, unseren Text zeichnen und die Leinwand wiederherstellen.

Wir berechnen auch verschiedene Radien, um den Text, die Linien, den Schatten und den weißen Bogen zu zeichnen.
Eine Sache, die ich bei Flutter neu fand, war, dass es uns erlaubt, Schatten zu zeichnen :), ich habe so etwas nirgendwo anders gefunden. Siehe hier ist unser Ergebnis:

Schatten

Umgang mit Benutzerinteraktion

Wenn der Benutzer mit dem Bogenmenü interagiert, müssen wir den Bogen entsprechend drehen. Dabei müssen zwei Szenarien behandelt werden:

  1. wenn der Benutzer den Bogen aktiv zieht
  2. wenn der Benutzer den Lichtbogen schleudert.

So gehen wir mit dem GestureDetector-Widget um:

GestureDetector(
  onPanStart: (DragStartDetails details) {
    startingPoint = details.globalPosition;
    var deltaX = centerPoint.dx - details.globalPosition.dx;
    var deltaY = centerPoint.dy - details.globalPosition.dy;
    startAngle = atan2(deltaY, deltaX);
  },
  onPanUpdate: (DragUpdateDetails details) {
    endingPoint = details.globalPosition;
    var deltaX = centerPoint.dx - details.globalPosition.dx;
    var deltaY = centerPoint.dy - details.globalPosition.dy;
    var freshAngle = atan2(deltaY, deltaX);
    userAngle += freshAngle - startAngle;
    setState(() {
      for (int i = 0; i < arcItems.length; i++) {
        arcItems[i].startAngle = angleInRadiansByTwo + userAngle + (i * angleInRadians);
      }
    });
    startAngle = freshAngle;
  },
  onPanEnd: (DragEndDetails details){
    //find top arc item with Magic!!
    bool rightToLeft = startingPoint.dx<endingPoint.dx;
    //Animate it from this values
    animationStart = userAngle;
    if(rightToLeft) {
      animationEnd +=angleInRadians;
      currentPosition--;
      if(currentPosition<0){
        currentPosition = arcItems.length-1;
      }
    }else{
      animationEnd -=angleInRadians;
      currentPosition++;
      if(currentPosition>=arcItems.length){
        currentPosition = 0;
      }
    }
    if(arcSelectedCallback!=null){
      arcSelectedCallback(currentPosition, arcItems[(currentPosition>=(arcItems.length-1))?0:currentPosition+1]);
    }
    animation.forward(from: 0.0);
  },
  child: CustomPaint(
    size: Size(MediaQuery.of(context).size.width,
    MediaQuery.of(context).size.width / 2),
    painter: ChooserPainter(arcItems, angleInRadians),
  ),
)

Wenn der Benutzer seine Berührungsgeste beendet hat, müssen wir unser Bogenmenü gemäß der Flugrichtung auf das am besten geeignete Bogenelement bringen. Da wir AnimationController verwenden, ist es wunderbar, dass Sie einen Bereich von Animationswerten definieren und ausführen können eine Teilmenge davon zu einem bestimmten Zeitpunkt,

Überprüfen Sie, wie wir die Änderung der Ausdrücke animieren, wenn wir eine Änderung des aktuellen Elements feststellen. Diese wird über unsere Rückrufimplementierung ArcSelectedCallback ausgelöst. So haben wir den Rückruf verwendet:

ArcChooser()
..arcSelectedCallback = (int pos, ArcItem item) {
  int animPosition = pos - 2;
  if (animPosition > 3) {
    animPosition = animPosition - 4;
  }
  if (animPosition < 0) {
    animPosition = 4 + animPosition;
  }
  if (lastAnimPosition == 3 && animPosition == 0) {
    animation.animateTo(4 * 100.0);
  } else if (lastAnimPosition == 0 && animPosition == 3) {
    animation.forward(from: 4 * 100.0);
    animation.animateTo(animPosition * 100.0);
  } else if (lastAnimPosition == 0 && animPosition == 1) {
    animation.forward(from: 0.0);
    animation.animateTo(animPosition * 100.0);
  } else {
    animation.animateTo(animPosition * 100.0);
  }
  lastAnimPosition = animPosition;
}

Wir befinden uns auf unserem letzten Stand. Wie Sie vielleicht wissen, funktioniert dies unter Android und iOS einwandfrei. Probieren Sie es aus:

Rezension

Überprüfen Sie den vollständigen Quellcode der Interaktion “Überprüfungsseite” auf Github

Similar Posts

Leave a Reply