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

时间秒杀一切 提交于 2020-05-28 07:16:48

问题


I got a task in which I had to make a something like crossword puzzle. First of all, I will show you the exact image which I want to achieve.

I have tried using many possible ways like

  • tried using GridView and Table widgets provided in flutter.

  • Tried putting GridView/Table inside GestureDetector

but the problem is that I can't get the word on which the user dragged with his finger. The alphabets and the correct word is coming from server-side. Also when the user dragged on some alphabets then if words match then I have to make an oval shape on the correct words and so there could be many words so many oval shapes. This means that how can I make the oval shape?

Using Positioned or some other tricks?

I searched for any packages in flutter which could help me but unfortunately, I didn't find any.


回答1:


Okay as promised I have an answer for you I want apologize for how messy it is. It's getting really late here and I wanted to get this to you tonight. Now this might not be this best way but it works and you could definitely modularize some parts of my code into their own functions. You are probably going to want to test this as I'm sure it is breakable at this point and add conditions as necessary. There seems like there should be an easier way to do this but I am could not find one so this is what I went with.

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,
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }

Pleasure working with you hope your project goes well. I tried to clean up all the unnecessary print statements sorry if I missed any.




回答2:


I've written something that might give you an idea. It's by no means a finished, quality app and it surely has several bugs.

First, I've created a WordMarker. This is the yellow rectangle that circles the word.

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,
    );
  }
}

Note:

  • a Rect, which combines size and offset, is used in order to position the marker above the correct word.

Then we have the WordSearch widget which is the puzzle board.

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;
  }
}

Note:

  • The widget recieves the alphabet from its parent, and it gives each letter a GlobalKey. This makes it possible to identify this letter later, when the user taps it, and get its offset and size.
  • See markerRect to understand the Rect calculation. Also see adjustedMarker() to understand how the Rect expands when the next letter is tapped.
  • a Stack and a GridView are being used, but a GestureDetector wraps each letter individually.
  • Each marker is saved along with the index of its first letter, so it can be achieved easily when creating a path between it and the next letter that is tapped. Note that I think this is not the best solution.
  • In terms of functionallity - the board lets you tap any two letters, one after the other. If they both give the path to a correct answer - it is circled. Otherwise the circle is removed. I hope it helps you grasp the code.

You can also open a project with both widgets and it should be easily runnable. I've ran it with the words and alphabet you sent:

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'
          ],
        ),



来源:https://stackoverflow.com/questions/61285753/how-to-make-crossword-type-puzzle-in-flutter-using-gridview-or-tableview

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!