鴻蒙NEXT開發案例:計數器

zhongcx發表於2024-11-17

【引言】(完整程式碼在最後面)

本文將透過一個簡單的計數器應用案例,介紹如何利用鴻蒙NEXT的特性開發高效、美觀的應用程式。我們將涵蓋計數器的基本功能實現、使用者介面設計、資料持久化及動畫效果的新增。

【環境準備】

電腦系統:windows 10

開發工具:DevEco Studio 5.0.1 Beta3 Build Version: 5.0.5.200

工程版本:API 13

真機:Mate60 Pro

語言:ArkTS、ArkUI

【專案概述】

本專案旨在建立一個多計數器應用,使用者可以自由地新增、編輯、重置和刪除計數器。每個計數器具有獨立的名稱、當前值、增加步長和減少步長。應用還包括總計數的顯示,以便使用者快速瞭解所有計數器的總和。

【功能實現】

1、計數器模型

首先,我們定義了一個CounterItem類來表示單個計數器,其中包含了計數器的基本屬性和行為。

@ObservedV2
class CounterItem {
  id: number = ++Index.counterId;
  @Trace name: string;
  @Trace count: number = 0;
  @Trace scale: ScaleOptions = { x: 1, y: 1 };
  upStep: number = 1;
  downStep: number = 1;

  constructor(name: string) {
    this.name = name;
  }
}

2、應用入口與狀態管理

應用的主入口元件Index負責管理計數器列表、總計數、以及UI的狀態。這裡使用了@State和@Watch裝飾器來監控狀態的變化。

@Entry
@Component
struct Index {
  static counterStorageKey: string = "counterStorageKey";
  static counterId: number = 0;

  @State listSpacing: number = 20;
  @State listItemHeight: number = 120;
  @State baseFontSize: number = 60;
  @State @Watch('updateTotalCount') counters: CounterItem[] = [];
  @State totalCount: number = 0;
  @State isSheetVisible: boolean = false;
  @State selectedIndex: number = 0;

  // ...其他方法
}

3、資料持久化

為了保證資料在應用重啟後仍然可用,我們使用了preferences模組來同步地讀取和寫入資料。

saveDataToLocal() {
  const saveData: object[] = this.counters.map(counter => ({
    count: counter.count,
    name: counter.name,
    upStep: counter.upStep,
    downStep: counter.downStep,
  }));

  this.dataPreferences?.putSync(Index.counterStorageKey, JSON.stringify(saveData));
  this.dataPreferences?.flush();
}

4、使用者介面

使用者介面的設計採用了現代簡潔的風格,主要由頂部的總計數顯示區、中間的計數器列表區和底部的操作按鈕組成。列表項支援左右滑動以顯示重置和刪除按鈕。

@Builder
itemStart(index: number) {
  Row() {
    Text('重置').fontColor(Color.White).fontSize('40lpx').textAlign(TextAlign.Center).width('180lpx');
  }
  .height('100%')
  .backgroundColor(Color.Orange)
  .justifyContent(FlexAlign.SpaceEvenly)
  .borderRadius({ topLeft: 10, bottomLeft: 10 })
  .onClick(() => {
    this.counters[index].count = 0;
    this.updateTotalCount();
    this.listScroller.closeAllSwipeActions();
  });
}

5、動畫效果

當使用者新增新的計數器時,透過動畫效果讓新計數器逐漸放大至正常尺寸,提升使用者體驗。

this.counters.unshift(new CounterItem(`新計數項${Index.counterId}`));
this.listScroller.scrollTo({ xOffset: 0, yOffset: 0 });
this.counters[0].scale = { x: 0.8, y: 0.8 };

animateTo({
  duration: 1000,
  curve: curves.springCurve(0, 10, 80, 10),
  iterations: 1,
  onFinish: () => {}
}, () => {
  this.counters[0].scale = { x: 1, y: 1 };
});

【總結】

透過上述步驟,我們成功地構建了一個具備基本功能的計數器應用。在這個過程中,我們不僅學習瞭如何使用鴻蒙NEXT提供的各種API,還掌握瞭如何結合動畫、資料持久化等技術點來最佳化使用者體驗。希望本文能為你的鴻蒙開發之旅提供一些幫助和靈感!

【完整程式碼】

import { curves, promptAction } from '@kit.ArkUI' // 匯入動畫曲線和提示操作
import { preferences } from '@kit.ArkData' // 匯入偏好設定模組

@ObservedV2
  // 觀察者裝飾器,監控狀態變化
class CounterItem {
  id: number = ++Index.counterId // 計數器ID,自動遞增
  @Trace name: string // 計數器名稱
  @Trace count: number = 0 // 計數器當前值,初始為0
  @Trace scale: ScaleOptions = { x: 1, y: 1 } // 計數器縮放比例,初始為1
  upStep: number = 1 // 增加步長,初始為1
  downStep: number = 1 // 減少步長,初始為1

