實戰Flutter程式開發 - Roulette Wheel Selection

閱讀時間約 40 分鐘
  輪盤賭選擇 ( Roulette Wheel Selection ) 策略是最基本的選擇策略之一,在群體中的個體被選中的概率與個體相應的適應度函式的值成正比,且此選擇方式常出現在賭場,而此範例程式主要是藉由下列三個部份來實作類似輪盤賭選擇 ( Roulette Wheel Selection )的功能。 
  1.   繪製輪盤與指針畫面   
  2.   建立隨機的指針旋轉動畫       
  3.   動態顯示選擇結果

1.   繪製輪盤與指針畫面  

  主要是藉由繼承 (extends) CustomePainter 類別建立新的類別並透過 Canvas 內部封裝繪製圖形的 API 分別建立指針與輪盤組件。

  1-1.  建立指針組件      

  在繪製指針前先使用畫布 (Canvas) 所提供的旋轉 (rotate) API 調整角度後;接著再使用 Path 類別來繪製指針的形狀,由於指針底部是圓弧的形狀因此在使用畫弧 (arcTo) API 時需要建立矩形 (Rect)物件用來定義所畫的圓弧範圍並使用 Paint 類別將繪製指針進行漸層式的顏色渲染。
class DrawPointer extends CustomPainter {
  ...
  @override
  void paint(Canvas canvas, Size size) { 
    // 1. Set the starting position of the drawing graphics on the canvas.
    centerOffst = Offset(size.width / 2, size.height / 2);   
    canvas.translate(centerOffst.dx, centerOffst.dy);
    // 2. The angle by which the pointer is rotated before drawing.
    canvas.rotate(getRadianFromAngle(angle));
    // 3. Crate a rectangle object to define the range of the drawing arc.
    centerSize = Size(size.width / 10, size.height / 5);
    var rect = Rect.fromLTWH(0 - centerSize.width / 2, 0 - centerSize.width / 2, centerSize.width, centerSize.width); 
  
    // 4. Use Path object and Paint object to draw the shape and color of pointer
    canvas.drawPath(
      Path()
        ..arcTo(rect, getRadianFromAngle(30), getRadianFromAngle(300), false)
        ..relativeLineTo(centerSize.height, centerSize.width / 30)
        ..relativeLineTo(0, - 10 * centerSize.width / 30)
        ..relativeLineTo( centerSize.width, 17 * centerSize.width / 30)
        ..relativeLineTo(-centerSize.width, 17 * centerSize.width / 30 )
        ..relativeLineTo(0, -10 * centerSize.width / 30)
        ..close(),
      Paint()
        ..isAntiAlias = true
        ..style = PaintingStyle.fill
        ..shader = LinearGradient(
          begin: Alignment.bottomRight, 
          end: Alignment.topLeft,
          colors:  [Colors.blueGrey.withAlpha(200),  Colors.cyan.withAlpha(200)],
          tileMode: TileMode.mirror,
        ).createShader(rect),
    );
    
    canvas.drawCircle(Offset.zero,  
          centerSize.width * 0.2, 
          Paint()
            ..color=Colors.black
            ..style = PaintingStyle.fill
        );
    canvas.drawCircle(Offset.zero,
          centerSize.width * 0.15, 
          Paint()
            ..color=Colors.red
            ..style = PaintingStyle.fill
        );   
  }
}

  1-2.  建立輪盤組件    

    1-2.1. 根據項目比例繪製扇形圖  
    在繪製輪盤前先使用建立矩形 (Rect)物件用來定義所畫圓的範圍與畫布(Canvas) 中的位置後再依據每個項目比例使用畫布 (Canvas) 所提供繪製圓弧 (drawArc) 的API繪製等比例的扇形使用 Paint 類別進行項目顏色的渲染;另外為了將各項目名稱放置相對應扇形圖的中間位置,因此在繪製扇形圖時利用串列 (List) 類別來儲存繪製扇形的起始弧度與掃角弧度。
