Flutter 滑動、拖動驗證 人機識別 界面實現


覺得這種交互很有意思,相對其他驗證 比較符合用戶體驗。

只實現了前端的動作邏輯,以及驗證是否拖動到正確區域。

未達到真正的人機識別

 

邏輯也很簡單

1:隨機生成 滑塊按鈕、答案區域 的位置。

2:拖動的過程中驗證是否在答案區域,如果在 答案區域變成綠色。

3:拖動結束,驗證是否在答案區域;在:返回成功 不在:執行滑塊歸位動畫。

 

 以下是github地址,覺得對你有幫助 請不要吝嗇star ~ 

 https://github.com/longer96/flutter-demo

 

(拼圖滑塊驗證碼)

 

 

 

主要的代碼也就100多行

import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shack/widget/dotted_border/r_dotted_line_border.dart';

/// 驗證widget
/// return
/// [true] 成功
/// [false] 失敗
class DemoVerity extends StatefulWidget {
  final Function lister;
  DemoVerity({required this.lister});
  @override
  _DemoVerityState createState() => _DemoVerityState();
}

class _DemoVerityState extends State<DemoVerity> with TickerProviderStateMixin {
  /// 半徑
  final double radius = 32.0;

  /// 拖動控制
  /// 左上角點坐標,布局時會自動轉換為中心點
  /// 初始值
  Offset offsetCtrInit = Offset.zero;

  /// 拖動控制
  /// 左上角點坐標,布局時會自動轉換為中心點
  /// 拖動會變化
  Offset offsetCtr = Offset.zero;

  /// 正確區域 中心點
  Offset offsetAwe = Offset.zero;

  late AnimationController anwerAnimationController;
  late AnimationController animationController;

  /// 錯誤歸位動畫
  late final Animation<double> moveAnimation;

  /// 答案區域縮放動畫
  late final Animation<double> scaleAnimation;

  /// 兩點距離
  double get distance => (offsetAwe - offsetCtr).distance;

  /// 是否滑入正確區域
  bool success = false;

  @override
  void initState() {
    super.initState();

    animationController =
        AnimationController(vsync: this, duration: Duration(milliseconds: 200));
    animationController.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        animationController.reset();
        setState(() {
          offsetCtr = offsetCtrInit;
        });
      }
    });

    moveAnimation = Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(
        parent: animationController,
        curve: const Cubic(0.68, 0, 0, 1.5),
      ),
    );

    anwerAnimationController = AnimationController(
        vsync: this, duration: const Duration(milliseconds: 800));

    scaleAnimation = Tween<double>(begin: 1.0, end: 1.3).animate(
      CurvedAnimation(
        parent: anwerAnimationController,
        curve: Curves.fastOutSlowIn,
      ),
    );

    WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
      final size = context.size ?? Size.zero;

      final x1 = radius + Random().nextInt((size.width - radius * 2.2).toInt());
      final y1 = radius + Random().nextInt(30);

      /// 正確答案在下半部分區域
      final x2 = radius + Random().nextInt((size.width - radius * 2.6).toInt());
      final y2 = size.height * 0.7 +
          Random().nextInt((size.height * 0.3).toInt()) -
          radius * 1.2 -
          MediaQuery.of(context).padding.bottom;

      setState(() {
        offsetCtr = Offset(x1, y1);
        offsetCtrInit = offsetCtr;
        offsetAwe = Offset(x2, y2);
      });
      anwerAnimationController.repeat(reverse: true);
    });
  }

  @override
  void dispose() {
    animationController.dispose();
    anwerAnimationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: double.infinity,
      child: Stack(
        clipBehavior: Clip.none,
        children: [
          /// 答案區域
          Positioned(
            top: offsetAwe.dy - radius,
            left: offsetAwe.dx - radius,
            child: ScaleTransition(
              scale: scaleAnimation,
              child: Container(
                width: radius * 2,
                height: radius * 2,
                alignment: const Alignment(0, 0),
                decoration: BoxDecoration(
                  border: RDottedLineBorder.all(
                      width: 1, color: success ? Colors.green : Colors.blue),
                  color: success
                      ? const Color(0xffe9faef)
                      : const Color(0xffe9f5fe),
                  shape: BoxShape.circle,
                ),
                child: success
                    ? const SizedBox()
                    : const Icon(Icons.add, size: 20, color: Colors.blue),
              ),
            ),
          ),

          /// 滑塊
          AnimatedBuilder(
            animation: animationController,
            builder: (_, child) {
              return Positioned(
                left: offsetCtr.dx -
                    ((offsetCtr.dx - offsetCtrInit.dx) * moveAnimation.value) -
                    radius,
                top: offsetCtr.dy -
                    ((offsetCtr.dy - offsetCtrInit.dy) * moveAnimation.value) -
                    radius,
                child: child!,
              );
            },
            child: GestureDetector(
              onPanUpdate: (DragUpdateDetails details) {
                /// 答案半徑 * 0.9
                final rDistance = radius * 0.9;

                /// 答案區 內
                if ((distance < rDistance) && !success) {
                  success = true;
                  // 震動
                  HapticFeedback.mediumImpact();
                  anwerAnimationController.stop();
                  anwerAnimationController.animateTo(1.0,
                      duration: const Duration());
                }

                /// 答案區 外
                if ((distance >= rDistance) && success) {
                  success = false;
                  if (!anwerAnimationController.isAnimating)
                    anwerAnimationController.repeat(reverse: true);
                }

                if (!mounted) return;
                setState(() {
                  offsetCtr += Offset(details.delta.dx, details.delta.dy);
                });
              },
              onPanEnd: (DragEndDetails details) {
                // debugPrint('longer  結束拖動 是否答案內 >>> $success ');
                // 如果沒有在答案內 執行返回位置的動畫
                if (!success) {
                  animationController.forward();
                }
                widget.lister(success);
              },
              child: Container(
                width: radius * 2,
                height: radius * 2,
                padding: const EdgeInsets.all(12),
                decoration: BoxDecoration(
                  color: const Color(0xff016df3),
                  shape: BoxShape.circle,
                  boxShadow: const <BoxShadow>[
                    BoxShadow(
                      color: Color(0xFF616161),
                      offset: Offset(4.0, 4.0),
                      blurRadius: 8.0,
                    ),
                  ],
                ),
                child: Image.asset('assets/img/safe_icon.jpg'),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM