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

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.

enter image description here

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.

2

3 Answers

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.

3

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

running app

1

For now, I have roughly found such a solution.

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

Your Answer

Sign up or log in

Sign up using Google Sign up using Facebook Sign up using Email and Password

Post as a guest

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

You Might Also Like