void drawRouletteWheel(Canvas canvas, Size size) {
  double startAngle = 0, sweepAngle = 0;   
  // 1. Create a rectagle object to define the range and positio of the drawing pie chart.
  Rect rect = Offset(wheelRectStartPointX, wheelRectStartPointY) & Size(diameter, diameter);
 
  for(int repeat = 0; repeat < itemRepeat; repeat++){
    itemPercent.keys.toList().forEach((element) {
      defaultPaint.color = itemColor.containsKey(element) ?  itemColor[element]!:Colors.white; 
      // 2. Draw an equal-scale pie chart according to the project scale.
      sweepAngle = 360 * itemPercent[element]! / itemRepeat;
      canvas.drawArc(rect, getRadianFromAngle(startAngle),  getRadianFromAngle(sweepAngle), true, defaultPaint);
        
      // 3. Use List class to store the start and the end radian of each sector.
      disPercent.add(WheelTextPosition(
        title:element, 
        startRadian:getRadianFromAngle(startAngle), 
        endRadian:getRadianFromAngle(sweepAngle)
      )); 
        
      startAngle = startAngle + sweepAngle;
     
    });
  }
}
    1-2.2. 根據扇形的位置繪製項目名稱
    由於每個項目文字列印長度與相應扇形的弧長並非相同,因此根據計算項目文字所需的弧長與相應扇形圖的弧長之間的差值來調整項目字串列印的起始位置與角度後,再依序將項目文字中的每個字元依序列印到指定的位置中;另外文字的起始角度為90度,因此在計算列印文字的弧度後需要再加上90度的弧度才是實際字元列印的位置與角度。
void drawWheelText(Canvas canvas, List<WheelTextPosition> disText){
  
  // 1. Set the starting position of the title at an angle of 0 degrees.
  canvas.translate(wheelRectStartPointX + radius , wheelRectStartPointY + radius);
  double radian = 0.0;
  for (var element in disText) {
    // 2. calculate the starting radian of the title on the side of the wheel
    radian = calculateStartRadianForString(element);
    // 3. Add the calculated radian by 90 degrees 
    radian += textRotateRadian;
    for (int i = 0; i< element.title.length; i++){
      canvas.save();
      
      // 4. Rotate each character of the title in order according to the starting radian. 
      radian = drawWheelCharWithAngle(canvas,  element.title[i], radian, lastradian );
      
      canvas.restore();
    }
  }
}
    藉由 TextPainter 類別取得列印字串所需的長度並除以半徑得到 sin θ後再使用 asin 函式轉換成弧度。
 double calculateStartRadianForString( WheelTextPosition whellText ) {  
    
  // 1. Generate title length.
  var _textPainter = TextPainter(
        textDirection: TextDirection.ltr,
        text: TextSpan(text: whellText.title, style:textStyle,),
      );
  _textPainter.layout(minWidth: 0,  maxWidth: double.maxFinite);
 
  // 2. Caculate arc length based on ratio of title to raidus.
  double stringWidthForRadian =  math.asin( _textPainter.width / raduis); 
  
  return whellText.startRadian + (whellText.sweepRadian - stringWidthForRadian) / 2 ;
 
}
    藉由 TextPainter 類別取得列印字元所需的高度並加上半徑後作為列印字元的長度並搭配 sin cos 函式計算其列印字元的座標。另外使用TextPainter 類別取得列印字串所需的長度並除以半徑得到 sin θ 後再使用 asin 函式轉換成弧度。
double drawWheelCharWithAngle(Canvas canvas, String disChar, double curRadian){
 
  // 1. Generate character length.
  var _textPainter = TextPainter(
    textDirection: TextDirection.ltr,
    text: TextSpan(text: disChar, style:textStyle,),
  );    
  _textPainter.layout(minWidth: 0,  maxWidth: double.maxFinite);
  // 2. Calculate character print position.
  double radiusWithHeight = radius +_textPainter.height;
  double shiftPointX = radiusWithHeight * math.sin(curRadian);  // sin(90):1
  double shiftPointY = radiusWithHeight * math.cos(curRadian) ; // cos(90):0
  // 3. adjust the print starting position of the canvas.
  canvas.translate(shiftPointX,  -shiftPointY);
  // 4. Caculate radian based on ratio of character to raidus.  
  curRadian = (curRadian +  math.asin( _textPainter.width / radius));
  
  // 5. Rotate the canvas by arc
  canvas.rotate(curRadian);
  _textPainter.paint(canvas, Offset.zero);
  return curRadian;
}

