This article mimics the Facebook Reaction Button, implemented using Reactive Programming, Overlay, Animation, Streams, BLoC Pattern and GestureDetector.

Difficulty: Intermediate

Introduction

Lately, somebody asked me how it would be possible to mimic the Facebook Reaction Button in Flutter. After some thoughts, I realized that this was an opportunity to put in practice topics I recently covered in previous articles.

The solution I am going to explain (I called it “reactive button") makes use of the following notions:

The source code of this article can be found on GitHub.

It is also available as a Flutter package: flutter_reactive_button.

This is an animation that shows the outcome of this article.

Requirements

Before going into the implementation details, let’s first consider how the Facebook Reaction Button works:

When a user presses a button, waits for some time (called Long Press), a panel is displayed on the screen, on top of everything, inviting the user to select one of the icons contained in that panel;

If the user moves his/her finger over an icon, the latter grows in size;

If the user moves his/her finger off that icon, the latter retrieves its original size;

If the user releases his/her finger while still over an icon, the latter is selected;

If the user releases his/her finger while no icon is hovered, there is no selection;

If the user simply taps the button, meaning it is not considered as a Long Press, the action is considered as a normal tap;

We could have multiple instances of that reactive button on the screen;

of that reactive button on the screen; The icons should be displayed in the Viewport.

Description of the Solution

The Different Visual Parts

The following graphic shows the different parts, involved in the solution:

ReactiveButton The ReactiveButton can be positioned anywhere on the screen. When the user performs a Long Press on it, it triggers the display of the ReactiveIconContainer.

ReactiveIconContainer Simple container to hold the different ReactiveIcons

ReactiveIcon An icon that may grow if the user hovers it.

Overlay

When an application has started, Flutter automatically creates and renders an Overlay Widget. This Overlay Widget is nothing else but a Stack, which allows visual widgets to “float” on top of other Widgets.

Most of the cases, this Overlay is mainly used by the Navigator to display Routes (= page or screen), Dialogs, DropDown…

The following picture illustrates the notion of Overlay. Widgets are layed out on top of each other.

Each Widget you insert into the Overlay has to be inserted via an OverlayEntry.

Taking advantage of this concept, we can easily display the ReactiveIconContainer, on top of everything, via an OverlayEntry.

Why an OverlayEntry and not a usual Stack?

One of the requirements says that we need to display the list of icons on top of everything.

@ override Widget build(BuildContext context){ return Stack( children: < Widget > [ _buildButton(), _buildIconsContainer(), ... ], ); } Widget _buildIconsContainer(){ return ! _isContainerVisible ? Container() : ...; }

If we were using a Stack, as shown here above, this will lead to a couple of problems:

We would never be sure that the ReactiveIconContainer would systematically be on top of everything, since the ReactiveButton could itself be part of another Stack (and maybe under another Widget); We would need to implement some logic to render or not the ReactiveIconContainer and therefore have to rebuild the Stack, which would not be very efficient

Based on this, I decided to use the notions of Overlay and OverlayEntry to display the ReactiveIconContainer.

Gesture Detection

In order to know what we have to do (display the icons, grow / shrink an icon, select…), we will need to use some Gesture Detection. In other words, the handling of the events related to the user’s finger. In Flutter, there are different ways of handling the interaction with the user’s finger(s).

Please note that the user’s finger is called Pointer in Flutter.

For this solution, I have opted for GestureDetector, which provides all the convenient tools we need, among which:

onHorizontalDragDown & onVerticalDragDown a callback which is called when the pointer has touched the screen

onHorizontalDragStart & onVerticalDragStart a callback which is called when the pointer starts to move on the screen

onHorizontalDragEnd & onVerticalDragEnd a callback which is called when the pointer, previously in contact with the screen, is no longer touching the screen

onHorizontalDragUpdate & onVerticalDragUpdate a callback which is called when the pointer is moving on the screen

onTap a callback which is called when the GestureDetector considers that the user tapped on the screen

onHorizontalDragCancel & onVerticalDragCancel a callback which is called when the GestureDetector considers that the action that has just been done with the Pointer, which previously touched the screen, will not lead to any tap event

As everything will start when the user will touch the screen at the level of the ReactiveButton, it seems natural to wrap the ReactiveButton with a GestureDetector, as follows:

@ override Widget build(BuildContext context){ return GestureDetector( onHorizontalDragStart: _onDragStart, onVerticalDragStart: _onDragStart, onHorizontalDragCancel: _onDragCancel, onVerticalDragCancel: _onDragCancel, onHorizontalDragEnd: _onDragEnd, onVerticalDragEnd: _onDragEnd, onHorizontalDragDown: _onDragReady, onVerticalDragDown: _onDragReady, onHorizontalDragUpdate: _onDragMove, onVerticalDragUpdate: _onDragMove, onTap: _onTap, child: _buildButton(), ); }

Special note related to onPan… callbacks

The GestureDetector also provides callbacks, called onPanStart, onPanCancel… which could also be used and it works fine when there is no Scrolling Area. Since in this example, we also need to consider cases where the ReactiveButton might be located somewhere in a Scrollable Area, this will not work as when the user will be dragging his/her finger on the screen, this will also cause the Scroll Area to scroll.

Special note related to onLongPress callback

As you can see, I am not using the onLongPress callback while the requirements say that we need to display the ReactiveIconContainer when the user proceeds with a long press on the button. Why?

The reason is twofold:

I will catch the gesture events to determine which icon is hovered/selected and using the onLongPress event, does not allow this (Dragging movements will be ignored)

Maybe we would need to customize the “long press” duration

Responsibilities

Let’s now figuring out the responsibility of the various parts…

ReactiveButton

The ReactiveButton will be responsible for:

capturing the gesture events

showing the ReactiveIconContainer when a longPress is detected

hiding the ReactiveIconContainer when the user releases his/her finger from the screen

providing its caller with the outcome of the user’s activity (onTap, onSelected)

displaying the ReactiveIconContainer appropriately on the screen

ReactiveIconContainer

The ReactiveIconContainer will only be responsible for:

building the container

instantiating the icons

ReactiveIcon

The ReactiveIcon will be responsible for:

showing the icon in different size depending on it is hovered or not

telling the ReactiveButton whether it is hovered on not

Communication between the components

We just saw that we need to initiate some communication between the components so that:

the ReactiveButton can provide the ReactiveIcon with the pointer position on the screen (which will be used to determine whether an icon is hovered or is not)

the ReactiveIcon can tell the ReactiveButton whether it is hovered or not

In order not to have any spaghetti code, I will make use of the notion of Streams.

This way,

the ReactiveButton will broadcast the pointer position to whomever is interested in knowing it

the ReactiveIcon will broadcast to whomever is interested, whether it is hovered or is not

The following picture illustrates this idea.

Exact position of the ReactiveButton

As the ReactiveButton might be positioned anywhere in the page, we need to obtain its position to be able to display the ReactiveIconContainer.

As the screen might be larger than the viewport and the ReactiveButton, located anywhere on the screen, we need to get its physical coordinates.

The following helper class gives us that position, as well as additional pieces of information related to the Viewport, Window, Scrollable…

Solution Details

Okay, now that we have the big blocks of the solution, let’s build all this…

Determination of the user’s intentions

The trickiest part of this Widget is to understand what the user wants to do, in other words, to understand the gestures.

1. LongPress vs Tap

As previously mentioned, we cannot use the onLongPress callback because we are also considering the Dragging movements. Therefore, we will have to implement this ourselves.

This will be achieved as follows:

When the user touches the screen (via onHorizontalDragDown or onVerticalDragDown), we start a Timer If the user releases his/her finger from the screen before the Timer delay, this means that the LongPress did not complete If the user has not released his/her finger before the Timer delay, this means that we need to consider the LongPress and no longer a Tap. We then display the ReactiveIconContainer. If the onTap callback is called, we need cancel the timer.

The following code extract illustrates the implementation of the explanation here above.



2. Show/Hide the icons

When we have determined that it is time to display the icons, as explained earlier, we are going to show them on top of everything, using an OverlayEntry.

The following code extract illustrates how to instantiate the ReactiveIconContainer and to add it to the Overlay (as well as how to remove it from the Overlay).

Line #9 We retrieve the instance of the Overlay from the BuildContext

Lines #12-18 We create a new instance of the OverlayEntry, which embeds a new instance of the ReactiveIconContainer

Line 21 We add the OverlayEntry to the Overlay

Line 25 When we need to remove the ReactiveIconContainer from the screen, we simply remove the corresponding OverlayEntry

3. Broadcasting of the Gesture

Earlier we said that the ReactiveButton would be used to broadcast the Pointer moves to the ReactiveIcon through the use of Streams.

In order to make this happen, we need to create the Stream which will be used to convey this information.

3.1. Simple StreamController vs BLoC

A simple implementation could have been the following, at the ReactiveButton level:

StreamController < Offset > _gestureStream = StreamController < Offset > .broadcast(); // then when we instantiate the OverlayEntry ... _overlayEntry = OverlayEntry( builder: (BuildContext context) { return ReactiveIconContainer( stream: _gestureStream.stream, ); } ); // then when we need to broadcast the gestures void _onDragMove(DragUpdateDetails details){ _gestureStream.sink.add(details.globalPosition); }

This would have just worked fine but from earlier in the article, we also mentioned that a second stream would be used to convey the information from the ReactiveIcons to the ReactiveButton. Therefore, I decided to go BLoC Pattern.

Here follows the extract of the resulting BLoC, which will only be used to convey the gesture through use of Streams.

As you will see, I am using the RxDart Package, and more specifically the PublishSubject and Observable (and not StreamController and Stream) since these classes offer additional features that we will be using a bit later.

3.2. Instantiation of the BLoC and provision to the ReactiveIcon

As the ReactiveButton is responsible for the broadcasting of the Gesture, it is logical that it is also responsible for instantiating the BLoC, providing it to the ReactiveIcons and disposing its resources.

We will do it the following way:

Line 10: we instantiate the bloc

Line 19: we release its resource

Line 29: when a Drag gesture is caught, we pass it down to the icons, via the Stream

Line 47: we pass the bloc down to the ReactiveIconContainer

Determination of which ReactiveIcon is hovered/selected

Another interesting part is to know which ReactiveIcon is hovered in order to highlight it.

1. Each icon will use the Stream to get the Pointer position

In order to get the Pointer position, each ReactiveIcon will subscribe to the Streams as follows:

We use a StreamSubscription to listen to the gesture positions, broadcasted by the ReactiveButton via the BLoC.

As the Pointer might move quite often, it would not be very efficient to check whether it is hovering the icon or not, each time a gesture position change occurs. In order to reduce this amount of validations, we take advantage of the Observable to bufferize the events emitted by the ReactiveButton and only consider changes every 100 milliseconds.

2. Determination whether the Pointer is hovering the Icon

In order to determine whether the Pointer hovers an icon, we:

get its position, via the WidgetPosition helper class

check whether the Pointer position hovers the icon, via the widgetPosition.rect.contains(position)

As the fact of buffering the events emitted by the Stream, generated an array of positions, we only consider the last one. This explains the position.last, used in this routine.

3. Highlighting the ReactiveIcon being hovered

In order to highlight the ReactiveIcon being hovered, we will use an Animation to increase its dimensions, as follows:

Explanation:

Line 1: we use a SingleTickerProviderStateMixin for the animation

Lines 16-20: we initialize an AnimationController

Lines 20-23: when the animation will run, we will rebuild the ReactiveIcon

Line 37: we need to release the resourced linked to the AnimationController when the ReactiveIcon will be removed

Lines 44-48: we will scale the ReactiveIcon based on the AnimationController.value (range [0..1]) by an arbitrary scale ratio

Line 67: when the ReactiveIcon is hovered, launch the Animation (from 0 -> 1)

Line 72: when the ReactiveIcon is no longer hovered, launch the Animation (from 1 -> 0)

4. Letting know whether a ReactiveIcon is hovered

The very last part of this explanation relates to the fact of conveying to the ReactiveButton which ReactiveIcon is currently hovered so that, if the user releases his/her finger from the screen at that moment, we need to know which ReactiveIcon is to be considered as selected.

As previously mentioned, we will use a second Stream to convey this information.

4.1. Message to be conveyed

In order to tell the ReactiveButton which ReactiveIcon is being hovered right now and which ReactiveIcon is no longer hovered, we will use a proprietary message: ReactiveIconSelectionMessage. This message will tell “which icon is potentially selected” and “which icon is no longer potentially selected".

4.2. Changes applied to the BLoC

The BLoC now needs to include the new Stream to convey the message.

Here is the new BLoC:

4.3. Allowing the ReactiveButton to receive the messages

In order for the ReactiveButton to receive such notifications, emitted by the ReactiveIcons, we need to subscribe to the message events, as follows:

Line 14: we subscribe to all messages, emitted by the Stream

Line 21: we release the subscription when the ReactiveButton is removed

Lines 32-42: processing of the message, emitted by the ReactiveIcons

4.4. Submission of the message by the ReactiveIcon

At the level of the ReactiveIcon, when the latter needs to send a message to the ReactiveButton, it simply uses the Stream as follows:

Lines 8 and 14: call the _sendNotification method when a change applies to the _isHovered variable

Lines 23-29: emit a ReactiveIconSelectionMessage to the Stream

Conclusions

The remainder of the source code does not require, I think, any further documentation since it only relates to parametrization and look of the ReactiveButton widget.

The objective of this article was to show how to combine several topics (BLoC, Reactive Programming, Animation, Overlay) together and provide a practical example of use.

I hope you enjoyed reading this article.

Stay tuned for next articles, soon. Happy coding.