  constructor(name: string) { // 建構函式,初始化計數器名稱
    this.name = name
  }
}

@Entry
  // 入口元件裝飾器
@Component
  // 元件裝飾器
struct Index {
  static counterStorageKey: string = "counterStorageKey" // 儲存計數器資料的鍵
  static counterId: number = 0 // 靜態計數器ID
  @State listSpacing: number = 20 // 列表項間距
  @State listItemHeight: number = 120 // 列表項高度
  @State baseFontSize: number = 60 // 基礎字型大小
  @State @Watch('updateTotalCount') counters: CounterItem[] = [] // 計數器陣列,監控總計數更新
  @State totalCount: number = 0 // 總計數
  @State isSheetVisible: boolean = false // 控制底部彈出表單的可見性
  @State selectedIndex: number = 0 // 當前選中的計數器索引
  listScroller: ListScroller = new ListScroller() // 列表滾動器例項
  dataPreferences: preferences.Preferences | undefined = undefined // 偏好設定例項

  updateTotalCount() { // 更新總計數的方法
    let total = 0; // 初始化總計數
    for (let i = 0; i < this.counters.length; i++) { // 遍歷計數器陣列
      total += this.counters[i].count // 累加每個計數器的count值
    }
    this.totalCount = total // 更新總計數
    this.saveDataToLocal() // 儲存資料到本地
  }

  saveDataToLocal() { // 儲存計數器資料到本地的方法
    const saveData: object[] = [] // 初始化儲存資料的陣列
    for (let i = 0; i < this.counters.length; i++) { // 遍歷計數器陣列
      let counter: CounterItem = this.counters[i] // 獲取當前計數器
      saveData.push(Object({
        // 將計數器資料新增到儲存陣列
        count: counter.count,
        name: counter.name,
        upStep: counter.upStep,
        downStep: counter.downStep,
      }))
    }
    this.dataPreferences?.putSync(Index.counterStorageKey, JSON.stringify(saveData)) // 將資料儲存到偏好設定
    this.dataPreferences?.flush() // 重新整理偏好設定
  }

  @Builder
  // 構建器裝飾器
  itemStart(index: number) { // 列表項左側的重置按鈕
    Row() {
      Text('重置').fontColor(Color.White).fontSize('40lpx')// 顯示“重置”文字
        .textAlign(TextAlign.Center)// 文字居中
        .width('180lpx') // 設定寬度
    }
    .height('100%') // 設定高度
    .backgroundColor(Color.Orange) // 設定背景顏色
    .justifyContent(FlexAlign.SpaceEvenly) // 設定內容均勻分佈
    .borderRadius({ topLeft: 10, bottomLeft: 10 }) // 設定圓角
    .onClick(() => { // 點選事件
      this.counters[index].count = 0 // 重置計數器的count為0
      this.updateTotalCount() // 更新總計數
      this.listScroller.closeAllSwipeActions() // 關閉所有滑動操作
    })
  }

  @Builder
  // 構建器裝飾器
  itemEnd(index: number) { // 列表項右側的刪除按鈕
    Row() {
      Text('刪除').fontColor(Color.White).fontSize('40lpx')// 顯示“刪除”文字
        .textAlign(TextAlign.Center)// 文字居中
        .width('180lpx') // 設定寬度
    }
    .height('100%') // 設定高度
    .backgroundColor(Color.Red) // 設定背景顏色
    .justifyContent(FlexAlign.SpaceEvenly) // 設定內容均勻分佈
    .borderRadius({ topRight: 10, bottomRight: 10 }) // 設定圓角
    .onClick(() => { // 點選事件
      this.counters.splice(index, 1) // 從陣列中刪除計數器
      this.listScroller.closeAllSwipeActions() // 關閉所有滑動操作
      promptAction.showToast({
        // 顯示刪除成功的提示
        message: '刪除成功',
        duration: 2000,
        bottom: '400lpx'
      });
    })
  }

  aboutToAppear(): void { // 元件即將出現時呼叫
    const options: preferences.Options = { name: Index.counterStorageKey }; // 獲取偏好設定選項
    this.dataPreferences = preferences.getPreferencesSync(getContext(), options); // 同步獲取偏好設定
    const savedData: string = this.dataPreferences.getSync(Index.counterStorageKey, "[]") as string // 獲取儲存的資料
    const parsedData: Array<CounterItem> = JSON.parse(savedData) as Array<CounterItem> // 解析資料
    console.info(`parsedData:${JSON.stringify(parsedData)}`) // 列印解析後的資料
    for (const item of parsedData) { // 遍歷解析後的資料
      const newItem = new CounterItem(item.name) // 建立新的計數器例項
      newItem.count = item.count // 設定計數器的count
      newItem.upStep = item.upStep // 設定計數器的upStep
      newItem.downStep = item.downStep // 設定計數器的downStep
      this.counters.push(newItem) // 將新計數器新增到陣列
    }
    this.updateTotalCount() // 更新總計數
  }

