Implement comprehensive end interaction system with split viewport keypad

This major enhancement transforms the scoring interface into a sophisticated end interaction system that mirrors professional archery scoring workflows.

Core behaviors: per-end descending sorting, smart cell selection snapping, active end visual highlighting, cross-end disarm, enhanced keypad pop-up/retract.

Split viewport: upper viewport with scrollable scorecard, lower viewport with 30% keypad, 250ms animated transitions, proper separation prevents UI obstruction.

Enhanced 4x4 keypad: responsive Expanded widgets, CLEAR and CLOSE buttons, percentage-based sizing eliminates overflow.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mikkeli Matlock
2025-08-23 22:36:10 +09:00
parent 12956c27dc
commit 074f827c8b
8 changed files with 1081 additions and 143 deletions
@@ -4,12 +4,14 @@ import '../../../config/constants/scoring_colors.dart';
class ScoringCell extends StatelessWidget {
final String score;
final bool isSelected;
final bool isInActiveEnd;
final VoidCallback onTap;
const ScoringCell({
super.key,
required this.score,
required this.isSelected,
this.isInActiveEnd = false,
required this.onTap,
});
@@ -27,14 +29,29 @@ class ScoringCell extends StatelessWidget {
width: ScoringColors.getBorderWidth(isSelected),
),
borderRadius: BorderRadius.circular(4),
// Add active end highlighting
boxShadow: isInActiveEnd ? [
BoxShadow(
color: Colors.blue.withValues(alpha: 0.3),
blurRadius: 4,
offset: const Offset(0, 2),
),
] : null,
),
child: Center(
child: Text(
score,
style: TextStyle(
fontSize: 16,
color: ScoringColors.getScoreTextColor(score),
fontWeight: ScoringColors.shouldUseBoldText(score) ? FontWeight.bold : FontWeight.normal,
child: Container(
decoration: BoxDecoration(
// Add subtle background highlight for active end
color: isInActiveEnd ? Colors.blue.withValues(alpha: 0.1) : null,
borderRadius: BorderRadius.circular(4),
),
child: Center(
child: Text(
score,
style: TextStyle(
fontSize: 16,
color: ScoringColors.getScoreTextColor(score),
fontWeight: ScoringColors.shouldUseBoldText(score) ? FontWeight.bold : FontWeight.normal,
),
),
),
),
@@ -8,6 +8,7 @@ class ScoringGrid extends StatelessWidget {
final int endsPerSet;
final int selectedRow;
final int selectedColumn;
final int? activeEndIndex;
final Function(int row, int column) onCellSelected;
const ScoringGrid({
@@ -17,6 +18,7 @@ class ScoringGrid extends StatelessWidget {
required this.endsPerSet,
required this.selectedRow,
required this.selectedColumn,
this.activeEndIndex,
required this.onCellSelected,
});
@@ -99,9 +101,12 @@ class ScoringGrid extends StatelessWidget {
return 0;
}
// Keep legacy method for accumulative calculation
int _calculateSumOfSix(int endIndex) {
return _calculateSumOfSixForLogicalEnd(endIndex);
bool _isVisualRowInActiveEnd(int visualRow) {
if (activeEndIndex == null) return false;
final logicalEnd = _getLogicalEndFromVisualRow(visualRow);
return logicalEnd == activeEndIndex;
}
int _calculateAccumulative(int endIndex) {
@@ -279,6 +284,7 @@ class ScoringGrid extends StatelessWidget {
child: cellActive ? ScoringCell(
score: scores[actualRow][actualColumn],
isSelected: selectedRow == actualRow && selectedColumn == actualColumn,
isInActiveEnd: _isVisualRowInActiveEnd(visualRow),
onTap: () => onCellSelected(actualRow, actualColumn),
) : Container(
decoration: BoxDecoration(
@@ -11,51 +11,97 @@ class ScoringKeypad extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Keypad layout matching user's design: 4 rows × 3 columns + CLEAR
final List<List<String>> keypadLayout = [
// Score buttons: 4 rows × 3 columns
final List<List<String>> scoreLayout = [
['X', '10', '9'],
['8', '7', '6'],
['5', '4', '3'],
['2', '1', 'M'],
];
return Column(
children: [
// Main keypad grid (4×3)
Expanded(
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1.5,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
// Functional buttons: 4 rows × 1 column
final List<String> functionalButtons = [
'CLEAR',
'CLOSE',
'', // Reserved for future functions
'', // Reserved for future functions
];
return LayoutBuilder(
builder: (context, constraints) {
// Use Flex widgets for proper percentage distribution
return Row(
children: [
// Score buttons section (75% width)
Expanded(
flex: 75,
child: Column(
children: List.generate(4, (rowIndex) {
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 1),
child: Row(
children: List.generate(3, (colIndex) {
final score = scoreLayout[rowIndex][colIndex];
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 1),
child: _buildKeypadButton(context, score),
),
);
}),
),
),
);
}),
),
),
itemCount: 12,
itemBuilder: (context, index) {
final row = index ~/ 3;
final col = index % 3;
final score = keypadLayout[row][col];
return _buildKeypadButton(context, score);
},
),
),
const SizedBox(height: 8),
// CLEAR button (full width)
SizedBox(
width: double.infinity,
height: 50,
child: _buildKeypadButton(context, 'CLEAR', isWide: true),
),
],
const SizedBox(width: 2),
// Functional buttons section (25% width)
Expanded(
flex: 25,
child: Column(
children: List.generate(4, (index) {
final buttonText = functionalButtons[index];
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 1),
child: buttonText.isNotEmpty
? _buildKeypadButton(context, buttonText, isFunctional: true)
: Container(), // Empty container for reserved buttons
),
);
}),
),
),
],
);
},
);
}
Widget _buildKeypadButton(BuildContext context, String score, {bool isWide = false}) {
// Use centralized color configuration for consistency
final buttonColor = ScoringColors.getScoreBackgroundColor(score);
final textColor = ScoringColors.getScoreTextColor(score);
Widget _buildKeypadButton(BuildContext context, String score, {bool isFunctional = false}) {
// Use different styling for functional buttons
Color buttonColor;
Color textColor;
if (isFunctional) {
if (score == 'CLEAR') {
buttonColor = Colors.red.shade400;
textColor = Colors.white;
} else if (score == 'CLOSE') {
buttonColor = Colors.blue.shade400;
textColor = Colors.white;
} else {
buttonColor = Colors.grey.shade300;
textColor = Colors.grey.shade600;
}
} else {
buttonColor = ScoringColors.getScoreBackgroundColor(score);
textColor = ScoringColors.getScoreTextColor(score);
}
return ElevatedButton(
onPressed: () => onScoreInput(score),
@@ -71,8 +117,9 @@ class ScoringKeypad extends StatelessWidget {
child: Text(
score,
style: TextStyle(
fontSize: isWide ? 16 : (score.length > 1 ? 14 : 18),
fontWeight: ScoringColors.shouldUseBoldText(score) ? FontWeight.bold : FontWeight.w600,
fontSize: isFunctional ? 12 : (score.length > 1 ? 14 : 18),
fontWeight: isFunctional ? FontWeight.bold :
(ScoringColors.shouldUseBoldText(score) ? FontWeight.bold : FontWeight.w600),
),
),
);