{"id":12332,"date":"2018-07-20T06:44:18","date_gmt":"2018-07-20T06:44:18","guid":{"rendered":"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/"},"modified":"2025-11-11T05:36:39","modified_gmt":"2025-11-11T05:36:39","slug":"create-review-page-custom-widgets-with-flutter","status":"publish","type":"post","link":"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/","title":{"rendered":"How we created Review Page Custom widgets with Flutter"},"content":{"rendered":"\n<p>At Mindinventory we have some incredibly creative minds, that live in wild and come up with wilder ideas, One of them is Ketan, One day he came to me with this Review page interaction, I saw the tweens between the different review expressions animations, It was one of the awesome looking interactions I ever saw, Promptly we decided to make it real.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-decided-to-do-it-with-flutter\"><span class=\"ez-toc-section\" id=\"Decided_to_do_it_with_Flutter\"><\/span>Decided to do it with Flutter<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>When this happened, I was just getting my hands dirty with Flutter, I saw in Flutter they have literally recreated all the default ui controls of Android and iOS using Dart(The language Flutter uses), This widgets are made with utmost precision and details, still the performance was par with the default ui kits, Although there are bunch of tailor made widgets available from first party but customisation was what we need to achieve as our goal, Dart was a new language for us at Mindinventory.<\/p>\n\n\n\n<p>It was largely unknown to many until they introduced Flutter, With Flutter the good thing is 60 FPS smooth animations are easily achievable and that is what we need, I jumped on it and decided to check the ability of Flutter by creating this Review Page widgets with Flutter. Meanwhile you can also check the animation on <a href=\"https:\/\/dribbble.com\/shots\/4332677-Review-Page-Interaction\">Dribbble<\/a> and <a href=\"https:\/\/www.behance.net\/gallery\/63142605\/Review-Page-Interaction\">Behance<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-a-widget-is-what-we-need\"><span class=\"ez-toc-section\" id=\"A_Widget_is_what_we_need\"><\/span>A Widget is what we need<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>One thing about Flutter that you will have love-hate relationship is Everything is a Widget, From your App to that shiney little checkbox and from Handling orientation to reacting to click events, A type of widget is what you need, What I have done is create two custom widgets in Flutter for this project, one is the Smiley with Expressions and another is the Arc menu,<\/p>\n\n\n\n<p>Let\u2019s talk in depth about what it takes to create this masterpiece.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-rendering-expressions-on-canvas\"><span class=\"ez-toc-section\" id=\"Rendering_expressions_on_canvas\"><\/span>Rendering expressions on canvas<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter\"><img loading=\"lazy\" decoding=\"async\" width=\"290\" height=\"292\" src=\"https:\/\/www.mindinventory.com\/blog\/wp-content\/uploads\/2022\/10\/canvas.png\" alt=\"canvas\" class=\"wp-image-3635\"\/><\/figure><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"h-creating-custom-painter\">Creating custom painter<\/h3>\n\n\n\n<p>It was straightforward to define a class that extends CustomPainter which directly gives us access to Canvas to draw anything in it at will.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>class SmilePainter extends CustomPainter {\n  @override\n  void paint(Canvas canvas, Size size) {\n        \u2026rendering logic\n  }\n    @override\n  bool shouldRepaint(CustomPainter oldDelegate) {  \n    \u2026logic to determine if we need to repaint or not\n    return true;\n  }\n}<\/code>\n<\/pre>\n\n\n\n<p>And below is how it can be added to layout.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>CustomPaint(\n  size: Size(MediaQuery.of(context).size.width, (MediaQuery.of(context).size.width \/ 2) + 80),\n  painter: SmilePainter(slideValue),\n)<\/code>\n<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-holding-state-of-expressions\">Holding State of Expressions<\/h3>\n\n\n\n<p>We have to draw smiley with 4 different expressions Okay, Good, Bad and Ugh, Here expressions are made to represent what user feel when he choose particular rating, What we got to do is Animate the Eyes and Mouth according to the selected expression.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter\"><img loading=\"lazy\" decoding=\"async\" width=\"725\" height=\"158\" src=\"https:\/\/www.mindinventory.com\/blog\/wp-content\/uploads\/2022\/10\/Expressions.png\" alt=\"Expressions\" class=\"wp-image-3636\"\/><\/figure><\/div>\n\n\n<p>First thing came to my mind was SVG files and VectorDrawables I am used to in Android, sadly Flutter doesn&#8217;t support this kind of SVG Animation just yet, flutter_svg package gave some hope but It was of no use as it is not possible to animate this expressions with it, I had to dig deeper and then I found this CustomPainter class, It gives us kind of total control over what is shown on screen(as widget), and I was like hale luya that is what we need :),<br>First I started with drawing the expressions of four states, as part of it I defined ReviewState class as follows:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>class ReviewState {\n  \/\/smile end points\n  Offset leftOffset;\n  Offset centerOffset;\n  Offset rightOffset;\n\n  \/\/smile handle points\n  Offset leftHandle;\n  Offset rightHandle;\n\n  String title;\n  Color titleColor;\n\n  \/\/gradient colors\n  Color startColor;\n  Color endColor;\n}<\/code>\n<\/pre>\n\n\n\n<p>It holds state of rendering the expression, In this class I covered<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Gradient start color and end color<\/li>\n\n\n\n<li>Three points of the mouth and two anchor points which define the curvature of the mouth<\/li>\n\n\n\n<li>the title text and it\u2019s color.<\/li>\n<\/ol>\n\n\n\n<p>Defining was the easy part, We defined the four states, one for each expression, Now let\u2019s check how we animate between those states.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-tweening-between-states\">Tweening between states<\/h3>\n\n\n\n<p>To tween between each of this state I had defined a range which covers all the states, I defined 0 to 400 as a range, within which I defined location of each state,<br>i. e. 0 would be BAD, 100 would be UGH, 200 would be OK, 300 would be GOOD and finally 400 would be BAD again.<\/p>\n\n\n\n<p>Essentially creating a loop as we need with the arc menu, which we will see later in this post.<br>You can check this logic by uncommenting Slider in main.dart<\/p>\n\n\n\n<p>Here is how we tween between each of the shapes:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>\/\/create new state between given two states.\nstatic ReviewState lerp(ReviewState start, ReviewState end, double ratio) {\n  var startColor = Color.lerp(start.startColor, end.startColor, ratio);\n  var endColor = Color.lerp(start.endColor, end.endColor, ratio);\n\n  return ReviewState(\n    Offset.lerp(start.leftOffset,end.leftOffset, ratio),\n    Offset.lerp(start.leftHandle, end.leftHandle, ratio),\n    Offset.lerp(start.centerOffset, end.centerOffset, ratio),\n    Offset.lerp(start.rightHandle, end.rightHandle, ratio),\n    Offset.lerp(start.rightOffset, end.rightOffset, ratio),\n    startColor,\n    endColor,\n    start.titleColor,\n    start.title);\n}<\/code>\n<\/pre>\n\n\n\n<p>Here we have created our own lerp(linear interpolation) method for our ReviewState while utilising Offset.lerp and Color.lerp, this methods will tween between the two ReviewState instances, It will create intermediate values for the gradient colors and mouth curve according to the ratio we provide, quite handy isn\u2019t it? Here is how it looks after linking the slide onChanged event with our rendering:<\/p>\n\n\n\n<figure class=\"wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio\"><div class=\"wp-block-embed__wrapper\">\n<iframe loading=\"lazy\" title=\"tweening\" width=\"500\" height=\"281\" src=\"https:\/\/www.youtube.com\/embed\/UKj8nzPunpI?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen><\/iframe>\n<\/div><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-tweening-the-text\">Tweening the Text<\/h3>\n\n\n\n<p>Alongside the Expressions we are also tweening the text, it will translate text position as well as opacity, for that we initially determine three positions, center, left and right text position and relative to that position we animate the Text animation as you can see here:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>tweenText(ReviewState centerReview, ReviewState rightReview, double diff,\nCanvas canvas) {\n\n  currentState = ReviewState.lerp(centerReview, rightReview, diff);\n\n  TextSpan spanCenter = new TextSpan(\n    style: new TextStyle(\n      fontWeight: FontWeight.bold,\n      fontSize: 52.0,\n      color: centerReview.titleColor.withAlpha(255 - (255 * diff).round())\n    ),\n    text: centerReview.title\n  );\n  TextPainter tpCenter =\nnew TextPainter(text: spanCenter, textDirection: TextDirection.ltr);\n\nTextSpan spanRight = new TextSpan(\n  style: new TextStyle(\n    fontWeight: FontWeight.bold,\n    fontSize: 52.0,\n    color: rightReview.titleColor.withAlpha((255 * diff).round())),\n    text: rightReview.title);\n    TextPainter tpRight = new TextPainter(text: spanRight, textDirection: TextDirection.ltr);\n    tpCenter.layout();\n    tpRight.layout();\n    Offset centerOffset = new Offset(centerCenter - (tpCenter.width \/ 2), smileHeight);\n    Offset centerToLeftOffset = new Offset(leftCenter - (tpCenter.width \/ 2), smileHeight);\n    Offset rightOffset = new Offset(rightCenter - (tpRight.width \/ 2), smileHeight);\n    Offset rightToCenterOffset = new Offset(centerCenter - (tpRight.width \/ 2), smileHeight);\n    tpCenter.paint(canvas, Offset.lerp(centerOffset, centerToLeftOffset, diff));\n    tpRight.paint(canvas, Offset.lerp(rightOffset, rightToCenterOffset, diff));\n}<\/code>\n<\/pre>\n\n\n\n<p>We are calculate the distance between the three text positions considering the longest text among all, which is \u2018GOOD\u2019, We are then doing the lerp procedure between those positions and at the same time we are changing opacity(alpha) in a way that the text animating to the center will increase it\u2019s opacity while the one leaving center position will fade out.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-animating-expression-of-eyes\">Animating expression of Eyes<\/h3>\n\n\n\n<p>The most exciting part of this expressions was animation with the eyes, especially in BAD and UGH states, Initially I thought of doing path transformation but that will include complex path creation process, later I decided to go with easy and practical way, which was drawing that cutouts over the eyes it self, essentially creating the desired effect:<br>For Eyes animation of BAD expression, what we did is draw a triangle above the eyes, whose one point will shrink and expand according to current state, Let\u2019s have a look at the code for same:<\/p>\n\n\n\n<p>\/\/draw the edges of BAD Review<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>if (slideValue &lt;= 100 || slideValue &gt; 300) {\n  double diff = -1.0;\n  double tween = -1.0;\n  if (slideValue &lt;= 100) {\n    diff = slideValue \/ 100;\n    tween = lerpDouble(eyeY-(eyeRadiusbythree*0.6), eyeY-eyeRadius, diff);\n  } else if (slideValue &gt; 300) {\n    diff = (slideValue - 300) \/ 100;\n    tween = lerpDouble(eyeY-eyeRadius, eyeY-(eyeRadiusbythree*0.6), diff);\n  }\n  List&lt;Offset&gt; polygonPath = List&lt;Offset&gt;();\n  polygonPath.add(Offset(leftEyeX-eyeRadiusbytwo, eyeY-eyeRadius));\n  polygonPath.add(Offset(leftEyeX+eyeRadius, tween));\n  polygonPath.add(Offset(leftEyeX+eyeRadius, eyeY-eyeRadius));\n  Path clipPath = new Path();\n  clipPath.addPolygon(polygonPath, true);\n  canvas.drawPath(clipPath, whitePaint);\n  List&lt;Offset&gt; polygonPath2 = List&lt;Offset&gt;();\n  polygonPath2.add(Offset(rightEyeX+eyeRadiusbytwo, eyeY-eyeRadius));\n  polygonPath2.add(Offset(rightEyeX-eyeRadius, tween));\n  polygonPath2.add(Offset(rightEyeX-eyeRadius, eyeY-eyeRadius));\n  Path clipPath2 = new Path();\n  clipPath2.addPolygon(polygonPath2, true);\n  canvas.drawPath(clipPath2, whitePaint);\n}<\/code>\n<\/pre>\n\n\n\n<p>For drawing the eyeballs for UGH state we are drawing circles above the eyes, To achieve the eyeball expand and shrink effect we are changing one end of the <em>Rect<\/em> we use to draw eyeball circles, Here is how we do it:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>\/\/draw the balls of UGH Review\nif (slideValue &gt; 0 &amp;&amp; slideValue &lt; 200) {\n  double diff = -1.0;\n  double leftTweenX = -1.0;\n  double leftTweenY = -1.0;\n  double rightTweenX = -1.0;\n  double rightTweenY = -1.0;\n  if (slideValue &lt;= 100) {\n    \/\/ bad to ugh\n    diff = slideValue \/ 100;\n    leftTweenX = lerpDouble(leftEyeX-eyeRadius, leftEyeX, diff);\n    leftTweenY = lerpDouble(eyeY-eyeRadius, eyeY, diff);\n    rightTweenX = lerpDouble(rightEyeX+eyeRadius, rightEyeX, diff);\n    rightTweenY = lerpDouble(eyeY, eyeY-(eyeRadius+eyeRadiusbythree), diff);\n  } else {\n    \/\/ ugh to ok\n    diff = (slideValue - 100) \/ 100;\n    leftTweenX = lerpDouble(leftEyeX, leftEyeX-eyeRadius, diff);\n    leftTweenY = lerpDouble(eyeY, eyeY-eyeRadius, diff);\n    rightTweenX = lerpDouble(rightEyeX, rightEyeX+eyeRadius, diff);\n    rightTweenY = lerpDouble(eyeY-(eyeRadius+eyeRadiusbythree), eyeY, diff);\n  }\n  canvas.drawOval(Rect.fromLTRB(leftEyeX-(eyeRadius+eyeRadiusbythree),\n  eyeY-(eyeRadius+eyeRadiusbythree), leftTweenX, leftTweenY), whitePaint);\n  canvas.drawOval(Rect.fromLTRB(rightTweenX, eyeY, rightEyeX+(eyeRadius+eyeRadiusbythree),eyeY-(eyeRadius+eyeRadiusbythree)), whitePaint);\n}<\/code>\n<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-creating-interactive-arc-menu-widget\"><span class=\"ez-toc-section\" id=\"Creating_Interactive_Arc_menu_widget\"><\/span>Creating Interactive Arc menu widget<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p>Earlier in this post what we saw was how we are drawing the expressions, it was non interactive and it needs to be triggered by other control, either by the Slider widget or this arc widget we will see,<\/p>\n\n\n\n<p>For this control I decided to add up what I learned till now, In this widget we need to create a CustomPainter too but this time decided to wrap it inside a widget called ArcChooser for better abstraction and structure, This needs to be StatefulWidget as we need to change states when interaction is done by user so we have state class called ChooserState and a Painter called ChooserPainter, With this implementation we are defining all the logic calc inside the state class while keeping the rendering related stuff in Painter as expected.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-defining-and-rendering-arc-items\">Defining and Rendering Arc items<\/h3>\n\n\n\n<p>In our case we need to create an infinite arc that keeps going on, regardless of which direction user scrolls, it will keep scrolling in circular motion, To achieve that we are defining 8 arc items which will take 45\u00b0 each as obvious act I have created following class to programatically represent an Arc menu Item:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>class ArcItem {\n  String text;\n  List&lt;Color&gt; colors;\n  double startAngle;\n  ArcItem(this.text, this.colors, this.startAngle);\n}<\/code>\n<\/pre>\n\n\n\n<p>We are passing this items to our ChooserPainter to further get it drawn on screen, Let\u2019s see what we do in it:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>var dummyRect = Rect.fromLTRB(0.0, 0.0, size.width, size.height);\nfor (int i = 0; i &lt; arcItems.length; i++) {\n  canvas.drawArc(\n    arcRect,\n    arcItems[i].startAngle,\n    angleInRadians,\n    true,\n    new Paint()\n    ..style = PaintingStyle.fill\n    ..shader = new LinearGradient(\n    colors: arcItems[i].colors,\n  ).createShader(dummyRect));\n  \/\/Draw text\n  TextSpan span = new TextSpan(style: new TextStyle(fontWeight: FontWeight.normal, fontSize: 32.0, color: Colors.white), text: arcItems[i].text);\n  TextPainter tp = new TextPainter(text: span, textAlign: TextAlign.center, textDirection: TextDirection.ltr,);\n  tp.layout();\n  \/\/find additional angle to make text in center\n  double f = tp.width\/2;\n  double t = sqrt((radiusText*radiusText) + (f*f));\n  double additionalAngle = acos(((t*t) + (radiusText*radiusText)-(f*f))\/(2*t*radiusText));\n  double tX = center.dx + radiusText*cos(arcItems[i].startAngle+angleInRadiansByTwo - additionalAngle);\/\/ - (tp.width\/2);\n  double tY = center.dy + radiusText*sin(arcItems[i].startAngle+angleInRadiansByTwo - additionalAngle);\/\/ - (tp.height\/2);\n  canvas.save();\n  canvas.translate(tX,tY);\n  \/\/ canvas.rotate(arcItems[i].startAngle + angleInRadiansByTwo);\n  canvas.rotate(arcItems[i].startAngle+angleInRadians+angleInRadians+angleInRadiansByTwo);\n  tp.paint(canvas, new Offset(0.0,0.0));\n  canvas.restore();\n  \/\/big lines\n  canvas.drawLine( new Offset(center.dx + radius4*cos(arcItems[i].startAngle), center.dy + radius4*sin(arcItems[i].startAngle)), center, linePaint);\n  canvas.drawLine( new Offset(center.dx + radius4*cos(arcItems[i].startAngle+angleInRadiansByTwo), center.dy + radius4*sin(arcItems[i].startAngle+angleInRadiansByTwo)), center, linePaint);\n  \/\/small lines\n  canvas.drawLine( new Offset(center.dx + radius5*cos(arcItems[i].startAngle+angleInRadians1), center.dy + radius5*sin(arcItems[i].startAngle+angleInRadians1)), center, linePaint);\n  canvas.drawLine(\nnew Offset(center.dx + radius5*cos(arcItems[i].startAngle+angleInRadians2), center.dy + radius5*sin(arcItems[i].startAngle+angleInRadians2)), center, linePaint);\n  canvas.drawLine( new Offset(center.dx + radius5*cos(arcItems[i].startAngle+angleInRadians3), center.dy + radius5*sin(arcItems[i].startAngle+angleInRadians3)), center, linePaint);\n  canvas.drawLine( new Offset(center.dx + radius5*cos(arcItems[i].startAngle+angleInRadians4), center.dy + radius5*sin(arcItems[i].startAngle+angleInRadians4)), center, linePaint);\n}<\/code>\n<\/pre>\n\n\n\n<p>As you can see in this code, drawing arcs was straight forward, just that for painting gradient inside the arc items required us to create a gradient paint object for each item.<\/p>\n\n\n\n<p>Apart from that we are drawing lot of other stuff, We need to render the arc item label in particular angle as user does interaction with our Arc menu, Drawing text in particular angle requires following steps:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>find the exact x,y position which draws text in center of arc, keeping in mind the width of text<\/li>\n\n\n\n<li>draw it in right angle, which requires us to rotate the canvas in given angle, draw our text and restore the canvas,<\/li>\n<\/ol>\n\n\n\n<p>We are also calculating different radius to draw the text, the lines, shadow and the white arc,<br>One thing I found new with Flutter was that It allows us to draw Shadow :), I didn\u2019t find such thing anywhere else, See here is our outcome:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter\"><img loading=\"lazy\" decoding=\"async\" width=\"740\" height=\"481\" src=\"https:\/\/www.mindinventory.com\/blog\/wp-content\/uploads\/2022\/10\/Shadow.png\" alt=\"Shadow\" class=\"wp-image-3637\"\/><\/figure><\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"h-handling-user-interaction\">Handling user interaction<\/h3>\n\n\n\n<p>When user does interaction with arc menu, we need to rotate the arc accordingly, we need to handle two scenarios in this,<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>when user is actively dragging the arc<\/li>\n\n\n\n<li>when user flings the arc.<\/li>\n<\/ol>\n\n\n\n<p>Here is how we handle it with GestureDetector widget:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>GestureDetector(\n  onPanStart: (DragStartDetails details) {\n    startingPoint = details.globalPosition;\n    var deltaX = centerPoint.dx - details.globalPosition.dx;\n    var deltaY = centerPoint.dy - details.globalPosition.dy;\n    startAngle = atan2(deltaY, deltaX);\n  },\n  onPanUpdate: (DragUpdateDetails details) {\n    endingPoint = details.globalPosition;\n    var deltaX = centerPoint.dx - details.globalPosition.dx;\n    var deltaY = centerPoint.dy - details.globalPosition.dy;\n    var freshAngle = atan2(deltaY, deltaX);\n    userAngle += freshAngle - startAngle;\n    setState(() {\n      for (int i = 0; i &lt; arcItems.length; i++) {\n        arcItems[i].startAngle = angleInRadiansByTwo + userAngle + (i * angleInRadians);\n      }\n    });\n    startAngle = freshAngle;\n  },\n  onPanEnd: (DragEndDetails details){\n    \/\/find top arc item with Magic!!\n    bool rightToLeft = startingPoint.dx&lt;endingPoint.dx;\n    \/\/Animate it from this values\n    animationStart = userAngle;\n    if(rightToLeft) {\n      animationEnd +=angleInRadians;\n      currentPosition--;\n      if(currentPosition&lt;0){\n        currentPosition = arcItems.length-1;\n      }\n    }else{\n      animationEnd -=angleInRadians;\n      currentPosition++;\n      if(currentPosition&gt;=arcItems.length){\n        currentPosition = 0;\n      }\n    }\n    if(arcSelectedCallback!=null){\n      arcSelectedCallback(currentPosition, arcItems[(currentPosition&gt;=(arcItems.length-1))?0:currentPosition+1]);\n    }\n    animation.forward(from: 0.0);\n  },\n  child: CustomPaint(\n    size: Size(MediaQuery.of(context).size.width,\n    MediaQuery.of(context).size.width \/ 2),\n    painter: ChooserPainter(arcItems, angleInRadians),\n  ),\n)<\/code>\n<\/pre>\n\n\n\n<p>Here when user finish his touch gesture, we need to bring our arc menu to most eligible arc item as per the direction of fling, For that we are using AnimationController, Wonderful thing about AnimationController is that you can define a range of Animation value and run a subset of it at any given time,<\/p>\n\n\n\n<p>Check how we are animating the change in expressions when we detect change in current item, it is triggered via our Callback implementation ArcSelectedCallback, here is how we have consume the callback:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>ArcChooser()\n..arcSelectedCallback = (int pos, ArcItem item) {\n  int animPosition = pos - 2;\n  if (animPosition &gt; 3) {\n    animPosition = animPosition - 4;\n  }\n  if (animPosition &lt; 0) {\n    animPosition = 4 + animPosition;\n  }\n  if (lastAnimPosition == 3 &amp;&amp; animPosition == 0) {\n    animation.animateTo(4 * 100.0);\n  } else if (lastAnimPosition == 0 &amp;&amp; animPosition == 3) {\n    animation.forward(from: 4 * 100.0);\n    animation.animateTo(animPosition * 100.0);\n  } else if (lastAnimPosition == 0 &amp;&amp; animPosition == 1) {\n    animation.forward(from: 0.0);\n    animation.animateTo(animPosition * 100.0);\n  } else {\n    animation.animateTo(animPosition * 100.0);\n  }\n  lastAnimPosition = animPosition;\n}<\/code>\n<\/pre>\n\n\n\n<p>We are on our last stand, As you might know this will work flawlessly on Android and iOS, check it out:<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter\"><img loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"600\" src=\"https:\/\/www.mindinventory.com\/blog\/wp-content\/uploads\/2022\/10\/smile.gif\" alt=\"review\" class=\"wp-image-3651\"\/><\/figure><\/div>\n\n\n<p>Check full source code of Review Page Interaction on <a href=\"https:\/\/github.com\/Mindinventory\/Flutter-review-page-interaction\">Github<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>At Mindinventory we have some incredibly creative minds, that live in wild and come up with wilder ideas, One of them is Ketan, One day he came to me with this Review page interaction, I saw the tweens between the different review expressions animations, It was one of the awesome looking interactions I ever saw, [&hellip;]<\/p>\n","protected":false},"author":17,"featured_media":12338,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"inline_featured_image":false,"footnotes":""},"categories":[1434],"tags":[1936,2011,1877],"industries":[],"class_list":["post-12332","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-mobile","tag-flutter","tag-review-page","tag-ui-design"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v19.3 (Yoast SEO v26.1.1) - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>How we created Review Page Custom widgets with Flutter<\/title>\n<meta name=\"description\" content=\"Review Page Interaction: A simple Review page interaction made with Flutter\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"How we created Review Page Custom widgets with Flutter\" \/>\n<meta property=\"og:description\" content=\"Review Page Interaction: A simple Review page interaction made with Flutter\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/\" \/>\n<meta property=\"og:site_name\" content=\"MindInventory\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/Mindiventory\" \/>\n<meta property=\"article:published_time\" content=\"2018-07-20T06:44:18+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2025-11-11T05:36:39+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.mindinventory.com\/blog\/wp-content\/uploads\/2022\/10\/review-flutter1200.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"600\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"Pratik Patel\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@mindinventory\" \/>\n<meta name=\"twitter:site\" content=\"@mindinventory\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Pratik Patel\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"8 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/\"},\"author\":{\"name\":\"Pratik Patel\",\"@id\":\"https:\/\/www.mindinventory.com\/blog\/#\/schema\/person\/3c9969f4f05d964960d21e1937a75147\"},\"headline\":\"How we created Review Page Custom widgets with Flutter\",\"datePublished\":\"2018-07-20T06:44:18+00:00\",\"dateModified\":\"2025-11-11T05:36:39+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/\"},\"wordCount\":1536,\"publisher\":{\"@id\":\"https:\/\/www.mindinventory.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/www.mindinventory.com\/blog\/wp-content\/uploads\/2022\/10\/review-flutter1200.jpg\",\"keywords\":[\"flutter\",\"review page\",\"UI design\"],\"articleSection\":[\"Mobile\"],\"inLanguage\":\"en-US\"},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/\",\"url\":\"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/\",\"name\":\"How we created Review Page Custom widgets with Flutter\",\"isPartOf\":{\"@id\":\"https:\/\/www.mindinventory.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/www.mindinventory.com\/blog\/wp-content\/uploads\/2022\/10\/review-flutter1200.jpg\",\"datePublished\":\"2018-07-20T06:44:18+00:00\",\"dateModified\":\"2025-11-11T05:36:39+00:00\",\"description\":\"Review Page Interaction: A simple Review page interaction made with Flutter\",\"breadcrumb\":{\"@id\":\"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/#primaryimage\",\"url\":\"https:\/\/www.mindinventory.com\/blog\/wp-content\/uploads\/2022\/10\/review-flutter1200.jpg\",\"contentUrl\":\"https:\/\/www.mindinventory.com\/blog\/wp-content\/uploads\/2022\/10\/review-flutter1200.jpg\",\"width\":1200,\"height\":600,\"caption\":\"Review flutter\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/www.mindinventory.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"How we created Review Page Custom widgets with Flutter\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/www.mindinventory.com\/blog\/#website\",\"url\":\"https:\/\/www.mindinventory.com\/blog\/\",\"name\":\"MindInventory\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\/\/www.mindinventory.com\/blog\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/www.mindinventory.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/www.mindinventory.com\/blog\/#organization\",\"name\":\"MindInventory\",\"alternateName\":\"Mind Inventory\",\"url\":\"https:\/\/www.mindinventory.com\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.mindinventory.com\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/www.mindinventory.com\/blog\/wp-content\/uploads\/2016\/12\/mindinventory-text-logo.png\",\"contentUrl\":\"https:\/\/www.mindinventory.com\/blog\/wp-content\/uploads\/2016\/12\/mindinventory-text-logo.png\",\"width\":277,\"height\":100,\"caption\":\"MindInventory\"},\"image\":{\"@id\":\"https:\/\/www.mindinventory.com\/blog\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/www.facebook.com\/Mindiventory\",\"https:\/\/x.com\/mindinventory\",\"https:\/\/www.instagram.com\/mindinventory\/\",\"https:\/\/www.linkedin.com\/company\/mindinventory\",\"https:\/\/www.pinterest.com\/mindinventory\/\",\"https:\/\/www.youtube.com\/c\/mindinventory\"]},{\"@type\":\"Person\",\"@id\":\"https:\/\/www.mindinventory.com\/blog\/#\/schema\/person\/3c9969f4f05d964960d21e1937a75147\",\"name\":\"Pratik Patel\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.mindinventory.com\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/27968e6599c2a496d513da68d50f8dd470e24866f861b363a8b10920bc1f55e1?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/27968e6599c2a496d513da68d50f8dd470e24866f861b363a8b10920bc1f55e1?s=96&d=mm&r=g\",\"caption\":\"Pratik Patel\"},\"description\":\"Pratik Patel is the Technical Head of the Mobile App Development team with 13+ years of experience in pioneering technologies. His expertise spans mobile and web development, cloud computing, and business intelligence. Pratik excels in creating robust, user-centric applications and leading innovative projects from concept to completion.\",\"sameAs\":[\"https:\/\/www.linkedin.com\/in\/pratik-patel-19b821138\/\"],\"url\":\"https:\/\/www.mindinventory.com\/blog\/author\/pratikpatel\/\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"How we created Review Page Custom widgets with Flutter","description":"Review Page Interaction: A simple Review page interaction made with Flutter","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/","og_locale":"en_US","og_type":"article","og_title":"How we created Review Page Custom widgets with Flutter","og_description":"Review Page Interaction: A simple Review page interaction made with Flutter","og_url":"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/","og_site_name":"MindInventory","article_publisher":"https:\/\/www.facebook.com\/Mindiventory","article_published_time":"2018-07-20T06:44:18+00:00","article_modified_time":"2025-11-11T05:36:39+00:00","og_image":[{"width":1200,"height":600,"url":"https:\/\/www.mindinventory.com\/blog\/wp-content\/uploads\/2022\/10\/review-flutter1200.jpg","type":"image\/jpeg"}],"author":"Pratik Patel","twitter_card":"summary_large_image","twitter_creator":"@mindinventory","twitter_site":"@mindinventory","twitter_misc":{"Written by":"Pratik Patel","Est. reading time":"8 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/#article","isPartOf":{"@id":"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/"},"author":{"name":"Pratik Patel","@id":"https:\/\/www.mindinventory.com\/blog\/#\/schema\/person\/3c9969f4f05d964960d21e1937a75147"},"headline":"How we created Review Page Custom widgets with Flutter","datePublished":"2018-07-20T06:44:18+00:00","dateModified":"2025-11-11T05:36:39+00:00","mainEntityOfPage":{"@id":"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/"},"wordCount":1536,"publisher":{"@id":"https:\/\/www.mindinventory.com\/blog\/#organization"},"image":{"@id":"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/#primaryimage"},"thumbnailUrl":"https:\/\/www.mindinventory.com\/blog\/wp-content\/uploads\/2022\/10\/review-flutter1200.jpg","keywords":["flutter","review page","UI design"],"articleSection":["Mobile"],"inLanguage":"en-US"},{"@type":"WebPage","@id":"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/","url":"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/","name":"How we created Review Page Custom widgets with Flutter","isPartOf":{"@id":"https:\/\/www.mindinventory.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/#primaryimage"},"image":{"@id":"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/#primaryimage"},"thumbnailUrl":"https:\/\/www.mindinventory.com\/blog\/wp-content\/uploads\/2022\/10\/review-flutter1200.jpg","datePublished":"2018-07-20T06:44:18+00:00","dateModified":"2025-11-11T05:36:39+00:00","description":"Review Page Interaction: A simple Review page interaction made with Flutter","breadcrumb":{"@id":"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/#primaryimage","url":"https:\/\/www.mindinventory.com\/blog\/wp-content\/uploads\/2022\/10\/review-flutter1200.jpg","contentUrl":"https:\/\/www.mindinventory.com\/blog\/wp-content\/uploads\/2022\/10\/review-flutter1200.jpg","width":1200,"height":600,"caption":"Review flutter"},{"@type":"BreadcrumbList","@id":"https:\/\/www.mindinventory.com\/blog\/create-review-page-custom-widgets-with-flutter\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.mindinventory.com\/blog\/"},{"@type":"ListItem","position":2,"name":"How we created Review Page Custom widgets with Flutter"}]},{"@type":"WebSite","@id":"https:\/\/www.mindinventory.com\/blog\/#website","url":"https:\/\/www.mindinventory.com\/blog\/","name":"MindInventory","description":"","publisher":{"@id":"https:\/\/www.mindinventory.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.mindinventory.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/www.mindinventory.com\/blog\/#organization","name":"MindInventory","alternateName":"Mind Inventory","url":"https:\/\/www.mindinventory.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.mindinventory.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/www.mindinventory.com\/blog\/wp-content\/uploads\/2016\/12\/mindinventory-text-logo.png","contentUrl":"https:\/\/www.mindinventory.com\/blog\/wp-content\/uploads\/2016\/12\/mindinventory-text-logo.png","width":277,"height":100,"caption":"MindInventory"},"image":{"@id":"https:\/\/www.mindinventory.com\/blog\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/Mindiventory","https:\/\/x.com\/mindinventory","https:\/\/www.instagram.com\/mindinventory\/","https:\/\/www.linkedin.com\/company\/mindinventory","https:\/\/www.pinterest.com\/mindinventory\/","https:\/\/www.youtube.com\/c\/mindinventory"]},{"@type":"Person","@id":"https:\/\/www.mindinventory.com\/blog\/#\/schema\/person\/3c9969f4f05d964960d21e1937a75147","name":"Pratik Patel","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.mindinventory.com\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/27968e6599c2a496d513da68d50f8dd470e24866f861b363a8b10920bc1f55e1?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/27968e6599c2a496d513da68d50f8dd470e24866f861b363a8b10920bc1f55e1?s=96&d=mm&r=g","caption":"Pratik Patel"},"description":"Pratik Patel is the Technical Head of the Mobile App Development team with 13+ years of experience in pioneering technologies. His expertise spans mobile and web development, cloud computing, and business intelligence. Pratik excels in creating robust, user-centric applications and leading innovative projects from concept to completion.","sameAs":["https:\/\/www.linkedin.com\/in\/pratik-patel-19b821138\/"],"url":"https:\/\/www.mindinventory.com\/blog\/author\/pratikpatel\/"}]}},"post_mailing_queue_ids":[],"_links":{"self":[{"href":"https:\/\/www.mindinventory.com\/blog\/wp-json\/wp\/v2\/posts\/12332","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.mindinventory.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.mindinventory.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.mindinventory.com\/blog\/wp-json\/wp\/v2\/users\/17"}],"replies":[{"embeddable":true,"href":"https:\/\/www.mindinventory.com\/blog\/wp-json\/wp\/v2\/comments?post=12332"}],"version-history":[{"count":1,"href":"https:\/\/www.mindinventory.com\/blog\/wp-json\/wp\/v2\/posts\/12332\/revisions"}],"predecessor-version":[{"id":29928,"href":"https:\/\/www.mindinventory.com\/blog\/wp-json\/wp\/v2\/posts\/12332\/revisions\/29928"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.mindinventory.com\/blog\/wp-json\/wp\/v2\/media\/12338"}],"wp:attachment":[{"href":"https:\/\/www.mindinventory.com\/blog\/wp-json\/wp\/v2\/media?parent=12332"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.mindinventory.com\/blog\/wp-json\/wp\/v2\/categories?post=12332"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.mindinventory.com\/blog\/wp-json\/wp\/v2\/tags?post=12332"},{"taxonomy":"industries","embeddable":true,"href":"https:\/\/www.mindinventory.com\/blog\/wp-json\/wp\/v2\/industries?post=12332"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}