问题
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
andTable
widgets provided in flutter.Tried putting
GridView/Table
insideGestureDetector
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 theRect
calculation. Also seeadjustedMarker()
to understand how theRect
expands when the next letter is tapped. - a
Stack
and aGridView
are being used, but aGestureDetector
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