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 7 import 'package:wow_english/common/widgets/ow_image_widget.dart';
8 8 import 'package:wow_english/models/course_process_entity.dart';
9 9 import 'package:wow_english/pages/practice/topic_type.dart';
10   -import 'package:wow_english/pages/practice/widgets/shake_widget.dart';
11 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 13 import '../../common/widgets/recorder_widget.dart';
15 14 import '../../common/widgets/speaker_widget.dart';
16 15 import 'bloc/topic_picture_bloc.dart';
... ... @@ -168,34 +167,30 @@ class _TopicPicturePage extends StatelessWidget {
168 167 final isAnswerOption = bloc.selectItem == index;
169 168 final answerCorrect =
170 169 isAnswerOption && bloc.checkAnswerRight(index) == true;
171   - final answerIncorrect =
172   - isAnswerOption && bloc.checkAnswerRight(index) == false;
173 170 return Container(
174 171 padding: EdgeInsets.symmetric(horizontal: 10.w),
175   - child: GestureDetector(
  172 + child: OptionWidget(
176 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 185 child: Container(
180   - padding: const EdgeInsets.all(4.5),
181 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 235 final isAnswerOption = bloc.selectItem == index;
241 236 final answerCorrect =
242 237 isAnswerOption && bloc.checkAnswerRight(index) == true;
243   - final answerIncorrect =
244   - isAnswerOption && bloc.checkAnswerRight(index) == false;
245 238 return Container(
246 239 padding: EdgeInsets.symmetric(horizontal: 10.w),
247   - child: GestureDetector(
  240 + child: OptionWidget(
248 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 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 340 final isAnswerOption = bloc.selectItem == index;
350 341 final answerCorrect =
351 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 346 child: Container(
357 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 358 child: Container(
361   - padding: const EdgeInsets.all(4.5),
362 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 413 final isAnswerOption = bloc.selectItem == index;
427 414 final answerCorrect =
428 415 isAnswerOption && bloc.checkAnswerRight(index) == true;
429   - final answerIncorrect =
430   - isAnswerOption && bloc.checkAnswerRight(index) == false;
431   - return GestureDetector(
  416 + return OptionWidget(
432 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 423 child: Container(
436   - width: 163.w,
  424 + width: 143.w,
437 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 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 ),
... ...