https://www.idblanter.com/search/label/Template
https://www.idblanter.com
BLANTERORBITv101

Let's design a macOS calculator with Flutter

Tuesday, July 28, 2020
Today I am using my MacBook pro then I see the calculator so look so good of mac. Then I decided to make a calculator with Flutter. So let's start.

Flutter

import 'dart:math';

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';

void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
home: FlutterPen(),
theme: ThemeData(
brightness: Brightness.dark,
buttonTheme: ButtonThemeData(
hoverColor: Colors.transparent,
focusColor: Colors.transparent,
highlightColor: Colors.transparent,
buttonColor: Colors.transparent,
splashColor: Colors.transparent,
minWidth: 12,
height: 12,
),
),
),
);
}

class FlutterPen extends StatefulWidget {

@override
_FlutterPenState createState() => _FlutterPenState();

}

class _FlutterPenState extends State<FlutterPen> {

int stackIndex = 1;
bool focused = true;
bool opened = true;
String input = '0';
num currentNumber = 0;
num previousNumber;
int operation = OperationType.none; // 1 sum | 2 subtract | 4 multiply | 8 divide
int selectedOperation = OperationType.none;
bool afterOperation = false;
bool calculatedAfterOperation = true;
bool calculated = true;
bool clearedLastInput = true;
num left = -1;
num top = -1;
bool optionsHovered = false;
GlobalKey _keyDraggable = GlobalKey();
//GlobalKey _keyCard = GlobalKey();
static const BorderSide bs = BorderSide(color: Colors.black87, width: 1);

String removeTrailingZero(String s) {
if (!s.contains(',')) {
return s;
}
s = s.replaceAll(RegExp(r'0*$'), '');
if (s.endsWith(',')) {
s = s.substring(0, s.length - 1);
}
return s;
}
String inputFrom(num number) {
if(number is int) {
return number.toString();
}
String s = number.toString().replaceAll('.', ',');
String tmpS = number.toStringAsFixed(15).replaceAll('.', ',');
if(s.length >= tmpS.length) {
s = tmpS;
}
return removeTrailingZero(s);
}
void clear([String symbol='']) {
setState(() {
if(clearedLastInput) {
input = '0';
currentNumber = 0;
previousNumber = null;
afterOperation = false;
calculatedAfterOperation = true;
calculated = true;
operation = OperationType.none;
selectedOperation = OperationType.none;
} else {
input = '0';
currentNumber = 0;
clearedLastInput = true;
}
});
}
void append([String symbol='']) {
clearedLastInput = false;
setState(() {
if(afterOperation) {
input = '0';
afterOperation = false;
}
input=(input=='0' && symbol != ',')?'':input;
input += symbol;
currentNumber = double.tryParse(input.replaceAll(',', '.'));
if(currentNumber == null) {
currentNumber = int.tryParse(input);
}
});
}
void changeSign([String symbol='']) {
setState(() {
if(input=='0') {
return;
}
input=(input.startsWith('-'))?input.substring(1):'-'+input;
currentNumber = double.tryParse(input.replaceAll(',', '.'));
if(currentNumber == null) {
currentNumber = int.tryParse(input);
}
calculated = true;
});
}
void equal([String symbol='', bool resetSelectedOperation=true]) {
previousNumber ??= 0;
num tmpResult;
switch(operation) {
case OperationType.sum:
tmpResult = previousNumber + currentNumber;
break;
case OperationType.sub:
tmpResult = previousNumber - currentNumber;
break;
case OperationType.mul:
tmpResult = previousNumber * currentNumber;
break;
case OperationType.div:
tmpResult = previousNumber / currentNumber;
break;
}
setState(() {
previousNumber = tmpResult;
input = inputFrom(previousNumber);
afterOperation = true;
calculatedAfterOperation = true;
if(resetSelectedOperation) {
selectedOperation = OperationType.none;
}
});
//isOperation();
}
void perCent([String symbol='']) {
setState(() {
if(previousNumber != null && !calculatedAfterOperation) {
currentNumber *= previousNumber;
}
if(afterOperation) {
afterOperation = false;
}
currentNumber /= 100;
input = inputFrom(currentNumber);
//isOperation();
});
}
void sum([String symbol='']) {
isOperation(OperationType.sum);
}
void sub([String symbol='']) {
isOperation(OperationType.sub);
}
void mul([String symbol='']) {
isOperation(OperationType.mul);
}
void div([String symbol='']) {
isOperation(OperationType.div);
}
void isOperation([int type=OperationType.none]) {
setState(() {
if(!calculatedAfterOperation && !afterOperation) {
equal('', false);
calculatedAfterOperation = true;
afterOperation = false;
}
selectedOperation = type;
if(calculated) {
previousNumber = currentNumber;
calculated = false;
}
if(type != 0) {
operation = type;
}
if(!afterOperation) {
calculatedAfterOperation = false;
}
afterOperation = true;
});
}
void close() {
minimize();
// we purge calculator's state
// by calling clear() twice
clear();
clear();
}
void minimize() {
setState(() {
optionsHovered = false;
opened = !opened;
focused = true;
});
}
void maximize() {
// TBI
setState(() {
focused = !focused;
});
}

void _updateLocation(DraggableDetails details) {
setState(() {
final RenderBox renderBox = _keyDraggable.currentContext.findRenderObject();
final position = renderBox.localToGlobal(Offset.zero);
left += details.offset.dx-position.dx;
top += details.offset.dy-position.dy;
});

}

double get scale {
return opened?1:0;
}

@override
void initState() {
super.initState();
}

@override
Widget build(BuildContext context) {
if(left == -1) {
left = ((MediaQuery.of(context).size.width - PenConfig.defaultSize.dx).floor()>>1).toDouble();
top = ((MediaQuery.of(context).size.height - PenConfig.defaultSize.dy).floor()>>1).toDouble();
}
return Scaffold(
backgroundColor: PenColors.scaffold,
body: Center(
child: Stack(
overflow: Overflow.visible,
children: [
Center(
child: FlatButton(
onPressed: () {
setState(() {
opened = !opened;
});
},
onLongPress: () {
setState(() {
opened = true;
left = ((MediaQuery.of(context).size.width - PenConfig.defaultSize.dx).floor()>>1).toDouble();
top = ((MediaQuery.of(context).size.height - PenConfig.defaultSize.dy).floor()>>1).toDouble();
});
},
child: Image.network('https://icons.iconarchive.com/icons/johanchalibert/mac-osx-yosemite/512/calculator-icon.png', width:64, height:64),
),
),
opened?AnimatedPositioned(
duration: Duration(milliseconds: 300),
curve: Curves.easeOut,
left: left,
top: top,
child: Listener(
onPointerUp: (PointerUpEvent pue) {
setState(() {
//focused = false;
});
},
onPointerDown: (PointerDownEvent pde) {
setState(() {
focused = true;
});
},
child: Draggable(
key: _keyDraggable,
feedback: Container(
width: PenConfig.defaultSize.dx,
height: PenConfig.defaultSize.dy,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(PenConfig.radius),
color: Colors.black12,
),
),
//feedback: _keyCard.currentContext.findRenderObject(),
onDragEnd: _updateLocation,
child: Card(
//key: _keyCard,
elevation: focused?20:10,
color: Colors.transparent,
child: Container(
width: PenConfig.defaultSize.dx+2,
height: PenConfig.defaultSize.dy+2,
padding: EdgeInsets.all(1),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(PenConfig.radius),
color: Colors.black,
),
child: Container(
width: PenConfig.defaultSize.dx,
height: PenConfig.defaultSize.dy,
padding: EdgeInsets.all(0),
margin: EdgeInsets.all(0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(PenConfig.radius),
color: PenColors.darkGrey,
border: Border.all(
color: PenColors.lightGrey,
),
),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
alignment: Alignment.topLeft,
margin: EdgeInsets.all(0),
padding: EdgeInsets.symmetric(vertical: 0.5, horizontal: PenConfig.gap),
child: MouseRegion(
onEnter: (PointerEnterEvent pee) {
setState(() { optionsHovered = true; });
},
onExit: (PointerExitEvent pee) {
setState(() { optionsHovered = false; });
},
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
OptionButton((optionsHovered&&focused)?'x':'', focused?PenColors.red:PenColors.lightGrey, focused?PenColors.darkRed:PenColors.lightGrey, close),
SizedBox( width: PenConfig.gap),
OptionButton((optionsHovered&&focused)?'−':'', focused?PenColors.yellow:PenColors.lightGrey, focused?PenColors.darkYellow:PenColors.lightGrey, minimize),
SizedBox( width: PenConfig.gap),
OptionButton((optionsHovered&&focused)?'+':'', focused?PenColors.green:PenColors.lightGrey, focused?PenColors.darkGreen:PenColors.lightGrey, maximize),
],
),
),
),
SizedBox(
width: PenConfig.defaultSize.dx-PenConfig.padding*2,
height: 56,
child: FittedBox(
fit: BoxFit.contain,
alignment: Alignment.bottomRight,
child: Text(
'$input',
style: TextStyle(color: PenColors.text, fontWeight: FontWeight.w200, fontSize: 60,), textAlign: TextAlign.end,
),
),
),
SizedBox(height: PenConfig.gap/2+1),
Wrap(
spacing: 1,
runSpacing: 1,
children: [
KeyButton(clearedLastInput?'AC':'C', PenColors.mediumGrey, PenColors.lightGrey, clear, 18),
KeyButton('+/−', PenColors.mediumGrey, PenColors.lightGrey, changeSign, 16),
KeyButton('%', PenColors.mediumGrey, PenColors.lightGrey, perCent, 18),
KeyButton('÷', PenColors.orange, PenColors.darkOrange, div, 26, null, (selectedOperation==OperationType.div)?Border(left: bs, top: bs, bottom: bs):null),
KeyButton('7', PenColors.lightGrey, PenColors.extraLightGrey, append),
KeyButton('8', PenColors.lightGrey, PenColors.extraLightGrey, append),
KeyButton('9', PenColors.lightGrey, PenColors.extraLightGrey, append),
KeyButton('*', PenColors.orange, PenColors.darkOrange, mul, 26, null, (selectedOperation==OperationType.mul)?Border(left: bs, top: bs, bottom: bs):null),
KeyButton('4', PenColors.lightGrey, PenColors.extraLightGrey, append),
KeyButton('5', PenColors.lightGrey, PenColors.extraLightGrey, append),
KeyButton('6', PenColors.lightGrey, PenColors.extraLightGrey, append),
KeyButton('−', PenColors.orange, PenColors.darkOrange, sub, 26, null, (selectedOperation==OperationType.sub)?Border(left: bs, top: bs, bottom: bs):null),
KeyButton('1', PenColors.lightGrey, PenColors.extraLightGrey, append),
KeyButton('2', PenColors.lightGrey, PenColors.extraLightGrey, append),
KeyButton('3', PenColors.lightGrey, PenColors.extraLightGrey, append),
KeyButton('+', PenColors.orange, PenColors.darkOrange, sum, 26, null, (selectedOperation==OperationType.sum)?Border(left: bs, top: bs, bottom: bs):null),
KeyButton('0', PenColors.lightGrey, PenColors.extraLightGrey, append, 22, BorderRadius.only(bottomLeft: Radius.circular(PenConfig.radius-1))),
KeyButton(',', PenColors.lightGrey, PenColors.extraLightGrey, append),
KeyButton('=', PenColors.orange, PenColors.darkOrange, equal, 24, BorderRadius.only(bottomRight: Radius.circular(PenConfig.radius-1))),
],
)
],
),
),
),
),
),
),
):null,
].where(Utils.notNull).toList(),
),
),
);
}