2.  建立隨機的指針旋轉動畫     

    為了讓指針組件能夠產生旋轉的動畫,因此需要建立可變組件 (State fullWidget) 並混入 (with) TickerProviderStateMixin 類別來建立包含計時器 (TickerProvider) 的可變狀態組件並運用 AnimationController 類別進行動畫控制及使用 CurvedAnimation 與 Tween 類別來定義旋轉動畫的行為;最後使用 Gesture Detector 類別來接收使用者點擊畫面中的位置並判斷點擊位置是否在指針的圓弧位置。
class _WheelPointerState extends State<WheelPointerWidget> with TickerProviderStateMixin {
  Animation<double>? _animation;
  AnimationController? _controller;
  Tween<double>? _rotationTween;   
  int pos = 0;
  bool isSelection = false; 
  
  @override
  void initState() {
    super.initState(); 
    _controller = AnimationController(vsync: this, duration: widget.duration); 
    _animation = CurvedAnimation(parent: _controller!, curve: Curves.fastLinearToSlowEaseIn);
    _rotationTween = Tween<double>(begin:  0, end: 0);
    _animation = _rotationTween!.animate(_animation!)
      ..addListener(() {
        setState(() {});
      })
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed){
          isSelection = false;
       
          // 1. Notify of selected radians when animation is done.
          widget.notifyCallbackFunc(isSelection, getRadianFromAngle(pos.toDouble()));
      }
    });
  }
 
  @override 
  void didUpdateWidget (WheelPointerWidget oldWidget){
    super.didUpdateWidget(oldWidget);
    _controller!.duration = widget.duration;    
  }
  @override
  void dispose() {
    _controller!.dispose();
    super.dispose();
  }
  
  void startAnimation() {    
    isSelection = true;
    widget.notifyCallbackFunc(isSelection, 0);
    _controller!.reset();
    // 2. Set the last end of angle as the current starting angle.
    _rotationTween!.begin = pos.toDouble();
    // 3. Generate a random angle from 0 to 360 degrees.  
    pos = math.Random().nextInt(360);
    // 4. Multiply the preset number of rotations by 360 plus a random value.
    _rotationTween!.end = widget.numberOfRevolutions * 360.0 + pos;
     
    // 5. Starts running this animation forward 
    _controller!.forward(); 
  }
  @override 
  Widget build(BuildContext context) {   
    var drawPointer = DrawPointer(_animation!.value);
    return GestureDetector( 
      child: CustomPaint(
        size: widget.canvasSize,
        painter: drawPointer
      ),
      onTapDown: (tapDownDetails) {
       
        // 6. if the animations has been completed and determine the click position.
        if(!isSelection && drawPointer.isclick(tapDownDetails.localPosition)){               
          startAnimation();                
        }
      },
    );
  }
}

3.  動態顯示選擇結果

    最後在指針動畫結束時需要依據指針的角度換算成弧度並以此確認選擇項目後顯示項目名稱,但由於完成上述行為需要將各組件態進行跨組件因此可使用 provider 套件來簡化實作流程。  

 3-1.  建立跨組件的共享數據類別

    首先建立一個可保存跨組件的共享數據類別,此類別是藉由繼承 (extends) InheritedWidget 類別而建立的 ShareDataInheritedWidget 類別,目的是實踐一個可由上至下的共享數據物件且由於具體保存數據類型不可預期,為了通用性則使用泛型的方式來定義此類別。
// 1. Build a generic InheritedWidget to hold state that needs to be shared across components.
class ShareDataInheritedWidget<T> extends InheritedWidget {
  const ShareDataInheritedWidget({
        Key? key,
        required this.prvObj,
        required Widget child
  }) : super(key: key, child: child);
  final T prvObj;
  // 2. Define a convenience method to facilitate the widgets in the subtree to obtain shared data.
  static ShareDataInheritedWidget? of<T>(BuildContext context){
    final ShareDataInheritedWidget? inheritedObj = context.dependOnInheritedWidgetOfExactType<ShareDataInheritedWidget<T>>();
    assert(inheritedObj != null, "No this object found in context!");
    return inheritedObj;
  }
  @override
  bool updateShouldNotify(ShareDataInheritedWidget<T> oldWidget) {
    // 3. This callback determines whether to notify the datadependent widgets in the subtree to rebuild when the data changes.
    return  prvObj != oldWidget.prvObj;
  }
}

  

  3-2.  建立跨組件的事件通知類別

     由於數據發生變化的時候需要重新建構 ShareDataInherited Widget 類別因此需要建立一個共享狀態的類別,此類別是藉由繼承 (extends) ChangeNotifier 類別而建立的 ChangeDataNotifier 類別,其目的是在共享狀態變更時能使用notifyListeners函數來通知註冊此事件的 ShareDataInherited Widget 類別進而實踐跨組件 (Widget) 的事件通知行為且由於具體回傳函數類型不可預期,為了通用性則使用泛型的方式來定義此類別。 
