首先看一下我們要實現的效果:
結合動圖演示,列出最終目標如下:
運用的主要技術點:Canvas和CustomPaint API。
運行平臺:Android、iOS
源碼地址:
GithubGitee
功能拆解
首先拆解前文中所列出的6個實現目標,顯而易見,要實現它們,我們需要:
邊界判定;
初始運動方向生成器;
定向移動位置更新器。
功能實現
接下來,我們逐步實現功能拆解中所列舉的6個具體功能。
隨機顏色生成器
隨機顏色生成器在程序啟動、單擊屏幕和自動變色中使用。在Flutter中,我們可以通過Color類對紅、綠、藍和透明度分別定義,來定義某個唯一的顏色,數值范圍是0-255。對于透明度,0表示完全透明,255表示完全不透明。
對于隨機數值,我們使用Random類生成0-255之間的隨機整數。
隨機顏色生成器則主要使用上述兩個類來實現,具體代碼片段如下:
Color _color = Color.fromARGB(0, 0, 0, 0);// 改變小球顏色void changeColor() {_color = Color.fromARGB(255, Random().nextInt(255), Random().nextInt(255),Random().nextInt(255));}
隨機位置生成器
隨機位置生成器在程序啟動時使用。要生成隨機位置,方法依然是使用Random類,但要注意隨機值范圍。通常我們需要小球出現的位置在屏幕內,因此,我們需要生成兩次隨機數,分別表示小球初始位置的x和y軸坐標。坐標值分別小于屏幕橫向尺寸和縱向尺寸。當然,它們都要大于0。
另外,我們還需要分別獲取屏幕的寬高。
因此,具體代碼實現如下:
[獲取屏幕寬高]
double screenX, screenY;@overrideWidget build(BuildContext context) {screenX = MediaQuery.of(context).size.width;screenY = MediaQuery.of(context).size.height;...}
[生成隨機位置]
double _x = 0, _y = 0;// 生成小球初始位置和大小void generateBall() {_x = Random().nextDouble() * screenX;_y = Random().nextDouble() * screenY;}
隨機尺寸生成器
隨機尺寸生成器在程序啟動時使用。完成了之前兩種隨機值的生成,到了尺寸這里,就很輕車熟路了。由于隨機尺寸和隨機位置都在程序啟動時調用,且操作對象都是小球,我們將其實現都放在generateBall()方法中。最終代碼如下:
double _x = 0, _y = 0, _size = 0;// 生成小球初始位置和大小void generateBall() { _size = Random().nextDouble() * (screenY - screenX).abs(); _x = Random().nextDouble() * screenX; _y = Random().nextDouble() * screenY;}
小球繪制邏輯
要在界面上繪制小球,我們需要使用CustomPaint組件。而CustomPaint組件需要一個CustomPainter實例。小球的繪制工作主要在繼承了CustomPainter的類中。我們直接看代碼:
import 'package:flutter/material.dart';import 'package:flutter/widgets.dart';class Ball extends CustomPainter { Paint _paint; double _x, _y, _size; Ball(double x, double y, double size, Color color) { _paint = new Paint(); _paint.isAntiAlias = true; _paint.color = color; this._x = x; this._y = y; this._size = size; } @override void paint(Canvas canvas, Size size) { canvas.drawOval(Rect.fromCenter(center: Offset(_x, _y), width: _size, height: _size), _paint); } @override bool shouldRepaint(CustomPainter oldDelegate) { return oldDelegate != this; }}
通過閱讀上面的代碼,可以發現,整個Ball類除了構造方法外,只有兩個override的方法,可以說是很簡單了。
在構造方法中,我們初始化了_paint對象,它是可以看做是“畫筆”;
在paint()方法中,我們調用canvas對象的drawOval方法畫圓,表示小球。canvas可以看做是“畫板”;
shouldRepaint()方法表示在刷新布局的時是否需要重繪,只有在返回true時會發生重繪,這里我們讓程序自行判斷就可以了。
我們將上述代碼保存為ball.dart備用。
注意,這里面無論是位置、顏色還有尺寸,都沒有寫固定的值。是因為該類只負責“畫圓”,而具體畫什么樣的圓,則交給該類的使用者來定義,也就是main.dart。
在main.dart中,我們將App設置為全屏,并添加全屏尺寸的CustomPaint組件,組件內放置Ball對象。
@overrideWidget build(BuildContext context) { screenX = MediaQuery.of(context).size.width; screenY = MediaQuery.of(context).size.height; return Scaffold( body: GestureDetector( child: Container( width: double.infinity, height: double.infinity, child: CustomPaint(painter: Ball(_x, _y, _size, _color))), onTap: () { // 改變小球顏色 changeColor(); }, onDoubleTap: () { // 暫停/恢復移動 _keep_move = !_keep_move; }, onLongPress: () { // 自動改變小球顏色 _auto_change_color = !_auto_change_color; }, ));}
上述代碼中,GestureDetector組件負責接收用戶點擊事件,其中的_keep_move、_auto_change_color都是布爾類型變量,是小球移動和自動變色功能的開關。
接下來,我們在initState()方法中調用之前的隨機位置生成器、隨機尺寸生成器和隨機顏色生成器,賦值_x、_y、_size和_color。
@overridevoid initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { generateBall(); changeColor(); calculateMoveAngle(); startMove(); });}
這里面,calculateMoveAngle()和startMove()方法分別對應初始運動方向生成器以及開始運動并定期更新UI的方法。除了這兩個方法外,如果現在運行程序的話,應該可以看到一個靜態的小球出現在屏幕上了,并且隨著每次重新運行程序,小球的樣式和位置都將發生變化。
接下來,我們就來讓小球動起來吧!
小球運動邏輯
要讓小球準確無誤地運動,我們需要遵循以下步驟:首先生成一個隨機的運動方向;然后以60FPS的頻率,每次在運動方向上前進5個像素的步長(當然,你可以自定義);最后還要注意邊界判定,在小球到達屏幕邊緣時正確轉向。
下面我們逐個實現。
初始運動方向生成器
既然是隨機方向,那么平面上360度范圍內任何一個角度都有可能。因此,我們這里需要先生成0-360范圍內的值。然后根據三角函數和運動方向的速度,計算出橫、縱坐標的速度。其實很簡單,就是勾股定理。
double _step_x, _step_y, _angle;// 計算小球初始移動角度(方向)void calculateMoveAngle() { _angle = Random().nextDouble() * 360; _step_x = sin(_angle) * _speed; _step_y = cos(_angle) * _speed;}
我們這里把運動速度(_speed)看做是三角形的斜邊,橫、縱坐標的移動速度(_step_x、_step_y)看做是三角形的直角邊即可。沒記錯的話,都是初中幾何知識,不會很難理解。
定向移動位置更新器
前文說到,我們將以60FPS的刷新率更新界面,這也就意味著,每隔大約16ms刷新一次小球位置。因為只有小球的運動,才能讓人感到界面在“更新”。這一步驟,我們用到Timer類。并將更新器在initState()方法中調用,以便程序啟動后,小球即刻運動,也就是前文代碼中見到的startMove()方法。
// 開始移動void startMove() { Timer.periodic(Duration(milliseconds: 16), (timer) { moveBall(); setState(() {}); });}// 小球移動void moveBall() { _x += _step_x; _y += _step_y;}
到此為止,小球已經可以開始沿著某個隨機方向移動了。但很快,它將移出屏幕。
邊界判定
顯然,小球每前進一步,都要做屏幕邊界判定,以防小球移出屏幕范圍。而邊界判定在moveBall()方法中實現似乎是最恰當的。
我們可以輕松地總結出小球移動的規律,當小球移動到屏幕邊緣時,我們只需讓其反向運動即可。比如,小球以3的速度移動并接觸屏幕的右邊緣,接下來,仍以3的速度移動并朝向屏幕的左邊緣。
水平方向如此,垂直方向亦如此。
因此,我們的邊界判定邏輯如下:
// 帶有便捷判定的小球移動void moveBall() { if (_x >= screenX || _x <= 0) { _step_x = 0 - _step_x; } _x += _step_x; if (_y >= screenY || _y <= 0) { _step_y = 0 - _step_y; } _y += _step_y;}
用戶手勢監聽器
最后,配合用戶手勢及相關的布爾變量,在每次刷新小球位置時實現變色和暫停移動。
繼續修改moveBall()方法:
// 帶有便捷判定的小球移動void moveBall() { if (_keep_move) { if (_x >= screenX || _x <= 0) { _step_x = 0 - _step_x; } _x += _step_x; if (_y >= screenY || _y <= 0) { _step_y = 0 - _step_y; } _y += _step_y; if (_auto_change_color) { changeColor(); } }}
到此,程序全部實現完成。下面放上完整的main.dart代碼:
import 'dart:async';import 'dart:math';import 'package:flutter/material.dart';import 'package:flutter/services.dart';import 'ball.dart';void main() { runApp(MyApp());}class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { SystemChrome.setEnabledSystemUIOverlays([]); return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: BounceBall(), ); }}class BounceBall extends StatefulWidget { @override _BounceBallState createState() => _BounceBallState();}class _BounceBallState extends State<BounceBall> { final double _speed = 5; double _x = 0, _y = 0, _size = 0; double _step_x, _step_y, _angle; Color _color = Color.fromARGB(0, 0, 0, 0); bool _auto_change_color = false; bool _keep_move = true; double screenX, screenY; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { generateBall(); changeColor(); calculateMoveAngle(); startMove(); }); } @override Widget build(BuildContext context) { screenX = MediaQuery.of(context).size.width; screenY = MediaQuery.of(context).size.height; return Scaffold( body: GestureDetector( child: Container( width: double.infinity, height: double.infinity, child: CustomPaint(painter: Ball(_x, _y, _size, _color))), onTap: () { // 改變小球顏色 changeColor(); }, onDoubleTap: () { // 暫停/恢復移動 _keep_move = !_keep_move; }, onLongPress: () { // 自動改變小球顏色 _auto_change_color = !_auto_change_color; }, )); } // 開始移動 void startMove() { Timer.periodic(Duration(milliseconds: 16), (timer) { moveBall(); setState(() {}); }); } // 改變小球顏色 void changeColor() { _color = Color.fromARGB(255, Random().nextInt(255), Random().nextInt(255), Random().nextInt(255)); } // 生成小球初始位置和大小 void generateBall() { _size = Random().nextDouble() * (screenY - screenX).abs(); _x = Random().nextDouble() * screenX; _y = Random().nextDouble() * screenY; } // 計算小球初始移動角度(方向) void calculateMoveAngle() { _angle = Random().nextDouble() * 360; _step_x = sin(_angle) * _speed; _step_y = cos(_angle) * _speed; } // 帶有便捷判定的小球移動 void moveBall() { if (_keep_move) { if (_x >= screenX || _x <= 0) { _step_x = 0 - _step_x; } _x += _step_x; if (_y >= screenY || _y <= 0) { _step_y = 0 - _step_y; } _y += _step_y; if (_auto_change_color) { changeColor(); } } }}
讓我們一起讓這個程序跑起來吧!
看完上述內容,是不是對如何用Flutter做桌上彈球有進一步的了解,如果還想學習更多內容,歡迎關注本站行業資訊頻道。
本文由 貴州做網站公司 整理發布,部分圖文來源于互聯網,如有侵權,請聯系我們刪除,謝謝!
c語言中正確的字符常量是用一對單引號將一個字符括起表示合法的字符常量。例如‘a’。數值包括整型、浮點型。整型可用十進制,八進制,十六進制。八進制前面要加0,后面...
2022年天津專場考試原定于3月19日舉行,受疫情影響確定延期,但目前延期后的考試時間推遲。 符合報名條件的考生,須在規定時間登錄招考資訊網(www.zha...
:喜歡聽,樂意看。指很受歡迎?!巴卣官Y料”喜聞樂見:[ xǐ wén lè jiàn ]詳細解釋1. 【解釋】:喜歡聽,樂意看。指很受歡迎。2. 【示例】:這是...
什么是人民幣離岸市場?人民幣離岸市場就是在中國大陸以外的國家及地區經營的人民幣交易市場。離岸貨幣的特性就是在發行國以外的國家及地區進行存放和交易,這個過程是不會受到貨幣發行國的金融法令管制的。而且離岸市場所在國及地區也基本不會約束別國貨幣在本土的經營,因此離岸貨幣被稱為自由貨幣。人民幣離岸市場有哪些三大離岸市場就目前來說,主要的人民幣離岸市場有香港人民帀離岸市場、新加坡人民幣離岸市場、倫敦人民幣離...
(相關資料圖)最近這段時間總有小伙伴問小編下顎粉碎踢詳細怎么使用是什么,小編為此在網上搜尋了一些有關于下顎粉碎踢詳細怎么使用的知識送給大家,希望能解答各位小伙伴的疑惑。下顎粉碎踢是攻擊對手下顎而造成對手腦震蕩而導致的暈眩,如果力量夠強大可以使對方下顎粉碎,普通的也可以造成對手輕微腦震蕩而暈眩,用出就差不多可以一擊必殺。 就算對手的下顎是鋼板做的也沒用,穿透力會直接造成腦震蕩。訓練方法:需要鍛煉韌性...
農業銀行金鑰匙安心得利系列靠譜嗎?金鑰匙安心得利系列的風險等級大多數為中低,根據小編在上一篇文章《農業銀行金鑰匙安心得利保本嗎 預期收益情況詳細記錄》中分析的,這系列理財產品都不保本,但是風險因素對本金和預期預期收益的影響較小。雖然農業銀行在說明中表示農業銀行金鑰匙安心得利不保證本金,但是理財筆記小編認為它是比較安全的。因為結合各銀行此風險等級的理財產品情況來考慮,中低風險的理財產品還未出現過本金...