Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add reaction vibration and reaction highlight when move the finger le… #55

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion example/lib/data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ final List<Reaction<String>> flagsReactions = [
const defaultInitialReaction = Reaction<String>(
value: null,
icon: Text(
key: ValueKey('no reaction'),
'No reaction',
),
);
Expand Down Expand Up @@ -210,7 +211,7 @@ Widget _buildEmojiTitle(String title) {
}

Widget _buildEmojiPreviewIcon(String path) {
return Image.asset(path);
return Image.asset(key: ValueKey(path), path);
}

Widget _buildFlagIcon(String path) {
Expand Down
4 changes: 3 additions & 1 deletion example/lib/post.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_reaction_button/flutter_reaction_button.dart';
import 'package:flutter_reaction_button_test/data.dart' as data;
import 'package:flutter_reaction_button_test/data.dart';

class PostWidget extends StatelessWidget {
const PostWidget({
Expand Down Expand Up @@ -45,8 +46,9 @@ class PostWidget extends StatelessWidget {
debugPrint('Selected value: ${reaction?.value}');
},
reactions: reactions,
placeholder: data.defaultInitialReaction,
placeholder: reactions.last,
selectedReaction: reactions.first,
emptyReaction: defaultInitialReaction,
),
Row(
children: [
Expand Down
3 changes: 3 additions & 0 deletions lib/src/models/reaction.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class Reaction<T> {
required this.icon,
Widget? previewIcon,
this.title,
this.isSelected = false,
}) : previewIcon = previewIcon ?? icon;

/// Widget showing as button after selecting preview Icon from box appear.
Expand All @@ -23,6 +24,8 @@ class Reaction<T> {

final T? value;

final bool isSelected;

@override
bool operator ==(Object? other) {
return other is Reaction &&
Expand Down
74 changes: 58 additions & 16 deletions lib/src/widgets/reaction_button.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_reaction_button/flutter_reaction_button.dart';
import 'package:flutter_reaction_button/src/enums/reaction.dart';
import 'package:flutter_reaction_button/src/extensions/key.dart';
Expand All @@ -11,6 +12,7 @@ class ReactionButton<T> extends StatefulWidget {
required this.reactions,
this.placeholder,
this.selectedReaction,
this.emptyReaction,
this.boxColor = Colors.white,
this.boxElevation = 5,
this.boxRadius = 50,
Expand Down Expand Up @@ -81,15 +83,16 @@ class ReactionButton<T> extends StatefulWidget {

final ReactionType _type;

final Reaction<T>? emptyReaction;

@override
State<ReactionButton<T>> createState() => _ReactionButtonState<T>();
}

class _ReactionButtonState<T> extends State<ReactionButton<T>> {
final GlobalKey _globalKey = GlobalKey();

late Reaction<T>? _selectedReaction =
_isChecked ? widget.selectedReaction : widget.placeholder;
late Reaction<T>? _selectedReaction = _isChecked ? widget.selectedReaction : widget.placeholder;

late bool _isChecked = widget.isChecked;

Expand All @@ -105,15 +108,6 @@ class _ReactionButtonState<T> extends State<ReactionButton<T>> {
});
}

void _onCheck() {
_isChecked = !_isChecked;
_updateReaction(
_isChecked
? widget.selectedReaction ?? widget.reactions.first
: widget.placeholder,
);
}

void _onShowReactionsBox([Offset? offset]) {
_overlayEntry = OverlayEntry(
builder: (context) {
Expand All @@ -131,6 +125,7 @@ class _ReactionButtonState<T> extends State<ReactionButton<T>> {
itemScaleDuration: widget.itemAnimationDuration,
animateBox: widget.animateBox,
direction: widget.direction,
reactionHighlightNotifier: reactionHighlightNotifier,
onReactionSelected: (reaction) {
_updateReaction(reaction);
_disposeOverlayEntry();
Expand Down Expand Up @@ -158,17 +153,55 @@ class _ReactionButtonState<T> extends State<ReactionButton<T>> {
super.dispose();
}

@override
void initState() {
super.initState();
reactionWidth = widget.itemSize.width.toInt() + widget.itemsSpacing.toInt();
reactionPositions = List.generate(widget.reactions.length, (index) => index + 1);
}

int reactionWidth = 0;

List<int> reactionPositions = [];

ValueNotifier<Reaction<T>?> reactionHighlightNotifier = ValueNotifier(null);

@override
Widget build(BuildContext context) {
final Widget? child = _isContainer
? widget.child
: (_selectedReaction ?? widget.reactions.first)!.icon;
final Widget? child =
_isContainer ? widget.child : (_selectedReaction ?? widget.reactions.first)!.icon;

return GestureDetector(
key: _globalKey,
onLongPressMoveUpdate: (details) {
debugPrint('onLongPressMoveUpdate global: ${details.globalPosition}');

final reactionPositiondx = details.globalPosition.dx;
final postition = reactionPositiondx ~/ reactionWidth;
if (postition > 0 && postition < widget.reactions.length + 1) {
reactionHighlightNotifier.value = widget.reactions[postition - 1];
debugPrint('reactionHighlight: $postition');
}
},
onLongPressUp: () {
final currentReaction = reactionHighlightNotifier.value;
if (currentReaction != null) {
reactionHighlightNotifier.value = Reaction(
value: currentReaction.value,
icon: currentReaction.icon,
previewIcon: currentReaction.icon,
title: currentReaction.title,
isSelected: true,
);
}
},
onTap: () {
if (widget.toggle) {
_onCheck();
if (_selectedReaction == widget.emptyReaction) {
_updateReaction(widget.selectedReaction);
} else {
_updateReaction(widget.emptyReaction);
}
} else {
_onShowReactionsBox();
}
Expand All @@ -178,7 +211,16 @@ class _ReactionButtonState<T> extends State<ReactionButton<T>> {
_onShowReactionsBox(_isContainer ? details.globalPosition : null);
}
},
child: child,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (child, animationController) {
return FadeTransition(
opacity: animationController,
child: child,
);
},
child: child,
),
);
}
}
15 changes: 8 additions & 7 deletions lib/src/widgets/reactions_box.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class ReactionsBox<T> extends StatefulWidget {
required this.itemScale,
required this.itemScaleDuration,
required this.onReactionSelected,
required this.reactionHighlightNotifier,
required this.onClose,
required this.animateBox,
this.direction = ReactionsBoxAlignment.ltr,
Expand All @@ -28,6 +29,8 @@ class ReactionsBox<T> extends StatefulWidget {

final Size itemSize;

final ValueNotifier<Reaction<T>?> reactionHighlightNotifier;

final List<Reaction<T>?> reactions;

final Color color;
Expand Down Expand Up @@ -58,8 +61,7 @@ class ReactionsBox<T> extends StatefulWidget {
State<ReactionsBox<T>> createState() => _ReactionsBoxState<T>();
}

class _ReactionsBoxState<T> extends State<ReactionsBox<T>>
with SingleTickerProviderStateMixin {
class _ReactionsBoxState<T> extends State<ReactionsBox<T>> with SingleTickerProviderStateMixin {
final PositionNotifier _positionNotifier = PositionNotifier();

late final AnimationController _boxAnimationController = AnimationController(
Expand All @@ -80,8 +82,7 @@ class _ReactionsBoxState<T> extends State<ReactionsBox<T>>
? widget.offset.dx + _boxWidth > MediaQuery.sizeOf(context).width
: widget.offset.dx - _boxWidth < 0;

bool get _shouldStartFromBottom =>
widget.offset.dy - _boxHeight - widget.boxPadding.vertical < 0;
bool get _shouldStartFromBottom => widget.offset.dy - _boxHeight - widget.boxPadding.vertical < 0;

void _checkIsOffsetOutsideBox(Offset offset) {
final Rect boxRect = Rect.fromLTWH(0, 0, _boxWidth, _boxHeight);
Expand Down Expand Up @@ -111,8 +112,7 @@ class _ReactionsBoxState<T> extends State<ReactionsBox<T>>
valueListenable: _positionNotifier,
builder: (context, fingerPosition, child) {
final bool isBoxHovered = fingerPosition?.isBoxHovered ?? false;
final double boxScale =
1 - (widget.itemScale / widget.reactions.length);
final double boxScale = 1 - (widget.itemScale / widget.reactions.length);

final double widthOverflow;
if (_isWidthOverflow) {
Expand Down Expand Up @@ -213,6 +213,7 @@ class _ReactionsBoxState<T> extends State<ReactionsBox<T>>
child: ReactionsBoxItem<T>(
index: index,
fingerPositionNotifier: _positionNotifier,
reactionHighlightNotifier: widget.reactionHighlightNotifier,
reaction: widget.reactions[index]!,
size: widget.itemSize,
scale: widget.itemScale,
Expand All @@ -230,4 +231,4 @@ class _ReactionsBoxState<T> extends State<ReactionsBox<T>>
),
);
}
}
}
31 changes: 22 additions & 9 deletions lib/src/widgets/reactions_box_item.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_reaction_button/flutter_reaction_button.dart';
import 'package:flutter_reaction_button/src/common/position_notifier.dart';
import 'package:flutter/services.dart';

class ReactionsBoxItem<T> extends StatefulWidget {
const ReactionsBoxItem({
super.key,
required this.reaction,
required this.onReactionSelected,
required this.reactionHighlightNotifier,
required this.scale,
required this.index,
required this.size,
Expand All @@ -19,6 +21,8 @@ class ReactionsBoxItem<T> extends StatefulWidget {

final ValueChanged<Reaction<T>?> onReactionSelected;

final ValueNotifier<Reaction<T>?> reactionHighlightNotifier;

final Duration animationDuration;

final PositionNotifier fingerPositionNotifier;
Expand Down Expand Up @@ -46,8 +50,7 @@ class _ReactionsBoxItemState<T> extends State<ReactionsBoxItem<T>>

void _listener() {
final Offset fingerOffset = widget.fingerPositionNotifier.value.offset;
final Offset topLeft =
Offset((widget.size.width + widget.space) * widget.index, 0);
final Offset topLeft = Offset((widget.size.width + widget.space) * widget.index, 0);
final Offset bottomRight = Offset(
(widget.size.width + widget.space) * (widget.index + 1),
widget.size.height,
Expand All @@ -56,8 +59,7 @@ class _ReactionsBoxItemState<T> extends State<ReactionsBoxItem<T>>
final bool selected = rect.contains(fingerOffset);

if (selected) {
final bool isBoxHovered =
widget.fingerPositionNotifier.value.isBoxHovered;
final bool isBoxHovered = widget.fingerPositionNotifier.value.isBoxHovered;
if (!isBoxHovered) {
widget.onReactionSelected(widget.reaction);
}
Expand All @@ -71,12 +73,25 @@ class _ReactionsBoxItemState<T> extends State<ReactionsBoxItem<T>>
void initState() {
super.initState();
widget.fingerPositionNotifier.addListener(_listener);
widget.reactionHighlightNotifier.addListener(reactionListener);
}

void reactionListener() {
if (widget.reactionHighlightNotifier.value == widget.reaction) {
_animationController.forward();
HapticFeedback.lightImpact();
} else if (widget.reactionHighlightNotifier.value?.isSelected == true) {
widget.onReactionSelected(widget.reactionHighlightNotifier.value);
} else {
_animationController.reverse();
}
}

@override
void dispose() {
widget.fingerPositionNotifier.removeListener(_listener);
_animationController.dispose();
widget.reactionHighlightNotifier.removeListener(reactionListener);
super.dispose();
}

Expand All @@ -85,9 +100,8 @@ class _ReactionsBoxItemState<T> extends State<ReactionsBoxItem<T>>
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
final bool showTitle =
_animationController.value == _animationController.upperBound &&
widget.reaction.title != null;
final bool showTitle = _animationController.value == _animationController.upperBound &&
widget.reaction.title != null;

return Stack(
clipBehavior: Clip.none,
Expand All @@ -102,8 +116,7 @@ class _ReactionsBoxItemState<T> extends State<ReactionsBoxItem<T>>
),
if (widget.reaction.title != null) ...{
Positioned(
bottom:
widget.size.height * (1 + (_animationController.value / 2)),
bottom: widget.size.height * (1 + (_animationController.value / 2)),
child: AnimatedOpacity(
opacity: showTitle ? 1 : 0,
duration: widget.animationDuration,
Expand Down