@override
void dispose() {
super.dispose();
}
}


class OptionButton extends StatelessWidget {

final String symbol;
final Color color;
final Color pressedColor;
final Function operation;
final num fontSize = 16;

OptionButton(this.symbol, this.color, this.pressedColor, this.operation);

Widget get label {
if(symbol == 'x') {
return Transform(
transform: Matrix4.identity()
..translate(1.5,-1,0)
..rotateZ( pi / 4),
alignment: FractionalOffset.center,
child: Text('+', style: TextStyle(color:Colors.black.withOpacity(0.6), fontSize:fontSize, fontWeight: FontWeight.bold), textAlign: TextAlign.justify,)
);
}
return Transform(
transform: Matrix4.identity()
..translate(0.5,-1,0),
alignment: FractionalOffset.center,
child: Text('$symbol', style: TextStyle(color:Colors.black.withOpacity(0.6), fontSize:fontSize, fontWeight: FontWeight.bold),),
);
}

@override
Widget build(BuildContext context) {
return FlatButton(
padding: EdgeInsets.all(0),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onPressed: () {
operation();
},
shape: CircleBorder(),
color: color,
highlightColor: pressedColor,
child: label,
);
}
}

class KeyButton extends StatelessWidget {

final String symbol;
final Color color;
final Color pressedColor;
final Function operation;
final num fontSize;
final BorderRadius br;
final BoxBorder bd;

KeyButton(this.symbol, this.color, this.pressedColor, this.operation, [this.fontSize=22, this.br, this.bd]);

Widget get label {
// there is an actual ÷ symbol
if(symbol == '−:') {
return Stack(
alignment: Alignment.center,
children: [
Positioned(
child: Text('−', style: TextStyle(color:PenColors.text, fontSize:fontSize),)
),
Positioned(
top: -1,
child: Text(':', style: TextStyle(color:PenColors.text, fontSize:fontSize),)
),
],
);
}
if(symbol == '+/−') {
return Stack(
fit: StackFit.loose,
overflow: Overflow.visible,
children: [
Positioned(
top: -2,
left: -4,
child: Text('+', style: TextStyle(color:PenColors.text, fontSize:fontSize-4),)
),
Positioned(
child: Transform(
transform: Matrix4.identity()
..translate(2,0,0)
// because italic won't work
..rotateZ( pi * 0.1),
alignment: FractionalOffset.center,
child: Text('/', style: TextStyle(color:PenColors.text, fontSize:fontSize),)
),
),
Positioned(
bottom: -2,
right: -6,
child: Text('−', style: TextStyle(color:PenColors.text, fontSize:fontSize-4),)
),
],
);
}
if(symbol == '*') {
return Transform(
transform: Matrix4.identity()
..translate(2,0,0)
..rotateZ( pi / 4),
alignment: FractionalOffset.center,
child: Text('+', style: TextStyle(color:PenColors.text, fontSize:fontSize+4,), textAlign: TextAlign.justify,)
);
}
return Text('$symbol', style: TextStyle(color:PenColors.text, fontSize:fontSize),);
}

@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(0),
width: symbol=='0'?PenConfig.buttonSize.dx*2+1:PenConfig.buttonSize.dx,
height: PenConfig.buttonSize.dy,
decoration: BoxDecoration(
color: color,
borderRadius: br,
border: bd,
),
child: FlatButton(
padding: EdgeInsets.all(0),
onPressed: () {
operation(symbol);
},
highlightColor: pressedColor,
child: label,
),
);
}

}