class ChangeDataNotify<T> extends ChangeNotifier {  
  ChangeDataNotify(this.hookData);
  late T hookData;
  void changeDataFunc(T changeData ){
    hookData = changeData;
    // 1. Notify the listeners to rebuild the update state of the InheritedWidget.
    notifyListeners();
  }
  T getDataFunc(){
    return hookData;
  }
}

   

  3-3.  建立 RouletteWheelWidget 組件

    使用前述的類別分別建立 DisplayTitleWidget 組件並搭配使用Consumer 組件用來即時顯示選擇項目和藉由整合輪盤組件和指針組件並搭配使用 provider 套件中的Provider.of 組件用來實現選擇項目更新的即時通知,最後整合建立 RouletteWheelWidget 組件。
class DisplayTitleWidget extends StatelessWidget {
  const DisplayTitleWidget ({Key? key}):super(key: key);
  @override 
  Widget build(BuildContext context) {
    // 1. ChangeDataNotify is provided to widgets in our app through the ChangeNotifierProvider declaration at the top. 
    return Consumer<ChangeDataNotify<String>>(
      builder: (context, obj, child) {
        // 2.  Use ChangeDataNotify widget  here, without rebuilding every time.
        return Text(obj.getDataFunc(), 
          style: const TextStyle(
            color: Colors.brown, 
            fontSize: 24, 
            fontWeight: FontWeight.normal,
            fontStyle: FontStyle.normal,
            decoration: TextDecoration.none,
          ),
        );
      }
    );
  }
}
class RouletteWheelWidget extends StatefulWidget  {
  const RouletteWheelWidget({
    Key? key, 
    required this.radius, 
    required this.textStyle,
    required this.itemRepeat,
    required this.itemPercent, 
    required this.itemColor,    
  }) : super(key: key);
  final double radius;
  final TextStyle textStyle;
  final int itemRepeat;   
  final Map<String, double> itemPercent;
  final Map<String, Color> itemColor;
  @override 
  State<RouletteWheelWidget> createState() => _RouletteWheelState();
}
class _RouletteWheelState extends State<RouletteWheelWidget> {
  // 1. Create uniquely identify elements for access by other object associated with elements.
  final GlobalKey <WheelWidgetState> _wheelWidgetKey = GlobalKey();
  
  WheelWidget? wheelWidget;
  String title = "未知"; 
  void notifyResultFunc(bool isSelection ,double radian) { 
    title = isSelection ? "選擇中" : _wheelWidgetKey.currentState!.getTitleFromRadian(radian);
    // 2. Use Provider.of with the listen parameter set to false without rebuild ChangeDataNotify<String> wedget.
    Provider.of<ChangeDataNotify<String>>(context, listen: false).changeDataFunc(title);   
  }
  @override
  Widget build(BuildContext context) {
    
    wheelWidget ??=  WheelWidget ( 
          key: _wheelWidgetKey,
          radius: widget.radius,
          canvasSize: Size(widget.radius*2, widget.radius*2),
          itemRepeat: widget.itemRepeat,
          itemPercent: widget.itemPercent,
          itemColor:  widget.itemColor,
          textStyle:  widget.textStyle,
    );  
    return Column(
          mainAxisAlignment: MainAxisAlignment.center,
          mainAxisSize: MainAxisSize.min,
          children:[ 
      
            // 3. Use ShareDataInheritedWidget<String> to share title with DisplayTitleWidget widget.         
            ShareDataInheritedWidget<String>(
              prvObj: title,
              child: const DisplayTitleWidget(),              
            ),
            SizedBox(
              height: widget.textStyle.fontSize! * 2,
            ),        
            Stack (                
              children:[                   
                wheelWidget!,                           
                WheelPointerWidget(
                  canvasSize: Size(widget.radius * 2, widget.radius * 2),
                  numberOfRevolutions: 3,
                  duration: const Duration(seconds: 7),
                  notifyCallbackFunc:notifyResultFunc,
              ] 
          ),
        ]
    );
  }
}

4.  各平台的執行結果

