如何使用 GridView 或 TableView 在 flutter 中制作填字游戏类型的拼图

How to make crossword type puzzle in flutter using GridView or TableView

我接到了一个任务,我必须做一个类似填字游戏的东西。首先,我将向您展示我想要实现的确切图像。

我试过很多可能的方法,比如

但问题是我无法获取用户用手指拖动的单词。字母和正确的单词来自服务器端。此外,当用户拖动某些字母表时,如果单词匹配,那么我必须在正确的单词上制作一个椭圆形,因此可能会有很多单词,所以有很多椭圆形。这意味着我怎样才能做出椭圆形?

使用 Positioned 或其他技巧?

我在 flutter 中搜索了任何可以帮助我的包,但不幸的是,我没有找到。

我写的东西可能会给你一个想法。它绝不是一个完成的、高质量的应用程序,它肯定有几个错误。

首先,我创建了一个 WordMarker。这是围绕单词的黄色矩形。

class WordMarker extends StatelessWidget {
  const WordMarker({
    Key key,
    @required this.rect,
    @required this.startIndex,
    this.color = Colors.yellow,
    this.width = 2.0,
    this.radius = 6.0,
  }) : super(key: key);

  final Rect rect;
  final Color color;
  final double width;
  final double radius;
  final int startIndex;

  @override
  Widget build(BuildContext context) {
    return Positioned.fromRect(
      rect: rect,
      child: DecoratedBox(
        decoration: BoxDecoration(
          border: Border.all(
            color: color,
            width: width,
          ),
          borderRadius: BorderRadius.circular(radius),
        ),
      ),
    );
  }

  WordMarker copyWith({Rect rect}) {
    return WordMarker(
      key: key,
      rect: rect ?? this.rect,
      startIndex: startIndex,
      color: color,
      width: width,
      radius: radius,
    );
  }
}

:

  • a Rect,结合了大小和偏移量,用于定位正确单词上方的标记。

然后我们有 WordSearch 小部件,它是拼图板。

class WordSearch extends StatefulWidget {
  const WordSearch({Key key, this.alphabet, this.words, this.wordsPerLine})
      : super(key: key);

  final int wordsPerLine;
  final List<String> alphabet;
  final List<String> words;

  @override
  _WordSearchState createState() => _WordSearchState();
}

class _WordSearchState extends State<WordSearch> {
  final markers = <WordMarker>[];
  int correctAnswers = 0;
  var uniqueLetters;

  @override
  void initState() {
    super.initState();
    uniqueLetters = widget.alphabet
        .map((letter) => {'letter': letter, 'key': GlobalKey()})
        .toList();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        GridView.count(
          crossAxisCount: widget.wordsPerLine,
          children: <Widget>[
            for (int i = 0; i != uniqueLetters.length; ++i)
              GestureDetector(
                behavior: HitTestBehavior.opaque,
                onTap: () {
                  setState(() {
                    final key = uniqueLetters[i]['key'];
                    final renderBox = key.currentContext.findRenderObject();
                    final markerRect = renderBox.localToGlobal(Offset.zero,
                            ancestor: context.findRenderObject()) &
                        renderBox.size;
                    if (markers.length == correctAnswers) {
                      addMarker(markerRect, i);
                    } else if (widget.words
                        .contains(pathAsString(markers.last.startIndex, i))) {
                      markers.last = adjustedMarker(markers.last, markerRect);
                      ++correctAnswers;
                    } else {
                      markers.removeLast();
                    }
                  });
                },
                child: Center(
                  child: Padding(
                    padding: const EdgeInsets.all(4.0),
                    key: uniqueLetters[i]['key'],
                    child: Text(
                      uniqueLetters[i]['letter'],
                    ),
                  ),
                ),
              ),
          ],
        ),
        ...markers,
      ],
    );
  }

  void addMarker(Rect rect, int startIndex) {
    markers.add(WordMarker(
      rect: rect,
      startIndex: startIndex,
    ));
  }

  WordMarker adjustedMarker(WordMarker originalMarker, Rect endRect) {
    return originalMarker.copyWith(
        rect: originalMarker.rect.expandToInclude(endRect));
  }

  String pathAsString(int start, int end) {
    final isHorizontal =
        start ~/ widget.wordsPerLine == end ~/ widget.wordsPerLine;
    final isVertical = start % widget.wordsPerLine == end % widget.wordsPerLine;

    String result = '';

    if (isHorizontal) {
      result = widget.alphabet.sublist(start, end + 1).join();
    } else if (isVertical) {
      for (int i = start;
          i < widget.alphabet.length;
          i += widget.wordsPerLine) {
        result += widget.alphabet[i];
      }
    }

    return result;
  }
}