  build() { // 構建元件的UI
    Column() {
      Text('計數器')// 顯示標題
        .width('100%')// 設定寬度
        .height('88lpx')// 設定高度
        .fontSize('38lpx')// 設定字型大小
        .backgroundColor(Color.White)// 設定背景顏色
        .textAlign(TextAlign.Center) // 文字居中
      Column() {
        List({ space: this.listSpacing, scroller: this.listScroller }) { // 建立列表
          ForEach(this.counters, (counter: CounterItem, index: number) => { // 遍歷計數器陣列
            ListItem() { // 列表項
              Row() { // 行佈局
                Stack() { // 堆疊佈局
                  Rect().fill("#65DACC").width(`${this.baseFontSize / 2}lpx`).height('4lpx') // 上方橫條
                  Circle()// 圓形按鈕
                    .width(`${this.baseFontSize}lpx`)
                    .height(`${this.baseFontSize}lpx`)
                    .fillOpacity(0)// 透明填充
                    .borderWidth('4lpx')// 邊框寬度
                    .borderRadius('50%')// 圓角
                    .borderColor("#65DACC") // 邊框顏色
                }
                .width(`${this.baseFontSize * 2}lpx`) // 設定寬度
                .height(`100%`) // 設定高度
                .clickEffect({ scale: 0.6, level: ClickEffectLevel.LIGHT }) // 點選效果
                .onClick(() => { // 點選事件
                  counter.count -= counter.downStep // 減少計數器的count
                  this.updateTotalCount() // 更新總計數
                })

                Stack() { // 堆疊佈局
                  Text(counter.name)// 顯示計數器名稱
                    .fontSize(`${this.baseFontSize / 2}lpx`)// 設定字型大小
                    .fontColor(Color.Gray)// 設定字型顏色
                    .margin({ bottom: `${this.baseFontSize * 2}lpx` }) // 設定底部邊距
                  Text(`${counter.count}`)// 顯示計數器當前值
                    .fontColor(Color.Black)// 設定字型顏色
                    .fontSize(`${this.baseFontSize}lpx`) // 設定字型大小
                }.height('100%') // 設定高度
                Stack() { // 堆疊佈局
                  Rect().fill("#65DACC").width(`${this.baseFontSize / 2}lpx`).height('4lpx') // 下方橫條
                  Rect()
                    .fill("#65DACC")
                    .width(`${this.baseFontSize / 2}lpx`)
                    .height('4lpx')
                    .rotate({ angle: 90 }) // 垂直橫條
                  Circle()// 圓形按鈕
                    .width(`${this.baseFontSize}lpx`)// 設定寬度
                    .height(`${this.baseFontSize}lpx`)// 設定高度
                    .fillOpacity(0)// 透明填充
                    .borderWidth('4lpx')// 邊框寬度
                    .borderRadius('50%')// 圓角
                    .borderColor("#65DACC") // 邊框顏色
                }
                .width(`${this.baseFontSize * 2}lpx`) // 設定堆疊佈局寬度
                .height(`100%`) // 設定堆疊佈局高度
                .clickEffect({ scale: 0.6, level: ClickEffectLevel.LIGHT }) // 點選效果
                .onClick(() => { // 點選事件
                  counter.count += counter.upStep // 增加計數器的count
                  this.updateTotalCount() // 更新總計數
                })
              }
              .width('100%') // 設定列表項寬度
              .backgroundColor(Color.White) // 設定背景顏色
              .justifyContent(FlexAlign.SpaceBetween) // 設定內容兩端對齊
              .padding({ left: '30lpx', right: '30lpx' }) // 設定左右內邊距
            }
            .height(this.listItemHeight) // 設定列表項高度
            .width('100%') // 設定列表項寬度
            .margin({
              // 設定列表項的外邊距
              top: index == 0 ? this.listSpacing : 0, // 如果是第一個項,設定上邊距
              bottom: index == this.counters.length - 1 ? this.listSpacing : 0 // 如果是最後一個項,設定下邊距
            })
            .borderRadius(10) // 設定圓角
            .clip(true) // 裁剪超出部分
            .swipeAction({ start: this.itemStart(index), end: this.itemEnd(index) }) // 設定滑動操作
            .scale(counter.scale) // 設定計數器縮放比例
            .onClick(() => { // 點選事件
              this.selectedIndex = index // 設定當前選中的計數器索引
              this.isSheetVisible = true // 顯示底部彈出表單
            })

          }, (counter: CounterItem) => counter.id.toString())// 使用計數器ID作為唯一鍵
            .onMove((from: number, to: number) => { // 列表項移動事件
              const tmp = this.counters.splice(from, 1); // 從原位置移除計數器
              this.counters.splice(to, 0, tmp[0]) // 插入到新位置
            })

        }
        .scrollBar(BarState.Off) // 隱藏捲軸
        .width('648lpx') // 設定列表寬度
        .height('100%') // 設定列表高度
      }
      .width('100%') // 設定列寬度
      .layoutWeight(1) // 設定佈局權重

      Row() { // 底部合計行
        Column() { // 列布局
          Text('合計').fontSize('26lpx').fontColor(Color.Gray) // 顯示“合計”文字
          Text(`${this.totalCount}`).fontSize('38lpx').fontColor(Color.Black) // 顯示總計數
        }.margin({ left: '50lpx' }) // 設定左邊距
        .justifyContent(FlexAlign.Start) // 設定內容左對齊
        .alignItems(HorizontalAlign.Start) // 設定專案左對齊
        .width('300lpx') // 設定列寬度

        Row() { // 新增按鈕行
          Text('新增').fontColor(Color.White).fontSize('28lpx') // 顯示“新增”文字
        }
        .onClick(() => { // 點選事件
          this.counters.unshift(new CounterItem(`新計數項${Index.counterId}`)) // 新增新計數器
          this.listScroller.scrollTo({ xOffset: 0, yOffset: 0 }) // 滾動到頂部
          this.counters[0].scale = { x: 0.8, y: 0.8 }; // 設定新計數器縮放
          animateTo({
            // 動畫效果
            duration: 1000, // 動畫持續時間
            curve: curves.springCurve(0, 10, 80, 10), // 動畫曲線
            iterations: 1, // 動畫迭代次數
            onFinish: () => { // 動畫完成後的回撥
            }
          }, () => {
            this.counters[0].scale = { x: 1, y: 1 }; // 恢復縮放
          })
        })

        .width('316lpx') // 設定按鈕寬度
        .height('88lpx') // 設定按鈕高度
        .backgroundColor("#65DACC") // 設定按鈕背景顏色
        .borderRadius(10) // 設定按鈕圓角
        .justifyContent(FlexAlign.Center) // 設定內容居中
      }.width('100%').height('192lpx').backgroundColor(Color.White) // 設定行寬度和高度

    }
    .backgroundColor("#f2f2f7") // 設定背景顏色
    .width('100%') // 設定寬度
    .height('100%') // 設定高度
    .bindSheet(this.isSheetVisible, this.mySheet(), {
      // 繫結底部彈出表單
      height: 300, // 設定表單高度
      dragBar: false, // 禁用拖動條
      onDisappear: () => { // 表單消失時的回撥
        this.isSheetVisible = false // 隱藏表單
      }
    })
  }