    最後使用 provider 套件中的 ChangeNotifierProvider 組件將RouletteWheelWidget 組件與ChangeDataNotify 組件作為參數後建立RoulettWheelSelectionApp 組件在各不同的平台進行實測。
void main() => runApp(const RoulettWheelSelectionApp());
class RoulettWheelSelectionApp extends StatelessWidget {
  const RoulettWheelSelectionApp({Key? key}):super(key: key);
  @override
  Widget build (BuildContext context) {
    return  MaterialApp(
      home: Scaffold(
        body: Center(
          // 1. Use ChangeNotifierProvider widget of provider package to provides an instance of a ChangeNotifier. 
          child: ChangeNotifierProvider(
            // 2. Create ChangeDataNotify class to keep private state.
            create: (context) =>  ChangeDataNotify<String>("未選擇"),      
            // 3. Initialize RouletteWheelWidget class 
            child:const RouletteWheelWidget(
              radius: 150, 
              textStyle: TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.bold,
                color: Colors.black45
              ),
              itemRepeat: 2,
              itemPercent: {"第一項":0.4, "第二項":0.3,"第三項": 0.2, "第四項":0.1}, 
              itemColor: {"第一項":Colors.green, "第二項":Colors.yellow ,"第三項": Colors.red, "第四項":Colors.brown},
            ), 
          ),
        ),
      ),
    );
  }
} 

  4-1.  Windows Desktop

  4-2.  Chrome Web

  4-3.  Linux APP (Ubuntu)

  4-4.  Android APP