:

  • 小部件从其父级接收字母表,并为每个字母赋予 GlobalKey。这使得稍后当用户点击它时识别该字母并获取其偏移量和大小成为可能。
  • 请参阅 markerRect 了解 Rect 计算。另请参阅 adjustedMarker() 以了解在点击下一个字母时 Rect 是如何扩展的。
  • a Stack 和 a GridView 被使用,但 GestureDetector 单独包装每个字母。
  • 每个标记与其第一个字母的索引一起保存,因此在它与下一个被点击的字母之间创建路径时可以轻松实现。请注意,我认为这不是最佳解决方案。
  • 就功能而言 - 该板可让您依次点击任意两个字母。如果他们都给出了正确答案的路径 - 它被圈起来了。否则圆圈将被删除。我希望它能帮助你掌握代码。

您还可以打开一个包含两个小部件的项目,它应该很容易运行。我已经 运行 收到了你发送的单词和字母:

WordSearch(
          wordsPerLine: 11,
          alphabet: [
            'I',
            'A',
            'G',
            'M',
            'F',
            'Y',
            'L',
            'I',
            'R',
            'V',
            'P',
            'D',
            'B',
            'R',
            'A',
            'I',
            'N',
            'S',
            'T',
            'O',
            'R',
            'M',
            'E',
            'S',
            'S',
            'T',
            'R',
            'A',
            'T',
            'E',
            'G',
            'Y',
            'E',
            'A',
            'B',
            'W',
            'O',
            'M',
            'G',
            'O',
            'A',
            'L',
            'S',
            'X',
            'S',
            'Q',
            'U',
            'K',
            'H',
            'J',
            'P',
            'M',
            'D',
            'W',
            'S'
          ],
          words: [
            'ARTHER',
            'GOLDEN',
            'AMADEUS',
            'IDEAS',
            'GOALS',
            'BRAINSTORM'
          ],
        ),

好的,正如我所承诺的那样,我有一个答案要给你我想为它有多混乱道歉。这里真的很晚了,我想今晚把这个给你。现在这可能不是最好的方法,但它确实有效,而且您绝对可以将我的代码的某些部分模块化为它们自己的函数。您可能想要对此进行测试,因为我确定此时它是易碎的,并根据需要添加条件。似乎应该有一种更简单的方法来做到这一点,但我找不到,所以这就是我的方法。

