Commit 61e3478a6898fa42d40d4abdfcabe95fc69aada1

Authored by 吴启风
1 parent 642081ad

feat:封装具备答错摇晃、答对缩放动效能力的选择题选项组件

lib/common/widgets/option_widget.dart 0 → 100644
  1 +import 'package:flutter/material.dart';
  2 +import 'package:wow_english/common/widgets/throttledGesture_gesture_detector.dart';
  3 +
  4 +import '../../utils/log_util.dart';
  5 +
  6 +/// 选择题选项组件,具备答错摇晃、答对缩放动效能力
  7 +class OptionWidget extends StatefulWidget {
  8 + final Widget child;
  9 +
  10 + //正确缩放动画;错误摇晃动画;为空即非选中选项,无操作
  11 + final bool? isCorrect;
  12 + final bool isClickable;
  13 + final VoidCallback? onTap;
  14 +
  15 + const OptionWidget({
  16 + Key? key,
  17 + required this.child,
  18 + this.isCorrect,
  19 + this.isClickable = true,
  20 + this.onTap,
  21 + }) : super(key: key);
  22 +
  23 + @override
  24 + _OptionWidgetState createState() => _OptionWidgetState();
  25 +}
  26 +
  27 +class _OptionWidgetState extends State<OptionWidget>
  28 + with SingleTickerProviderStateMixin {
  29 + late AnimationController _controller;
  30 + late Animation<double> _shakeAnimation;
  31 + late Animation<double> _scaleAnimation;
  32 + static const String TAG = 'OptionWidget';
  33 +
  34 + @override
  35 + void initState() {
  36 + super.initState();
  37 +
  38 + _controller = AnimationController(
  39 + vsync: this,
  40 + duration: const Duration(milliseconds: 500),
  41 + );
  42 +
  43 + _shakeAnimation = TweenSequence([
  44 + TweenSequenceItem(
  45 + tween: Tween(begin: 0.0, end: 10.0)
  46 + .chain(CurveTween(curve: Curves.easeInOut)),
  47 + weight: 1,
  48 + ),
  49 + TweenSequenceItem(
  50 + tween: Tween(begin: 10.0, end: -10.0)
  51 + .chain(CurveTween(curve: Curves.easeInOut)),
  52 + weight: 1,
  53 + ),
  54 + TweenSequenceItem(
  55 + tween: Tween(begin: -10.0, end: 10.0)
  56 + .chain(CurveTween(curve: Curves.easeInOut)),
  57 + weight: 1,
  58 + ),
  59 + TweenSequenceItem(
  60 + tween: Tween(begin: 10.0, end: -10.0)
  61 + .chain(CurveTween(curve: Curves.easeInOut)),
  62 + weight: 1,
  63 + ),
  64 + TweenSequenceItem(
  65 + tween: Tween(begin: -10.0, end: 0.0)
  66 + .chain(CurveTween(curve: Curves.easeInOut)),
  67 + weight: 1,
  68 + ),
  69 + ]).animate(_controller);
  70 +
  71 + _scaleAnimation = TweenSequence<double>([
  72 + TweenSequenceItem(tween: Tween(begin: 1.0, end: 0.8), weight: 1),
  73 + TweenSequenceItem(tween: Tween(begin: 0.8, end: 1.25), weight: 1),
  74 + TweenSequenceItem(tween: Tween(begin: 1.25, end: 1.0), weight: 1),
  75 + ]).animate(CurvedAnimation(
  76 + parent: _controller,
  77 + curve: Curves.easeInOut,
  78 + ));
  79 + }
  80 +
  81 + @override
  82 + void didUpdateWidget(OptionWidget oldWidget) {
  83 + super.didUpdateWidget(oldWidget);
  84 + Log.d(
  85 + '$TAG didUpdateWidget widget.isCorrect=${widget.isCorrect} oldWidget.isCorrect=${oldWidget.isCorrect} isAnimation=${_controller.isAnimating} status=${_controller.status}');
  86 + if (widget.isCorrect != oldWidget.isCorrect) {
  87 + _controller.reset();
  88 + if (widget.isCorrect == true) {
  89 + _controller.forward();
  90 + } else if (widget.isCorrect == false) {
  91 + _controller.forward();
  92 + }
  93 + }
  94 + }
  95 +
  96 + @override
  97 + void dispose() {
  98 + _controller.dispose();
  99 + super.dispose();
  100 + }
  101 +
  102 + @override
  103 + Widget build(BuildContext context) {
  104 + return AnimatedBuilder(
  105 + animation: _controller,
  106 + builder: (context, child) {
  107 + double offsetX = widget.isCorrect == false ? _shakeAnimation.value : 0;
  108 + double scale = widget.isCorrect == true ? _scaleAnimation.value : 1.0;
  109 +
  110 + return ThrottledGestureDetector(
  111 + onTap: widget.isClickable ? widget.onTap : null,
  112 + child: Transform.translate(
  113 + offset: Offset(offsetX, 0),
  114 + child: Transform.scale(
  115 + scale: scale,
  116 + child: Opacity(
  117 + opacity: widget.isClickable ? 1.0 : 0.5,
  118 + child: widget.child,
  119 + ),
  120 + ),
  121 + ),
  122 + );
  123 + },
  124 + );
  125 + }
  126 +}
lib/pages/practice/topic_picture_page.dart
@@ -7,10 +7,9 @@ import &#39;package:wow_english/common/extension/string_extension.dart&#39;; @@ -7,10 +7,9 @@ import &#39;package:wow_english/common/extension/string_extension.dart&#39;;
7 import 'package:wow_english/common/widgets/ow_image_widget.dart'; 7 import 'package:wow_english/common/widgets/ow_image_widget.dart';
8 import 'package:wow_english/models/course_process_entity.dart'; 8 import 'package:wow_english/models/course_process_entity.dart';
9 import 'package:wow_english/pages/practice/topic_type.dart'; 9 import 'package:wow_english/pages/practice/topic_type.dart';
10 -import 'package:wow_english/pages/practice/widgets/shake_widget.dart';  
11 import 'package:wow_english/route/route.dart'; 10 import 'package:wow_english/route/route.dart';
12 -import 'package:wow_english/utils/toast_util.dart';  
13 11
  12 +import '../../common/widgets/option_widget.dart';
14 import '../../common/widgets/recorder_widget.dart'; 13 import '../../common/widgets/recorder_widget.dart';
15 import '../../common/widgets/speaker_widget.dart'; 14 import '../../common/widgets/speaker_widget.dart';
16 import 'bloc/topic_picture_bloc.dart'; 15 import 'bloc/topic_picture_bloc.dart';
@@ -168,34 +167,30 @@ class _TopicPicturePage extends StatelessWidget { @@ -168,34 +167,30 @@ class _TopicPicturePage extends StatelessWidget {
168 final isAnswerOption = bloc.selectItem == index; 167 final isAnswerOption = bloc.selectItem == index;
169 final answerCorrect = 168 final answerCorrect =
170 isAnswerOption && bloc.checkAnswerRight(index) == true; 169 isAnswerOption && bloc.checkAnswerRight(index) == true;
171 - final answerIncorrect =  
172 - isAnswerOption && bloc.checkAnswerRight(index) == false;  
173 return Container( 170 return Container(
174 padding: EdgeInsets.symmetric(horizontal: 10.w), 171 padding: EdgeInsets.symmetric(horizontal: 10.w),
175 - child: GestureDetector( 172 + child: OptionWidget(
176 onTap: () => bloc.add(SelectItemEvent(index)), 173 onTap: () => bloc.add(SelectItemEvent(index)),
177 - child: ShakeWidget(  
178 - shouldShake: answerIncorrect, 174 + isCorrect: isAnswerOption ? bloc.checkAnswerRight(index) : null,
  175 + child: Container(
  176 + padding: const EdgeInsets.all(4.5),
  177 + decoration: BoxDecoration(
  178 + color: isAnswerOption
  179 + ? getResultColor(answerCorrect)
  180 + : Colors.white,
  181 + borderRadius: BorderRadius.circular(15),
  182 + ),
  183 + height: 143.h,
  184 + width: 143.w,
179 child: Container( 185 child: Container(
180 - padding: const EdgeInsets.all(4.5),  
181 decoration: BoxDecoration( 186 decoration: BoxDecoration(
182 - color: isAnswerOption  
183 - ? getResultColor(answerCorrect)  
184 - : Colors.white,  
185 - borderRadius: BorderRadius.circular(15),  
186 - ),  
187 - height: 143.h,  
188 - width: 143.w,  
189 - child: Container(  
190 - decoration: BoxDecoration(  
191 - color: Colors.white,  
192 - borderRadius: BorderRadius.circular(15),  
193 - border: Border.all(  
194 - width: 1.0, color: const Color(0xFF140C10)),  
195 - image: DecorationImage(  
196 - fit: BoxFit.fitWidth,  
197 - image: NetworkImage(answerLis?.picUrl ?? ''))),  
198 - ), 187 + color: Colors.white,
  188 + borderRadius: BorderRadius.circular(15),
  189 + border: Border.all(
  190 + width: 1.0, color: const Color(0xFF140C10)),
  191 + image: DecorationImage(
  192 + fit: BoxFit.fitWidth,
  193 + image: NetworkImage(answerLis?.picUrl ?? ''))),
199 ), 194 ),
200 ), 195 ),
201 ), 196 ),
@@ -240,53 +235,49 @@ class _TopicPicturePage extends StatelessWidget { @@ -240,53 +235,49 @@ class _TopicPicturePage extends StatelessWidget {
240 final isAnswerOption = bloc.selectItem == index; 235 final isAnswerOption = bloc.selectItem == index;
241 final answerCorrect = 236 final answerCorrect =
242 isAnswerOption && bloc.checkAnswerRight(index) == true; 237 isAnswerOption && bloc.checkAnswerRight(index) == true;
243 - final answerIncorrect =  
244 - isAnswerOption && bloc.checkAnswerRight(index) == false;  
245 return Container( 238 return Container(
246 padding: EdgeInsets.symmetric(horizontal: 10.w), 239 padding: EdgeInsets.symmetric(horizontal: 10.w),
247 - child: GestureDetector( 240 + child: OptionWidget(
248 onTap: () => bloc.add(SelectItemEvent(index)), 241 onTap: () => bloc.add(SelectItemEvent(index)),
249 - child: ShakeWidget(  
250 - shouldShake: answerIncorrect,  
251 - child: Container(  
252 - width: 143.w,  
253 - height: 143.h,  
254 - padding: EdgeInsets.only(  
255 - left: 13.w, right: 13.w, top: 13.h, bottom: 13.h),  
256 - decoration: BoxDecoration(  
257 - color: Colors.white,  
258 - borderRadius: BorderRadius.circular(15),  
259 - border: Border.all(  
260 - width: 1.0, color: const Color(0xFF140C10)),  
261 - ),  
262 - child: Column(  
263 - mainAxisAlignment: MainAxisAlignment.end,  
264 - children: [  
265 - Expanded(  
266 - child: Container(  
267 - alignment: Alignment.center,  
268 - child: Text(answerLis?.word ?? '',  
269 - style: TextStyle(  
270 - fontSize: 20.sp,  
271 - color: const Color(0xFF333333))),  
272 - ),  
273 - ),  
274 - Container(  
275 - height: 30.h,  
276 - width: double.infinity,  
277 - decoration: BoxDecoration(  
278 - color: isAnswerOption  
279 - ? getResultColor(answerCorrect)  
280 - : Colors.white,  
281 - borderRadius: BorderRadius.circular(15.r),  
282 - border: Border.all(  
283 - width: 1.5, color: const Color(0xFF140C10)),  
284 - ), 242 + isCorrect: isAnswerOption ? bloc.checkAnswerRight(index) : null,
  243 + child: Container(
  244 + width: 143.w,
  245 + height: 143.h,
  246 + padding: EdgeInsets.only(
  247 + left: 13.w, right: 13.w, top: 13.h, bottom: 13.h),
  248 + decoration: BoxDecoration(
  249 + color: Colors.white,
  250 + borderRadius: BorderRadius.circular(15),
  251 + border:
  252 + Border.all(width: 1.0, color: const Color(0xFF140C10)),
  253 + ),
  254 + child: Column(
  255 + mainAxisAlignment: MainAxisAlignment.end,
  256 + children: [
  257 + Expanded(
  258 + child: Container(
285 alignment: Alignment.center, 259 alignment: Alignment.center,
286 - child: Image.asset('choose'.assetPng),  
287 - )  
288 - ],  
289 - ), 260 + child: Text(answerLis?.word ?? '',
  261 + style: TextStyle(
  262 + fontSize: 20.sp,
  263 + color: const Color(0xFF333333))),
  264 + ),
  265 + ),
  266 + Container(
  267 + height: 30.h,
  268 + width: double.infinity,
  269 + decoration: BoxDecoration(
  270 + color: isAnswerOption
  271 + ? getResultColor(answerCorrect)
  272 + : Colors.white,
  273 + borderRadius: BorderRadius.circular(15.r),
  274 + border: Border.all(
  275 + width: 1.5, color: const Color(0xFF140C10)),
  276 + ),
  277 + alignment: Alignment.center,
  278 + child: Image.asset('choose'.assetPng),
  279 + )
  280 + ],
290 ), 281 ),
291 ), 282 ),
292 ), 283 ),
@@ -349,32 +340,28 @@ class _TopicPicturePage extends StatelessWidget { @@ -349,32 +340,28 @@ class _TopicPicturePage extends StatelessWidget {
349 final isAnswerOption = bloc.selectItem == index; 340 final isAnswerOption = bloc.selectItem == index;
350 final answerCorrect = 341 final answerCorrect =
351 isAnswerOption && bloc.checkAnswerRight(index) == true; 342 isAnswerOption && bloc.checkAnswerRight(index) == true;
352 - final answerIncorrect =  
353 - isAnswerOption && bloc.checkAnswerRight(index) == false;  
354 - return ShakeWidget(  
355 - shouldShake: answerIncorrect, 343 + return OptionWidget(
  344 + onTap: () => bloc.add(SelectItemEvent(index)),
  345 + isCorrect: isAnswerOption ? bloc.checkAnswerRight(index) : null,
356 child: Container( 346 child: Container(
357 padding: EdgeInsets.symmetric(horizontal: 20.w), 347 padding: EdgeInsets.symmetric(horizontal: 20.w),
358 - child: GestureDetector(  
359 - onTap: () => bloc.add(SelectItemEvent(index)), 348 + child: Container(
  349 + padding: const EdgeInsets.all(4.5),
  350 + decoration: BoxDecoration(
  351 + color: isAnswerOption
  352 + ? getResultColor(answerCorrect)
  353 + : Colors.white,
  354 + borderRadius: BorderRadius.circular(15),
  355 + ),
  356 + height: 143.h,
  357 + width: 163.w,
360 child: Container( 358 child: Container(
361 - padding: const EdgeInsets.all(4.5),  
362 decoration: BoxDecoration( 359 decoration: BoxDecoration(
363 - color: isAnswerOption  
364 - ? getResultColor(answerCorrect)  
365 - : Colors.white,  
366 - borderRadius: BorderRadius.circular(15),  
367 - ),  
368 - height: 143.h,  
369 - width: 163.w,  
370 - child: Container(  
371 - decoration: BoxDecoration(  
372 - color: Colors.white,  
373 - borderRadius: BorderRadius.circular(15),  
374 - image: DecorationImage(  
375 - fit: BoxFit.fill,  
376 - image: NetworkImage(answerList?.picUrl ?? ''))),  
377 - ), 360 + color: Colors.white,
  361 + borderRadius: BorderRadius.circular(15),
  362 + image: DecorationImage(
  363 + fit: BoxFit.fill,
  364 + image: NetworkImage(answerList?.picUrl ?? ''))),
378 ), 365 ),
379 ), 366 ),
380 ), 367 ),
@@ -426,55 +413,51 @@ class _TopicPicturePage extends StatelessWidget { @@ -426,55 +413,51 @@ class _TopicPicturePage extends StatelessWidget {
426 final isAnswerOption = bloc.selectItem == index; 413 final isAnswerOption = bloc.selectItem == index;
427 final answerCorrect = 414 final answerCorrect =
428 isAnswerOption && bloc.checkAnswerRight(index) == true; 415 isAnswerOption && bloc.checkAnswerRight(index) == true;
429 - final answerIncorrect =  
430 - isAnswerOption && bloc.checkAnswerRight(index) == false;  
431 - return GestureDetector( 416 + return OptionWidget(
432 onTap: () => bloc.add(SelectItemEvent(index)), 417 onTap: () => bloc.add(SelectItemEvent(index)),
433 - child: ShakeWidget(  
434 - shouldShake: answerIncorrect, 418 + isCorrect: isAnswerOption ? bloc.checkAnswerRight(index) : null,
  419 + child: Container(
  420 + width: 163.w,
  421 + height: 143.h,
  422 + padding: EdgeInsets.symmetric(horizontal: 10.w),
435 child: Container( 423 child: Container(
436 - width: 163.w, 424 + width: 143.w,
437 height: 143.h, 425 height: 143.h,
438 - padding: EdgeInsets.symmetric(horizontal: 10.w),  
439 - child: Container(  
440 - width: 143.w,  
441 - height: 143.h,  
442 - padding: EdgeInsets.only(  
443 - left: 13.w, right: 13.w, top: 13.h, bottom: 13.h),  
444 - decoration: BoxDecoration(  
445 - color: Colors.white,  
446 - borderRadius: BorderRadius.circular(15),  
447 - border: Border.all(  
448 - width: 1.0, color: const Color(0xFF140C10)),  
449 - ),  
450 - child: Column(  
451 - mainAxisAlignment: MainAxisAlignment.end,  
452 - children: [  
453 - Expanded(  
454 - child: Container(  
455 - alignment: Alignment.center,  
456 - child: Text(answerList.word ?? '',  
457 - style: TextStyle(  
458 - fontSize: 20.sp,  
459 - color: const Color(0xFF333333))),  
460 - ),  
461 - ),  
462 - Container(  
463 - height: 30.h,  
464 - width: double.infinity,  
465 - decoration: BoxDecoration(  
466 - color: isAnswerOption  
467 - ? getResultColor(answerCorrect)  
468 - : Colors.white,  
469 - borderRadius: BorderRadius.circular(15.r),  
470 - border: Border.all(  
471 - width: 1.5, color: const Color(0xFF140C10)),  
472 - ), 426 + padding: EdgeInsets.only(
  427 + left: 13.w, right: 13.w, top: 13.h, bottom: 13.h),
  428 + decoration: BoxDecoration(
  429 + color: Colors.white,
  430 + borderRadius: BorderRadius.circular(15),
  431 + border:
  432 + Border.all(width: 1.0, color: const Color(0xFF140C10)),
  433 + ),
  434 + child: Column(
  435 + mainAxisAlignment: MainAxisAlignment.end,
  436 + children: [
  437 + Expanded(
  438 + child: Container(
473 alignment: Alignment.center, 439 alignment: Alignment.center,
474 - child: Image.asset('choose'.assetPng),  
475 - )  
476 - ],  
477 - ), 440 + child: Text(answerList.word ?? '',
  441 + style: TextStyle(
  442 + fontSize: 20.sp,
  443 + color: const Color(0xFF333333))),
  444 + ),
  445 + ),
  446 + Container(
  447 + height: 30.h,
  448 + width: double.infinity,
  449 + decoration: BoxDecoration(
  450 + color: isAnswerOption
  451 + ? getResultColor(answerCorrect)
  452 + : Colors.white,
  453 + borderRadius: BorderRadius.circular(15.r),
  454 + border: Border.all(
  455 + width: 1.5, color: const Color(0xFF140C10)),
  456 + ),
  457 + alignment: Alignment.center,
  458 + child: Image.asset('choose'.assetPng),
  459 + )
  460 + ],
478 ), 461 ),
479 ), 462 ),
480 ), 463 ),