GitHub : https://github.com/crosscode-software/roulette_wheel_selection
Pub.dev : https://pub.dev/packages/roulette_wheel_selection
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
說明Flutter 模組(Module)專案範例的架構與如何載入Android專案中的流程與執行畫面
說明Flutter 插件(Plugin)專案範例的架構與實際載入並執行在各平台的顯示畫面
說明Flutter 包(Package)專案範例的架構與實際載入並執行在各平台的顯示畫面
說明Flutter 骨架(skeleton)專案範例的架構與在各平台執行的顯示畫面
說明Flutter 應用軟體(Application)專案範例的架構與在各平台執行的顯示畫面
說明如何在Windows系統中安裝 Flutter 與 Visual Studio Code 的開發環境
說明Flutter 模組(Module)專案範例的架構與如何載入Android專案中的流程與執行畫面
說明Flutter 插件(Plugin)專案範例的架構與實際載入並執行在各平台的顯示畫面
說明Flutter 包(Package)專案範例的架構與實際載入並執行在各平台的顯示畫面
說明Flutter 骨架(skeleton)專案範例的架構與在各平台執行的顯示畫面
說明Flutter 應用軟體(Application)專案範例的架構與在各平台執行的顯示畫面
說明如何在Windows系統中安裝 Flutter 與 Visual Studio Code 的開發環境
你可能也想看
Google News 追蹤
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
Thumbnail
可能包含敏感內容
*吉原可接待外國人激安店 *CP值高不容易踩雷店。
Thumbnail
可能包含敏感內容
*泡泡浴中級店以上90min均在3~4萬日幣以上,高級泡泡浴120min(只有2小時選項),外國人在7~8萬+日幣以上。但一旦踩雷就是打水漂了。JoJo在實際體驗與客戶反饋 *付費專區會提供的資料大部分都是外國人也能體驗/可以避免花了1萬台幣叫來卻是阿婆的窘境。 以下詳細說明。
Thumbnail
這篇文章分享了就業服務乙級技術士證照考試衝刺心得和結果。文章中包含了為什麼想考取證照的起因、基礎背景、準備方式、考試當天的經驗分享,以及對考試的祝福。希望能幫助讀者順利考取證照。
Thumbnail
以下結合幾個基本觀念,用實戰範例說明。 基本觀念1:底型突破 基本觀念2:大週期找機會,小週期抓買點 基本觀念3:買點3個,其一為區間突破,其二為回檔轉折,其三為超跌反彈(反轉)。 那麼,我們開始吧。 基本觀念1:底型突破 首先,4543萬在,利用還原權息月k圖,呈現如下。 在去年
Thumbnail
Hi!大家好,我是 Alvin. 距離上次寫文章的時間,剛好隔了 12 個月,這個期間也有不少人問我: 「為什麼不繼續寫文章了」?
Thumbnail
這篇我們來談談有關Solidity Function visibility(能見度) Solidity的function visibility有四個關鍵字(private、internal、external或public)。
Thumbnail
所以寫程式的人都知道的一句話:「Hello World」,每一個學程式語言開始的時候都是從這句話開始的,我們也不免俗的來上這一句,從Hello World來看看智能合約入門是個什麼樣子。
Thumbnail
鴨子的泰然自若似乎能給我們許多啟示。之前介紹過英文說要像水面上的鴨子(duck on water)一樣處變不驚,水面上悠哉地滑啊滑,水面下的腳丫子其實打水打得非常激烈。今天介紹的另一句英文也跟鴨子有關,我們要透過變裝皇后的實境秀節目來學它的意境。
Thumbnail
上個月開了Potato後就擱著沒動了。一方面忙,二方面在思考怎麼做這個平台,這幾天既然卡在杜拜,那就好好研究一下吧。 先看了一下Potato 幾個區塊。 推薦的部份除了官方發文外,其他的文章看不出來有特別需要推薦的地方,感覺和熱門的文章差不多,有很多文章的贊數甚至沒有其他區塊多。 晃了一下發現文
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
Thumbnail
可能包含敏感內容
*吉原可接待外國人激安店 *CP值高不容易踩雷店。
Thumbnail
可能包含敏感內容
*泡泡浴中級店以上90min均在3~4萬日幣以上,高級泡泡浴120min(只有2小時選項),外國人在7~8萬+日幣以上。但一旦踩雷就是打水漂了。JoJo在實際體驗與客戶反饋 *付費專區會提供的資料大部分都是外國人也能體驗/可以避免花了1萬台幣叫來卻是阿婆的窘境。 以下詳細說明。
Thumbnail
這篇文章分享了就業服務乙級技術士證照考試衝刺心得和結果。文章中包含了為什麼想考取證照的起因、基礎背景、準備方式、考試當天的經驗分享,以及對考試的祝福。希望能幫助讀者順利考取證照。
Thumbnail
以下結合幾個基本觀念,用實戰範例說明。 基本觀念1:底型突破 基本觀念2:大週期找機會,小週期抓買點 基本觀念3:買點3個,其一為區間突破,其二為回檔轉折,其三為超跌反彈(反轉)。 那麼,我們開始吧。 基本觀念1:底型突破 首先,4543萬在,利用還原權息月k圖,呈現如下。 在去年
Thumbnail
Hi!大家好,我是 Alvin. 距離上次寫文章的時間,剛好隔了 12 個月,這個期間也有不少人問我: 「為什麼不繼續寫文章了」?
Thumbnail
這篇我們來談談有關Solidity Function visibility(能見度) Solidity的function visibility有四個關鍵字(private、internal、external或public)。
Thumbnail
所以寫程式的人都知道的一句話:「Hello World」,每一個學程式語言開始的時候都是從這句話開始的,我們也不免俗的來上這一句,從Hello World來看看智能合約入門是個什麼樣子。
Thumbnail
鴨子的泰然自若似乎能給我們許多啟示。之前介紹過英文說要像水面上的鴨子(duck on water)一樣處變不驚,水面上悠哉地滑啊滑,水面下的腳丫子其實打水打得非常激烈。今天介紹的另一句英文也跟鴨子有關,我們要透過變裝皇后的實境秀節目來學它的意境。
Thumbnail
上個月開了Potato後就擱著沒動了。一方面忙,二方面在思考怎麼做這個平台,這幾天既然卡在杜拜,那就好好研究一下吧。 先看了一下Potato 幾個區塊。 推薦的部份除了官方發文外,其他的文章看不出來有特別需要推薦的地方,感覺和熱門的文章差不多,有很多文章的贊數甚至沒有其他區塊多。 晃了一下發現文