List<bool> isSelected = [];
  List<String> selectedLetters = [];
  Map<GlobalKey, String> lettersMap;

  Offset initialTappedPosition = Offset(0, 0);
  Offset initialPosition = Offset(0, 0);
  Offset finalPosition;

  int intialSquare;
  int crossAxisCount = 4; //whether you use GridView or not still need to provide this
  int index = -1;
  bool isTapped = false;

  String selectedWord = '';

  double width = 50;
  double height = 50;
  Size size;

  List<String> letters = [
    'a',
    'b',
    'c',
    'd',
    'e',
    'f',
    'g',
    'h',
    'i',
    'j',
    'k',
    'b',
    'b',
    'b',
    'b',
    'z',
  ];

  @override
  void initState() {
    super.initState();
    lettersMap =
        Map.fromIterable(letters, key: (i) => GlobalKey(), value: (i) => i[0]);
    isSelected = List.generate(letters.length, (e) => false);
  }

  _determineWord() {
    double differnce;
    int numberOfSquares;

    if ((finalPosition.dx - initialPosition.dx) > 20) {
      print('right');

      //moved right
      differnce = finalPosition.dx - initialPosition.dx;
      numberOfSquares = (differnce / size.width).ceil();
      for (int i = intialSquare + 1;
          i < (intialSquare + numberOfSquares);
          i++) {
        isSelected[i] = true;
      }
      for (int i = 0; i < isSelected.length; i++) {
        if (isSelected[i]) {
          selectedWord += letters[i];
        }
      }
      print(selectedWord);
    } else if ((initialPosition.dx - finalPosition.dx) > 20) {
      print('left');

      // moved left
      differnce = finalPosition.dx + initialPosition.dx;
      numberOfSquares = (differnce / size.width).ceil();
      for (int i = intialSquare - 1;
          i >= (intialSquare - numberOfSquares + 1);
          i--) {
        isSelected[i] = true;
      }
      for (int i = 0; i < isSelected.length; i++) {
        if (isSelected[i]) {
          selectedWord += letters[i];
        }
      }
      print(selectedWord);
    } else if ((finalPosition.dy - initialPosition.dy) > 20) {
      //moved up when moving up/down number of squares numberOfSquares is also number of rows

      differnce = finalPosition.dy - initialPosition.dy;
      numberOfSquares = (differnce / size.height).ceil();

      for (int i = intialSquare + crossAxisCount;
          i < (intialSquare + (numberOfSquares * crossAxisCount));
          i += 4) {
        isSelected[i] = true;
      }
      for (int i = 0; i < isSelected.length; i++) {
        if (isSelected[i]) {
          selectedWord += letters[i];
        }
      }

      print(selectedWord);
    } else if ((initialPosition.dy - finalPosition.dy) > 20) {
      //moved down
      differnce = initialPosition.dy - finalPosition.dy;
      numberOfSquares = (differnce / size.height).ceil();

      for (int i = intialSquare - crossAxisCount;
          i > (intialSquare - (numberOfSquares * crossAxisCount));
          i -= 4) {
        isSelected[i] = true;
        print('$i');
      }
      for (int i = isSelected.length - 1; i >= 0; i--) {
        if (isSelected[i]) {
          selectedWord += letters[i];
        }
      }
      print(selectedWord);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Stack(
        children: <Widget>[
          Center(
            child: Padding(
              padding: const EdgeInsets.all(30.0),
              child: GestureDetector(
                child: GridView(
                  physics: NeverScrollableScrollPhysics(), //Very Important if
// you don't have this line you will have conflicting touch inputs and with
// gridview being the child will win
                  shrinkWrap: true,
                  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: crossAxisCount,
                    childAspectRatio: 2,
                  ),
                  children: <Widget>[
                    for (int i = 0; i != lettersMap.length; ++i)
                      Listener(
                        child: Container(
                          key: lettersMap.keys.toList()[i],
                          child: Text(
                            lettersMap.values.toList()[i],
                            textAlign: TextAlign.center,
                            style: TextStyle(
                              color: Colors.amber,
                              fontSize: 18,
                            ),
                          ),
                        ),
                        onPointerDown: (PointerDownEvent event) {

                          final RenderBox renderBox = lettersMap.keys
                              .toList()[i]
                              .currentContext
                              .findRenderObject();
                          size = renderBox.size;
                          setState(() {
                            isSelected[i] = true;
                            intialSquare = i;
                          });
                        },
                      ),
                  ],
                ),
                onTapDown: (TapDownDetails details) {
                  //User Taps Screen
                  // print('Global Position: ${details.globalPosition}');
                  setState(() {
                    initialPosition = Offset(
                      details.globalPosition.dx - 25,
                      details.globalPosition.dy - 25,
                    );
                    initialTappedPosition = Offset(
                      details.globalPosition.dx - 25,
                      details.globalPosition.dy - 25,
                    );
                    isTapped = true;
                  });
                  // print(initialPosition);
                },
                onVerticalDragUpdate: (DragUpdateDetails details) {
                  // print('${details.delta.dy}');
                  setState(() {
                    if (details.delta.dy < 0) {
                      initialTappedPosition = Offset(initialTappedPosition.dx,
                          initialTappedPosition.dy + details.delta.dy);
                      height -= details.delta.dy;
                    } else {
                      height += details.delta.dy;
                    }
                    finalPosition = Offset(
                      details.globalPosition.dx - 25,
                      details.globalPosition.dy - 25,
                    );
                  });
                },
                onHorizontalDragUpdate: (DragUpdateDetails details) {
                  // print('${details.delta.dx}');
                  setState(() {
                    if (details.delta.dx < 0) {
                      initialTappedPosition = Offset(
                        initialTappedPosition.dx + details.delta.dx,
                        initialTappedPosition.dy,
                      );
                      width -= details.delta.dx;
                    } else {
                      width += details.delta.dx;
                    }

                    finalPosition = Offset(
                      details.globalPosition.dx - 25,
                      details.globalPosition.dy - 25,
                    );
                  });
                },
                onHorizontalDragEnd: (DragEndDetails details) {
                  _determineWord();
                },
                onVerticalDragEnd: (DragEndDetails details) {
                  _determineWord();
                },
              ),
            ),
          ),
          Positioned(
            top: initialTappedPosition.dy,
            left: initialTappedPosition.dx,
            child: Container(
              height: height,
              width: width,
              decoration: ShapeDecoration(
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(30),
                  side: BorderSide(
                    color: isTapped ? Colors.blue : Colors.transparent,
                    width: 3.0,
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

很高兴与您合作希望您的项目顺利。我试图清理所有不必要的打印语句,如果我遗漏了任何内容,我深表歉意。

目前我大致找到了这样的解决方案

class WordSearchGame extends StatefulWidget {
  @override
  GridState createState() {
    return new GridState();
  }
}

class GridState extends State<WordSearchGame> {
  final Set<int> selectedIndexes = Set<int>();
  final key = GlobalKey();
  final Set<_Foo> _trackTaped = Set<_Foo>();
  List<String> alphabet=[
    'I',
    'A',
    'G',
    'M',
    'F',
    'Y',
    'L',
    'I',
    'R',
    'V',
    'P',
    'D',
    'B',
    'R',
    'A',
    'I',
    'N',
    'S',
    'T',
    'O',
    'R',
    'M',
    'E',
    'S',
    'S',
    'T',
    'R',
    'A',
    'T',
    'E',
    'G',
    'Y',
    'E',
    'A',
    'B',
    'W',
    'O',
    'M',
    'G',
    'O',
    'A',
    'L',
    'S',
    'X',
    'S',
    'Q',
    'U',
    'K',
    'H',
    'J',
    'P',
    'M',
    'D',
    'W',
    'S'
  ];
  List<String> words= [
  'ARTHER',
  'GOLDEN',
  'AMADEUS',
  'IDEAS',
  'GOALS',
  'BRAINSTORM'
  ];

  _detectTapedItem(PointerEvent event) {
    final RenderBox box = key.currentContext.findRenderObject();
    final result = BoxHitTestResult();
    Offset local = box.globalToLocal(event.position);
    if (box.hitTest(result, position: local)) {
      for (final hit in result.path) {
        /// temporary variable so that the [is] allows access of [index]
        final target = hit.target;
        if (target is _Foo && !_trackTaped.contains(target)) {
          _trackTaped.add(target);
          _selectIndex(target.index);
        }
      }
    }
  }

  _selectIndex(int index) {
    setState(() {
      selectedIndexes.add(index);

    });
  }


  String word='';
  String getWord(Set<int> selectedIndexes,List<String> alphabet){
    word='';
    selectedIndexes.forEach((element) {word=word+alphabet[element];});
//    word+=alphabet[selectedIndexes.elementAt(0)];
    return word;
  }

  @override
  Widget build(BuildContext context) {
    return Listener(
      onPointerDown: _detectTapedItem,
      onPointerMove: _detectTapedItem,
      onPointerUp: _clearSelection,

      child: GridView.builder(
        key: key,
        itemCount: alphabet.length,
        physics: NeverScrollableScrollPhysics(),
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 11,
          childAspectRatio: 0.7,
          crossAxisSpacing: 5.0,
          mainAxisSpacing: 5.0,
        ),
        itemBuilder: (context, index) {
          return Foo(
            index: index,
            child: Container(
              child: Text(alphabet[index]),
              color: selectedIndexes.contains(index) ? Colors.red : Colors.blue,
            ),
          );
        },
      ),
    );
  }

  void _clearSelection(PointerUpEvent event) {

    if(words.contains(getWord(selectedIndexes, alphabet))){
      print("ok");

    }
    else{
      _trackTaped.clear();
      setState(() {
        selectedIndexes.clear();
      });
    }

  }
}

class Foo extends SingleChildRenderObjectWidget {
  final int index;

  Foo({Widget child, this.index, Key key}) : super(child: child, key: key);

  @override
  _Foo createRenderObject(BuildContext context) {
    return _Foo()..index = index;
  }

  @override
  void updateRenderObject(BuildContext context, _Foo renderObject) {
    renderObject..index = index;

  }
}

class _Foo extends RenderProxyBox {
  int index;
}