diff --git a/assets/lang/en.json b/assets/lang/en.json index 6ad2a42d4..5432fa6cb 100644 --- a/assets/lang/en.json +++ b/assets/lang/en.json @@ -53,5 +53,6 @@ "title.consent_summary": "Consent Summary", "title.consent_sections": "Consent Sections", "title.consents_section": "Consent Section", + "select_star": "Select how many", "": "" } \ No newline at end of file diff --git a/example/assets/images/starActive.png b/example/assets/images/starActive.png new file mode 100644 index 000000000..80d8be2e5 Binary files /dev/null and b/example/assets/images/starActive.png differ diff --git a/example/assets/images/starNotActive.png b/example/assets/images/starNotActive.png new file mode 100644 index 000000000..d0243073d Binary files /dev/null and b/example/assets/images/starNotActive.png differ diff --git a/lib/model.dart b/lib/model.dart index 9db476770..218a2625c 100644 --- a/lib/model.dart +++ b/lib/model.dart @@ -26,6 +26,7 @@ part 'src/model/answerformat/choice_answer_format.dart'; part 'src/model/answerformat/form_answer_format.dart'; part 'src/model/answerformat/slider_answer_format.dart'; part 'src/model/answerformat/image_choice_answer_format.dart'; +part 'src/model/answerformat/star_choice_answer_format.dart'; part 'src/model/answerformat/date_time_answer_format.dart'; part 'src/model/answerformat/text_answer_format.dart'; diff --git a/lib/model.g.dart b/lib/model.g.dart index 0f2793f63..cdfd9940e 100644 --- a/lib/model.g.dart +++ b/lib/model.g.dart @@ -193,6 +193,46 @@ Map _$RPImageChoiceToJson(RPImageChoice instance) => 'description': instance.description, }; +RPStarChoiceAnswerFormat _$RPStarChoiceAnswerFormatFromJson( + Map json) => + RPStarChoiceAnswerFormat( + choices: (json['choices'] as List) + .map((e) => RPStarChoice.fromJson(e as Map)) + .toList(), + ) + ..$type = json['__type'] as String? + ..questionType = + $enumDecode(_$RPQuestionTypeEnumMap, json['questionType']); + +Map _$RPStarChoiceAnswerFormatToJson( + RPStarChoiceAnswerFormat instance) => + { + if (instance.$type case final value?) '__type': value, + 'choices': instance.choices.map((e) => e.toJson()).toList(), + 'questionType': _$RPQuestionTypeEnumMap[instance.questionType]!, + }; + +RPStarChoice _$RPStarChoiceFromJson(Map json) => + RPStarChoice( + starActiveUrl: json['starActiveUrl'] as String, + starNotActiveUrl: json['starNotActiveUrl'] as String, + keyActive: json['keyActive'] as String?, + keyNotActive: json['keyNotActive'] as String?, + value: json['value'], + description: json['description'] as String, + )..$type = json['__type'] as String?; + +Map _$RPStarChoiceToJson(RPStarChoice instance) => + { + if (instance.$type case final value?) '__type': value, + 'starActiveUrl': instance.starActiveUrl, + 'starNotActiveUrl': instance.starNotActiveUrl, + if (instance.keyActive case final value?) 'keyActive': value, + if (instance.keyNotActive case final value?) 'keyNotActive': value, + if (instance.value case final value?) 'value': value, + 'description': instance.description, + }; + RPDateTimeAnswerFormat _$RPDateTimeAnswerFormatFromJson( Map json) => RPDateTimeAnswerFormat( diff --git a/lib/model.json.dart b/lib/model.json.dart index f1fd5ae93..c770418a4 100644 --- a/lib/model.json.dart +++ b/lib/model.json.dart @@ -19,6 +19,8 @@ void registerFromJsonFunctions() { RPFormAnswerFormat(), RPImageChoiceAnswerFormat(choices: []), RPImageChoice(description: '', imageUrl: ''), + RPStarChoiceAnswerFormat(choices: []), + RPStarChoice(starActiveUrl: '', starNotActiveUrl: '', description: ''), RPIntegerAnswerFormat(maxValue: 1, minValue: 1), RPDoubleAnswerFormat(maxValue: 1, minValue: 1), RPSliderAnswerFormat(divisions: 1, maxValue: 1, minValue: 1), diff --git a/lib/src/model/answerformat/answer_format.dart b/lib/src/model/answerformat/answer_format.dart index 6ac6b535e..765abde4d 100644 --- a/lib/src/model/answerformat/answer_format.dart +++ b/lib/src/model/answerformat/answer_format.dart @@ -43,6 +43,7 @@ enum RPQuestionType { Date, Duration, ImageChoice, + StarChoice, Double, // Eligibility, // TimeInterval, diff --git a/lib/src/model/answerformat/star_choice_answer_format.dart b/lib/src/model/answerformat/star_choice_answer_format.dart new file mode 100644 index 000000000..4d0aaff32 --- /dev/null +++ b/lib/src/model/answerformat/star_choice_answer_format.dart @@ -0,0 +1,61 @@ +part of '../../../model.dart'; + +/// Class representing an Answer Format that lets participants choose a +/// star rating +@JsonSerializable(includeIfNull: false, explicitToJson: true) +class RPStarChoiceAnswerFormat extends RPAnswerFormat { + /// A list of available [RPStarChoice] objects which represent the choices to + /// the participants. + List choices; + + /// Returns an initialized [RPStarChoiceAnswerFormat] with the given list of + /// [RPStarChoice]s. + RPStarChoiceAnswerFormat({required this.choices}) : super(); + + @override + RPQuestionType get questionType => RPQuestionType.ImageChoice; + + @override + Function get fromJsonFunction => _$RPStarChoiceAnswerFormatFromJson; + factory RPStarChoiceAnswerFormat.fromJson(Map json) => + FromJsonFactory().fromJson(json); + @override + Map toJson() => _$RPStarChoiceAnswerFormatToJson(this); +} + +/// The image choice object which the participants can choose from, during a +/// [RPQuestionStep] with [RPStarChoiceAnswerFormat] +@JsonSerializable(includeIfNull: false, explicitToJson: true) +class RPStarChoice extends Serializable { + /// The image portraying the choice. + String starActiveUrl; + String starNotActiveUrl; + + /// The key of the image if this is to be loaded from the images + /// in the assets on the phone. + /// Specify either the [image] or the [key]. + String? keyActive; + String? keyNotActive; + + /// The value of the choice. Can be any type but MUST be serializable if this feature is used. + dynamic value; + + /// The description fitting the image. Is displayed when selected. + String description; + + RPStarChoice({ + required this.starActiveUrl, + required this.starNotActiveUrl, + this.keyActive, + this.keyNotActive, + this.value, + required this.description, + }) : super(); + + @override + Function get fromJsonFunction => _$RPStarChoiceFromJson; + factory RPStarChoice.fromJson(Map json) => + FromJsonFactory().fromJson(json); + @override + Map toJson() => _$RPStarChoiceToJson(this); +} diff --git a/lib/src/ui/question_step.dart b/lib/src/ui/question_step.dart index ffd3fddbd..8bbd008d0 100644 --- a/lib/src/ui/question_step.dart +++ b/lib/src/ui/question_step.dart @@ -111,6 +111,11 @@ class RPUIQuestionStepState extends State with CanSaveResult { (answerFormat as RPImageChoiceAnswerFormat), (result) { currentQuestionBodyResult = result; }); + case const (RPStarChoiceAnswerFormat): + return RPUIStarChoiceQuestionBody( + (answerFormat as RPStarChoiceAnswerFormat), (result) { + currentQuestionBodyResult = result; + }); case const (RPDateTimeAnswerFormat): return RPUIDateTimeQuestionBody( (answerFormat as RPDateTimeAnswerFormat), (result) { diff --git a/lib/src/ui/questions/star_choice_question_body.dart b/lib/src/ui/questions/star_choice_question_body.dart new file mode 100644 index 000000000..095f1e56c --- /dev/null +++ b/lib/src/ui/questions/star_choice_question_body.dart @@ -0,0 +1,97 @@ +part of '../../../ui.dart'; + +class RPUIStarChoiceQuestionBody extends StatefulWidget { + final RPStarChoiceAnswerFormat answerFormat; + final void Function(dynamic) onResultChance; + + const RPUIStarChoiceQuestionBody( + this.answerFormat, + this.onResultChance, { + super.key, + }); + + @override + RPUIStarChoiceQuestionBodyState createState() => + RPUIStarChoiceQuestionBodyState(); +} + +class RPUIStarChoiceQuestionBodyState + extends State + with AutomaticKeepAliveClientMixin { + RPStarChoice? _selectedItem; + + bool isLeftOfSelected(RPStarChoice item,List items){ + return (_selectedItem==null)?false:(items.indexOf(_selectedItem!)>=items.indexOf(item)); + //items.indexOf(_selectedItem); + } + @override + Widget build(BuildContext context) { + super.build(context); + RPLocalizations? locale = RPLocalizations.of(context); + String text = (_selectedItem == null) + ? (locale?.translate('select_star') ?? 'Select how many') + : (locale?.translate(_selectedItem!.description) ?? + _selectedItem!.description); + return SizedBox( + height: 160, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildList(context, widget.answerFormat.choices), + Text( + text, + style: Theme.of(context).textTheme.headlineSmall, + ) + ], + )); + } + + Row _buildList(BuildContext context, List items) { + List list = []; + for (var item in items) { + list.add( + InkWell( + borderRadius: BorderRadius.circular(15), + onTap: () { + setState(() { + _selectedItem = item == _selectedItem ? null : item; + }); + widget.onResultChance(_selectedItem); + }, + child: Container( + // Highlighting of chosen answer + decoration: BoxDecoration( + borderRadius: + BorderRadius.all(Radius.circular(5 * 25 / items.length)), + border: Border.all( + color: _selectedItem == item + ? Theme.of(context).dividerColor + : Colors.transparent, + width: 3, + ), + ), + // Scaling item size with number of choices + // Max size is 125 + padding: EdgeInsets.all(10 / items.length), + width: + (MediaQuery.of(context).size.width * 0.8) / items.length > 125 + ? 125 + : MediaQuery.of(context).size.width * 0.8 / items.length, + height: + (MediaQuery.of(context).size.width * 0.8) / items.length > 125 + ? 125 + : MediaQuery.of(context).size.width * 0.8 / items.length, + child: isLeftOfSelected(item,items)?Image.asset(item.starActiveUrl):Image.asset(item.starNotActiveUrl), + ), + ), + ); + } + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: list, + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/ui.dart b/lib/ui.dart index a7f949216..198d26cb5 100644 --- a/lib/ui.dart +++ b/lib/ui.dart @@ -28,6 +28,7 @@ part 'src/loggers/activity_event_logger.dart'; part 'src/ui/questions/choice_question_body.dart'; part 'src/ui/questions/date_time_question_body.dart'; part 'src/ui/questions/image_choice_question_body.dart'; +part 'src/ui/questions/star_choice_question_body.dart'; part 'src/ui/questions/integer_question_body.dart'; part 'src/ui/questions/double_question_body.dart'; part 'src/ui/questions/slider_question_body.dart';