效果圖
實現原理
第三方庫
總體設計是基於375*812的,為統一尺寸,所以這裡採用了flutter_screenutil
庫。同時為解決浮點數精度問題,使用了decimal
。
具體引入如下:
dependencies:
...
flutter_screenutil: 0.5.3
decimal: 0.3.5
複製程式碼
總體佈局
佈局方式可以使用多種方式,如ListView,Stack等。本專案採用的是Stack+Positioned(動畫顯示部分用到),底部Positionedc數字及操作符號部分採用Column+Row。
佈局虛擬碼
Stack(
chindren:<Widget>[
Positioned(),
Positioned(),
Positioned(
left:0,
right:0,
bottom:0,
child:Container(
child:Colum(
Row(),
Row(),
Row(),
Row(
children:<Widget>[
buildNumberItem("00"),
buildNumberItem("0"),
buildNumberItem("."),
buildEqualItem()
]
)
)
)
)
]
);
複製程式碼
CalculatorItem操作符元件
數字及操作符號的佈局類似,我們統一建一個CalculatorItem
元件
class CalculatorItem extends StatefulWidget {
final Color activeColor;
final Color color;
final Widget child;
final GestureTapCallback onTap;
final double width;
const CalculatorItem({Key key, this.activeColor, this.color, this.child, this.onTap, this.width}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _CalculatorItemState(activeColor, color, child, onTap, width);
}
}
class _CalculatorItemState extends State<CalculatorItem> {
bool active = false;
final Color activeColor;
final Color color;
final Widget child;
final GestureTapCallback onTap;
final double width;
_CalculatorItemState(this.activeColor, this.color, this.child, this.onTap, this.width);
void _active(bool flag) {
setState(() {
active = flag;
});
}
@override
Widget build(BuildContext context) {
double dp8 = WidgetUtil.getWidth(8);
double dp24 = WidgetUtil.getWidth(24);
double dp60 = WidgetUtil.getWidth(58);
return GestureDetector(
onTapDown: (arg) => _active(true),
onTapUp: (arg) => _active(false),
onTapCancel: () => _active(false),
onTap: onTap,
child: Container(
margin: EdgeInsets.fromLTRB(dp8, dp24, dp8, 0),
width: width ?? dp60,
height: dp60,
decoration:
BoxDecoration(borderRadius: BorderRadius.circular(dp60 / 2), color: active ? activeColor : color),
child: child));
}
}
複製程式碼
實現buildNumberItem
建立數字及操作符元件
Widget buildNumberItem(String text) {
return CalculatorItem(
activeColor: Color(0xffD3D3D3),
color: Color(0xFF1D3247),
onTap: () => addText(text),
child: Center(
child: Text(
text,
style: TextStyle(
fontSize: WidgetUtil.getFontSize(34),
color: Colors.white,
fontFamily: "din_medium"),
),
),
);
}
複製程式碼
表示式佈局及計算結果佈局
我們通過改變Positioned的top值來實現計算結果的動畫展示效果,當點選"="號,計算結果移至表示式,有一個字型向上平移放大效果。
首先建立表示式佈局和計算結果佈局,放在Stack
的前兩個子view中
Stack(
children: <Widget>[
Positioned(
top: top1,
right: 5,
child: Container(
height: 50,
child: Center(
child: Text(
text1,
style: TextStyle(
fontSize: font1,
color: const Color(0xff333333),
fontFamily: "din_medium"),
),
)),
),
Positioned(
top: top2,
right: 5,
child: Offstage(
offstage: hideFont2,
child: Container(
height: 50,
child: Center(
child: Text(
text2,
style: TextStyle(
fontSize: font2,
color: const Color(0xffCCCCCC),
fontFamily: "din_medium"),
),
)),
)),
Positioned()
]
)
複製程式碼
然後使用AnimationController
實現動畫效果,通過呼叫controller.forward()
開始動畫,Positioned(計算結果)通過top屬性上移(移至表示式佈局所在位置),當動畫完成時,重置計算結果佈局和表示式佈局的位置,並且Offstage
控制隱藏結果佈局,完成動畫效果。
String text1 = "";
String text2 = "";
AnimationController controller;
Animation<double> animation;
double top1 = 100;
double top2 = 150;
double font1 = 35;
double font2 = 20;
bool hideFont2 = false;
@override
void initState() {
super.initState();
controller = AnimationController(
vsync: this, duration: const Duration(milliseconds: 300));
CurvedAnimation easy =
CurvedAnimation(parent: controller, curve: Curves.easeIn);
animation = Tween(begin: 0.0, end: 1.0).animate(easy)
..addListener(() {
double v = animation.value * 50;
double level = text2.length > 15 ? fontLevel2 : fontLevel1;
double f = animation.value * (level - fontLevel2);
top1 = 100 - v;
top2 = 150 - v;
font2 = fontLevel2 + f;
setState(() {});
})
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
top1 = 100;
top2 = 150;
hideFont2 = true;
text1 = text2;
calculator.reset(text2);
text2 = "";
setState(() {});
controller.reset();
} else if (status == AnimationStatus.forward) {
setState(() {
hideFont2 = false;
});
}
});
}
void calculateAndUpdate() {
if (text2.isEmpty) return;
controller.forward();
}
...
}
複製程式碼
計算邏輯封裝Calculator
我們用一個陣列來存放計算表示式,如2×3-4÷5
在陣列裡表現為["2","×","3","-","4","÷","5"]
,通過addText
來接收進來的操作符如1,2,3,+,-,×,÷
等,特殊的操作如清空,和刪除等操作用C,<
代替,於是在addText就有了這些邏輯。
class CalcuCalculatorlator {
//計算器表示式項
var expressionItems = [];
String getCurrent() {
var len = expressionItems.length;
if (len == 0)
return "";
else
return expressionItems[len - 1];
}
void addText(String text) {
if (expressionItems.isEmpty && isOpt(text)) return;
var current = getCurrent();
if (current.isEmpty && !("C" == text || "<" == text || "00" == text)) {
expressionItems.add(text);
return;
}
switch (text) {
case "1":
case "2":
case "3":
case "4":
case "5":
case "6":
case "7":
case "8":
case "9":
case "0":
if (isNumber(current)) {//如果當前是數字,替換當前數字
current += text;
replaceLast(current);
} else {
expressionItems.add(text);
}
break;
case ".":
if (isInteger(current)) {//如果是整數,新增小數點
current = "$current.";
replaceLast(current);
}
break;
case "00":
if (isNumber(current)) {
current += "00";
replaceLast(current);
}
break;
case "+":
case "-":
case "×":
case "÷":
if (isNumber(current)) {//如果最後的操作符為數字,則新增操作符,否則替換操作符
expressionItems.add(text);
} else {
replaceLast(text);
}
break;
case "C":
expressionItems.clear();//清空操作符
break;
case "<":
deleteItem(); //退格操作
break;
}
}
...
}
複製程式碼
接下來就是進行四則運算了,我們通過兩個棧實現。
String calculate() {
DStack<Decimal> numbers = DStack(30);
DStack<String> opts = DStack(30);
int i = 0;
if (expressionItems.isEmpty) return "";
if (expressionItems.length == 1) return expressionItems[0];
var end = expressionItems.length;
if (isCurrentOpt()) end -= 1;
while (i < end || !opts.isEmpty) {
String str;
if (i < end) str = expressionItems[i];
if (str != null && isNumber(str)) {
numbers.push(Decimal.parse(str));
i++;
} else if (str != null &&
(opts.isEmpty || level(str) > level(opts.top))) {
opts.push(str);
i++;
} else {
try {
Decimal right = numbers.pop();
Decimal left = numbers.pop();
String opt = opts.top;
if ("+" == opt) {
numbers.push(left + right);
} else if ("-" == opt) {
numbers.push(left - right);
} else if ("×" == opt) {
numbers.push(left * right);
} else if ("÷" == opt) {
numbers.push(left / right);
}
opts.pop();
} catch (e) {
print(e);
}
}
}
Decimal v = numbers.pop();
var result = "$v";
if (result.length > 15)//超出15位,使用指數表示
return v.toStringAsExponential(5).replaceAll("+", "");
return result;
}
int level(String str) {
if ("×÷".contains(str)) {
return 2;
} else if ("+-".contains(str)) {
return 1;
} else {
return 0;
}
}
複製程式碼