stock_keyboard_by_tun 1.0.0
stock_keyboard_by_tun: ^1.0.0 copied to clipboard
A highly customizable special stock numeric keyboard for trading dashboards with native selection and caret support.
// ignore_for_file: deprecated_member_use
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:stock_keyboard_by_tun/stock_keyboard_by_tun.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool _isDarkMode = false;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Stock Keyboard Demo',
debugShowCheckedModeBanner: false,
themeMode: _isDarkMode ? ThemeMode.dark : ThemeMode.light,
theme: ThemeData(
brightness: Brightness.light,
scaffoldBackgroundColor: const Color(0xFFF6F8FA),
primaryColor: const Color(0xFF8B1E22),
colorScheme: const ColorScheme.light(
primary: Color(0xFF8B1E22),
secondary: Color(0xFFE53935),
),
fontFamily: 'Inter',
useMaterial3: true,
),
darkTheme: ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xFF0F0F0F),
primaryColor: const Color(0xFFD32F2F),
colorScheme: const ColorScheme.dark(
primary: Color(0xFFD32F2F),
secondary: Color(0xFFE53935),
),
fontFamily: 'Inter',
useMaterial3: true,
),
home: StockOrderScreen(
isDarkMode: _isDarkMode,
onThemeChanged: (val) {
setState(() {
_isDarkMode = val;
});
},
),
);
}
}
/// The fields we want to edit in our stock trading dashboard
enum OrderFields { price, volume }
class StockOrderScreen extends StatefulWidget {
final bool isDarkMode;
final ValueChanged<bool> onThemeChanged;
const StockOrderScreen({
super.key,
required this.isDarkMode,
required this.onThemeChanged,
});
@override
State<StockOrderScreen> createState() => _StockOrderScreenState();
}
class _StockOrderScreenState extends State<StockOrderScreen> {
final ScrollController _scrollController = ScrollController();
// Controllers & Nodes
final TextEditingController _priceController = TextEditingController();
final TextEditingController _volumeController = TextEditingController();
final FocusNode _priceFocusNode = FocusNode();
final FocusNode _volumeFocusNode = FocusNode();
late final CustomKeyboardController<OrderFields> _keyboardController;
@override
void initState() {
super.initState();
// Initialize the single generic controller with layouts and controllers
_keyboardController = CustomKeyboardController<OrderFields>(
controllers: {
OrderFields.price: _priceController,
OrderFields.volume: _volumeController,
},
focusNodes: {
OrderFields.price: _priceFocusNode,
OrderFields.volume: _volumeFocusNode,
},
layouts: {
OrderFields.price: KeyboardLayout.decimal(),
OrderFields.volume: KeyboardLayout.integer(),
},
defaultLanguage: KeyboardLanguage.vi,
);
}
@override
void dispose() {
_keyboardController.dispose();
_priceController.dispose();
_volumeController.dispose();
_priceFocusNode.dispose();
_volumeFocusNode.dispose();
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme =
widget.isDarkMode ? KeyboardTheme.dark() : KeyboardTheme.light();
// Suggestions configs for our fields
final Map<OrderFields, List<KeyboardSuggestionModel>> suggestions = {
OrderFields.price: [
const KeyboardSuggestionModel(
displayLabel: 'LO',
value: 'LO',
leadingIcon: Icon(Icons.lock_outline, size: 14, color: Colors.blue),
),
const KeyboardSuggestionModel(displayLabel: 'MP', value: 'MP'),
const KeyboardSuggestionModel(displayLabel: 'ATC', value: 'ATC'),
KeyboardSuggestionModel(
displayLabel: 'Sàn 60.9',
value: '60.9',
leadingIcon:
Icon(Icons.arrow_downward, size: 14, color: Colors.cyan.shade600),
),
KeyboardSuggestionModel(
displayLabel: 'TC 61.5',
value: '61.5',
leadingIcon: Icon(Icons.fiber_manual_record,
size: 12, color: Colors.orange.shade600),
),
KeyboardSuggestionModel(
displayLabel: 'Trần 63.5',
value: '63.5',
leadingIcon:
Icon(Icons.arrow_upward, size: 14, color: Colors.purple.shade400),
),
],
OrderFields.volume: [
const KeyboardSuggestionModel(displayLabel: '100', value: '100'),
const KeyboardSuggestionModel(displayLabel: '500', value: '500'),
const KeyboardSuggestionModel(displayLabel: '1,000', value: '1000'),
const KeyboardSuggestionModel(displayLabel: '5,000', value: '5000'),
const KeyboardSuggestionModel(displayLabel: '10,000', value: '10000'),
],
};
return ListenableBuilder(
listenable: _keyboardController,
builder: (context, child) {
final currentLang = _keyboardController.language;
// Custom chips for volume input matching screenshot style
List<Widget>? customChips;
if (_keyboardController.focusedField == OrderFields.volume) {
customChips = [
_buildPercentageChip(
prefix: 'M: ',
percentage: '50%',
value: '5000',
prefixColor: Colors.green.shade600,
theme: theme,
),
_buildPercentageChip(
prefix: 'M: ',
percentage: '100%',
value: '10000',
prefixColor: Colors.green.shade600,
theme: theme,
),
_buildPercentageChip(
prefix: 'B: ',
percentage: '50%',
value: '2500',
prefixColor: Colors.red.shade600,
theme: theme,
),
_buildPercentageChip(
prefix: 'B: ',
percentage: '100%',
value: '5000',
prefixColor: Colors.red.shade600,
theme: theme,
),
];
}
return Scaffold(
appBar: AppBar(
title: Text(
currentLang == KeyboardLanguage.vi
? 'Đặt Lệnh Chứng Khoán'
: 'Stock Order Terminal',
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 20),
),
elevation: 0,
scrolledUnderElevation: 0,
backgroundColor:
widget.isDarkMode ? const Color(0xFF161616) : Colors.white,
actions: [
// Vibration Toggle (Live test haptics flag)
IconButton(
icon: Icon(
_keyboardController.enableHapticFeedback
? Icons.vibration
: Icons.phone_android,
),
tooltip: currentLang == KeyboardLanguage.vi
? 'Bật/Tắt Rung'
: 'Toggle Haptics',
onPressed: () {
_keyboardController.enableHapticFeedback =
!_keyboardController.enableHapticFeedback;
},
),
// Language Switcher Toggle
TextButton(
onPressed: () {
_keyboardController.setLanguage(
currentLang == KeyboardLanguage.vi
? KeyboardLanguage.en
: KeyboardLanguage.vi,
);
},
child: Text(
currentLang == KeyboardLanguage.vi ? 'EN' : 'VI',
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
// Theme Toggle
IconButton(
icon: Icon(
widget.isDarkMode ? Icons.light_mode : Icons.dark_mode),
onPressed: () => widget.onThemeChanged(!widget.isDarkMode),
),
],
),
body: StockKeyboardOverlay<OrderFields>(
controller: _keyboardController,
theme: theme,
suggestions: suggestions,
customSuggestionChips: customChips,
centerSuggestions: true,
suggestionBorderRadius: BorderRadius.circular(8.0),
backspaceIcon: const Icon(
Icons.backspace,
size: 20,
),
child: SingleChildScrollView(
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Stock info panel
_buildStockInfoCard(currentLang),
const SizedBox(height: 16.0),
// Form card
_buildOrderForm(context, currentLang, theme),
const SizedBox(height: 24.0),
// Extra dummy inputs to test auto-scrolling
_buildScrollTestingPlaceholder(currentLang),
],
),
),
),
);
},
);
}
Widget _buildStockInfoCard(KeyboardLanguage lang) {
final isVi = lang == KeyboardLanguage.vi;
return Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: widget.isDarkMode
? const Color(0xFF2C2C2C)
: Colors.black.withOpacity(0.05),
),
),
color: widget.isDarkMode ? const Color(0xFF161616) : Colors.white,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'HPG',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w800,
letterSpacing: -0.5,
),
),
Text(
isVi ? 'Tập đoàn Hòa Phát' : 'Hoa Phat Group JS Co.',
style: TextStyle(
fontSize: 12,
color:
widget.isDarkMode ? Colors.white70 : Colors.black54,
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const Text(
'61.5',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w800,
color: Color(
0xFFE5A93C), // Reference color (Orange/Yellow)
),
),
Row(
children: [
Icon(
Icons.arrow_drop_up,
color: Colors.green.shade600,
size: 18,
),
Text(
'+0.7 (+1.15%)',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.green.shade600,
),
),
],
),
],
),
],
),
const Divider(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildInfoColumn(
isVi ? 'Trần' : 'Ceiling', '63.5', Colors.purple.shade400),
_buildInfoColumn(
isVi ? 'TC' : 'Ref', '61.5', const Color(0xFFE5A93C)),
_buildInfoColumn(
isVi ? 'Sàn' : 'Floor', '60.9', Colors.cyan.shade500),
],
),
],
),
),
);
}
Widget _buildInfoColumn(String label, String value, Color color) {
return Column(
children: [
Text(
label,
style: TextStyle(
fontSize: 11,
color: widget.isDarkMode ? Colors.white60 : Colors.black54,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
value,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: color,
),
),
],
);
}
Widget _buildOrderForm(
BuildContext context, KeyboardLanguage lang, KeyboardTheme theme) {
final isVi = lang == KeyboardLanguage.vi;
return Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: widget.isDarkMode ? const Color(0xFF161616) : Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: widget.isDarkMode
? const Color(0xFF2C2C2C)
: Colors.black.withOpacity(0.05),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Price textfield wrapper
Text(
isVi ? 'Giá đặt (VND)' : 'Order Price (VND)',
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
),
const SizedBox(height: 6),
TextField(
controller: _priceController,
focusNode: _priceFocusNode,
showCursor: true,
keyboardType: TextInputType.none,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
decoration: InputDecoration(
hintText: isVi ? 'Nhập giá đặt' : 'Enter order price',
hintStyle:
const TextStyle(fontSize: 14, fontWeight: FontWeight.normal),
contentPadding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
filled: true,
fillColor: widget.isDarkMode
? const Color(0xFF222222)
: const Color(0xFFF9FBFC),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: theme.actionKeyColor, width: 2),
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: widget.isDarkMode
? const Color(0xFF333333)
: Colors.black.withOpacity(0.08),
width: 1.5,
),
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 16.0),
// Volume textfield wrapper
Text(
isVi ? 'Khối lượng' : 'Volume',
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
),
const SizedBox(height: 6),
TextField(
controller: _volumeController,
focusNode: _volumeFocusNode,
showCursor: true,
keyboardType: TextInputType.none,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
decoration: InputDecoration(
hintText: isVi ? 'Nhập khối lượng' : 'Enter volume',
hintStyle:
const TextStyle(fontSize: 14, fontWeight: FontWeight.normal),
contentPadding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
filled: true,
fillColor: widget.isDarkMode
? const Color(0xFF222222)
: const Color(0xFFF9FBFC),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: theme.actionKeyColor, width: 2),
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: widget.isDarkMode
? const Color(0xFF333333)
: Colors.black.withOpacity(0.08),
width: 1.5,
),
borderRadius: BorderRadius.circular(12),
),
),
),
const SizedBox(height: 20.0),
// Action Buttons
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
isVi
? 'Đã gửi lệnh MUA HPG giá ${_priceController.text} SL ${_volumeController.text}'
: 'Submitted BUY order HPG at ${_priceController.text} Qty ${_volumeController.text}',
),
),
);
_keyboardController.unfocus();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green.shade600,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
elevation: 0,
),
child: Text(
isVi ? 'MUA' : 'BUY',
style: const TextStyle(
fontWeight: FontWeight.w800, fontSize: 16),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
isVi
? 'Đã gửi lệnh BÁN HPG giá ${_priceController.text} SL ${_volumeController.text}'
: 'Submitted SELL order HPG at ${_priceController.text} Qty ${_volumeController.text}',
),
),
);
_keyboardController.unfocus();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade600,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
elevation: 0,
),
child: Text(
isVi ? 'BÁN' : 'SELL',
style: const TextStyle(
fontWeight: FontWeight.w800, fontSize: 16),
),
),
),
],
),
],
),
);
}
Widget _buildScrollTestingPlaceholder(KeyboardLanguage lang) {
final isVi = lang == KeyboardLanguage.vi;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 32.0),
Center(
child: Text(
isVi
? '--- Kéo xuống để kiểm thử tính năng Auto-Scroll ---'
: '--- Scroll down to test Auto-Scroll ---',
style: const TextStyle(
fontSize: 11, color: Colors.grey, fontWeight: FontWeight.w500),
),
),
const SizedBox(
height: 100.0), // Large spacer to make scroll view scrollable
// Additional dummy inputs near the bottom
Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: widget.isDarkMode
? const Color(0xFF2C2C2C)
: Colors.black.withOpacity(0.04),
),
),
color: widget.isDarkMode ? const Color(0xFF161616) : Colors.white,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isVi ? 'Thông tin tài khoản' : 'Account Details',
style: const TextStyle(
fontSize: 13, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
isVi
? 'Sức mua: 150,000,000 VND\nTỷ lệ ký quỹ: 40%\nTài sản ròng: 280,000,000 VND'
: 'Purchasing Power: 150,000,000 VND\nMargin Ratio: 40%\nNet Asset Value: 280,000,000 VND',
style: const TextStyle(
fontSize: 12, height: 1.6, color: Colors.grey),
),
],
),
),
),
const SizedBox(
height:
200.0), // Massive padding to check how text inputs behave when pushed
],
);
}
Widget _buildPercentageChip({
required String prefix,
required String percentage,
required String value,
required Color prefixColor,
required KeyboardTheme theme,
}) {
final currentText = _volumeController.text;
final displayLabel = '$prefix$percentage';
final isActive = currentText == value || currentText == displayLabel;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0),
child: Center(
child: GestureDetector(
onTap: () {
if (_keyboardController.enableHapticFeedback) {
HapticFeedback.lightImpact();
}
_keyboardController.fillSuggestionValue(value);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
decoration: BoxDecoration(
color: isActive
? theme.activeSuggestionChipColor
: theme.suggestionChipColor,
borderRadius: BorderRadius.circular(20.0),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 1.0,
offset: const Offset(0, 1),
),
],
border: Border.all(
color: isActive
? Colors.transparent
: Colors.black.withOpacity(0.05),
width: 0.5,
),
),
child: RichText(
textScaler: TextScaler.noScaling,
text: TextSpan(
children: [
TextSpan(
text: prefix,
style: TextStyle(
color: isActive
? theme.activeSuggestionTextColor
: prefixColor,
fontSize: 13,
fontWeight: FontWeight.w700,
),
),
TextSpan(
text: percentage,
style: TextStyle(
color: isActive
? theme.activeSuggestionTextColor
: theme.suggestionTextColor,
fontSize: 13,
fontWeight: isActive ? FontWeight.w700 : FontWeight.w500,
),
),
],
),
),
),
),
),
);
}
}