class OperationType {
static const int none = 0;
static const int sum = 1;
static const int sub = 2;
static const int mul = 4;
static const int div = 8;
}

class PenColors {
static const scaffold = const Color(0xff1a1b1d);
static const green = const Color(0xff57c038);
static const darkGreen = const Color(0xff5a983d);
static const yellow = const Color(0xffe5bf3c);
static const darkYellow = const Color(0xffb39233);
static const red = const Color(0xfffc5b57);
static const darkRed = const Color(0xffa6443e);
static const orange = const Color(0xfffd9e2b);
static const darkOrange = const Color(0xffb77d20);
static const extraLightGrey = const Color(0xffa1a1a1);
static const lightGrey = const Color(0xff5f6062);
static const mediumGrey = const Color(0xff3f4143);
static const darkGrey = const Color(0xff2b2d2f);
static const text = const Color(0xffe7e7e7);
}

class PenData {
}

class PenConfig {
static const Offset defaultSize = Offset(232, 322);
static const Offset expandedSize = Offset(575, 322);
static const Offset buttonSize = Offset(56.75, 47);
static const num radius = 6;
static const num gap = 8;
static const num padding = 15;
}

class Utils {
static bool notNull(Object o) => o != null;

static void showSnack(BuildContext context, String msg) {
Scaffold.of(context).showSnackBar(
SnackBar(content: Text(msg)),
);
}
}
I hope this article is helpful.

Author

hhhhh