  @Builder
  // 構建器裝飾器
  mySheet() { // 建立底部彈出表單
    Column({ space: 20 }) { // 列布局,設定間距
      Row() { // 行佈局
        Text('計數標題:') // 顯示“計數標題”文字
        TextInput({ text: this.counters[this.selectedIndex].name }).width('300lpx').onChange((value) => { // 輸入框,繫結計數器名稱
          this.counters[this.selectedIndex].name = value // 更新計數器名稱
        })

      }

      Row() { // 行佈局
        Text('增加步長:') // 顯示“增加步長”文字
        TextInput({ text: `${this.counters[this.selectedIndex].upStep}` })// 輸入框,繫結增加步長
          .width('300lpx')// 設定輸入框寬度
          .type(InputType.Number)// 設定輸入框型別為數字
          .onChange((value) => { // 輸入框變化事件
            this.counters[this.selectedIndex].upStep = parseInt(value) // 更新增加步長
            this.updateTotalCount() // 更新總計數
          })

      }

      Row() { // 行佈局
        Text('減少步長:') // 顯示“減少步長”文字
        TextInput({ text: `${this.counters[this.selectedIndex].downStep}` })// 輸入框,繫結減少步長
          .width('300lpx')// 設定輸入框寬度
          .type(InputType.Number)// 設定輸入框型別為數字
          .onChange((value) => { // 輸入框變化事件
            this.counters[this.selectedIndex].downStep = parseInt(value) // 更新減少步長
            this.updateTotalCount() // 更新總計數
          })

      }
    }
    .justifyContent(FlexAlign.Start) // 設定內容左對齊
    .padding(40) // 設定內邊距
    .width('100%') // 設定寬度
    .height('100%') // 設定高度
    .backgroundColor(Color.White) // 設定背景顏色
  }
}

  

相關文章