博客 / 詳情

返回

鴻蒙 PiPWindow 開發實戰:多場景畫中畫功能深度實現與場景化落地

在移動應用開發中,畫中畫(Picture-in-Picture,PiP)功能已成為提升用户體驗的核心特性之一。無論是視頻App讓用户邊刷資訊邊追劇,會議軟件支持邊看文檔邊參會,還是直播平台允許用户邊互動邊觀看,畫中畫都能打破單一窗口的限制,實現多任務並行。鴻蒙系統通過@ohos.PiPWindow模塊提供了標準化、高擴展性的畫中畫解決方案,覆蓋手機、平板、PC、電視等多終端,支持API version 11及以上版本。本文將結合視頻播放、視頻通話、在線會議、直播四大核心場景,通過完整案例講解PiPWindow的深度集成與場景化優化技巧。

一、模塊核心能力與場景適配基礎

1.1 核心能力全景

@ohos.PiPWindow模塊的核心價值在於全場景適配+精細化控制,其核心能力可概括為:

  • 跨設備兼容:API 20前支持手機、平板,API 20後新增PC/2in1設備支持,電視、穿戴設備同樣適配
  • 多模板預設:提供視頻播放、通話、會議、直播4類模板,無需從零開發控制欄
  • 全生命週期管理:支持啓動、停止、恢復、異常處理等完整狀態流轉
  • 精細化控制:窗口尺寸調整、控制欄自定義、狀態監聽、自動啓動配置等
  • 靈活擴展:支持自定義UI疊加、LocalStorage狀態同步、XComponent內容渲染等高級能力

1.2 場景-模板-控件組適配關係

不同場景對應不同的模板類型和控制組件,合理搭配能大幅提升開發效率,具體適配關係如下:

應用場景 模板類型(PiPTemplateType) 核心控制組(PiPControlGroup) 典型設備
視頻播放(影視、短視頻) VIDEO_PLAY 上一個/下一個、快進/後退 手機、平板、電視
視頻通話(一對一通話) VIDEO_CALL 麥克風開關、攝像頭開關、掛斷、靜音 手機、PC
在線會議(多人協作) VIDEO_MEETING 掛斷、靜音、攝像頭開關、麥克風開關 PC、平板、手機
視頻直播(電商直播、賽事直播) VIDEO_LIVE 播放/暫停、靜音 手機、平板、電視

1.3 開發前置準備

(1)環境配置

  • 開發工具:DevEco Studio 4.0+(需安裝HarmonyOS SDK API 11及以上)
  • 測試設備:HarmonyOS 3.1+真機或模擬器(建議使用API 12+版本以支持完整功能)
  • 權限説明:無需額外申請懸浮窗權限,系統自動適配(部分設備需在設置中開啓"應用畫中畫權限")

(2)核心模塊導入

// 核心模塊導入
import { 
  PiPWindow, PiPTemplateType, PiPControlGroup, PiPController,
  PiPState, PiPControlType, PiPControlStatus, PiPWindowInfo, PiPWindowSize
} from '@kit.ArkUI';
// 輔助模塊導入
import { BusinessError } from '@kit.BasicServicesKit';
import { XComponentController, XComponentType, UIContext, NodeController, BuilderNode } from '@kit.ArkUI';
import { Context, AbilityConstant } from '@kit.AbilityKit';
import { LocalStorage } from '@kit.ArkUI';

二、場景化開發實戰:四大核心場景完整案例

場景一:視頻播放場景(影視App)

場景需求

用户在觀看電影時,點擊Home鍵返回桌面或切換到其他應用,視頻自動縮小為畫中畫窗口繼續播放;支持在畫中畫窗口控制播放/暫停、快進/後退、切換視頻;返回原App時,畫中畫恢復為全屏播放。

實現步驟

1. 基礎配置與UI佈局

首先創建視頻播放頁面,包含視頻渲染容器(XComponent)、控制按鈕和畫中畫觸發按鈕:

@Entry
@Component
struct VideoPlayerPage {
  // 視頻播放相關狀態
  private videoUrl: string = 'https://example.com/movie.mp4';
  private currentTime: number = 0; // 當前播放進度(秒)
  private isPlaying: boolean = false;
  // PiP相關實例
  private xComponentController: XComponentController = new XComponentController();
  private pipController: PiPWindow.PiPController | undefined;
  private localStorage: LocalStorage = new LocalStorage({ 'playbackTime': 0 });
  // 頁面導航ID(用於從PiP恢復時定位頁面)
  private navId: string = 'video_player_page';
  
  build() {
    Column() {
      // 視頻渲染容器:通過XComponent實現硬件加速渲染
      XComponent({
        id: 'video_surface',
        type: XComponentType.SURFACE,
        controller: this.xComponentController
      })
      .width('100%')
      .height(300)
      .onLoad(() => {
        // 初始化視頻播放器,綁定XComponent的Surface
        this.initVideoPlayer(this.xComponentController.getSurfaceId());
      })
      
      // 播放控制欄
      Row() {
        Button('播放/暫停')
          .onClick(() => this.togglePlay())
        Button('開啓畫中畫')
          .marginLeft(20)
          .onClick(() => this.startPiP())
        Button('切換視頻')
          .marginLeft(20)
          .onClick(() => this.switchVideo('https://example.com/next-movie.mp4'))
      }
      .margin(20)
    }
    .padding(16)
    .width('100%')
  }
  
  // 初始化視頻播放器(實際開發中需結合媒體播放模塊)
  private initVideoPlayer(surfaceId: string) {
    console.info(`綁定視頻渲染Surface:${surfaceId}`);
    // 此處省略視頻播放器初始化邏輯,核心是將播放內容渲染到XComponent的Surface
  }
  
  private togglePlay() {
    this.isPlaying = !this.isPlaying;
    // 省略播放/暫停控制邏輯
  }
}
2. PiP配置與控制器創建

創建適配視頻播放場景的PiPConfiguration,包含模板類型、控制組、狀態同步等配置:

// 初始化PiP配置
private initPiPConfig(): PiPWindow.PiPConfiguration {
  return {
    // 上下文環境:從UIContext獲取宿主上下文
    context: this.getUIContext().getHostContext() as Context,
    // XComponent控制器:關聯視頻渲染容器
    componentController: this.xComponentController,
    // 導航ID:用於從PiP恢復時回到當前播放頁面
    navigationId: this.navId,
    // 模板類型:視頻播放
    templateType: PiPTemplateType.VIDEO_PLAY,
    // 視頻原始尺寸(影響PiP窗口比例)
    contentWidth: 1280,
    contentHeight: 720,
    // 控制組:快進/後退(與上一個/下一個互斥)
    controlGroups: [PiPWindow.VideoPlayControlGroup.FAST_FORWARD_BACKWARD],
    // LocalStorage:同步主窗口與PiP窗口的播放進度
    localStorage: this.localStorage,
    // 默認窗口大小:小窗(1=小窗,2=大窗,0=上次尺寸)
    defaultWindowSizeType: 1
  };
}

// 創建PiP控制器並啓動畫中畫
async startPiP() {
  // 1. 兼容性檢測
  if (!PiPWindow.isPiPEnabled()) {
    Toast.show({ message: '當前設備不支持畫中畫功能' });
    return;
  }
  
  // 2. 保存當前播放進度到LocalStorage
  this.localStorage.setOrCreate('playbackTime', this.currentTime);
  
  try {
    // 3. 創建PiP控制器
    const config = this.initPiPConfig();
    this.pipController = await PiPWindow.create(config);
    console.info('PiP控制器創建成功');
    
    // 4. 啓動畫中畫
    await this.pipController.startPiP();
    console.info('畫中畫啓動成功');
    
    // 5. 註冊監聽事件
    this.registerPiPListeners();
    
    // 6. 設置返回桌面時自動啓動PiP(可選)
    this.pipController.setAutoStartEnabled(true);
  } catch (err) {
    const error = err as BusinessError;
    console.error(`PiP啓動失敗:錯誤碼${error.code},消息${error.message}`);
    this.handlePiPError(error.code);
  }
}
3. 狀態監聽與業務聯動

畫中畫的核心價值在於與主應用的狀態同步,需監聽生命週期、控制欄操作、窗口尺寸變化三類事件:

private registerPiPListeners() {
  if (!this.pipController) return;
  
  // 1. 生命週期狀態監聽:處理啓動、停止、恢復等流轉
  this.pipController.on('stateChange', (state: PiPState, reason: string) => {
    switch (state) {
      case PiPState.STARTED:
        // PiP啓動成功:暫停主窗口視頻,避免音視頻衝突
        this.isPlaying = false;
        this.pauseVideo();
        console.info(`PiP啓動,原因:${reason}`);
        break;
      case PiPState.STOPPED:
        // PiP停止:恢復主窗口視頻播放(從LocalStorage讀取進度)
        this.currentTime = this.localStorage.get<number>('playbackTime') || 0;
        this.seekTo(this.currentTime);
        this.isPlaying = true;
        this.playVideo();
        console.info(`PiP停止,原因:${reason}`);
        // 移除監聽,釋放資源
        this.removePiPListeners();
        break;
      case PiPState.ABOUT_TO_RESTORE:
        // PiP即將恢復到主窗口:準備UI狀態
        this.setPageState('restoring');
        console.info(`PiP準備恢復,原因:${reason}`);
        break;
      case PiPState.ERROR:
        // 異常處理:提示用户並恢復播放
        Toast.show({ message: '畫中畫異常,已恢復原窗口播放' });
        this.resumeVideo();
        console.error(`PiP異常,原因:${reason}`);
        break;
    }
  });
  
  // 2. 控制欄操作監聽:響應播放/暫停、快進/後退
  this.pipController.on('controlEvent', (param) => {
    switch (param.controlType) {
      case PiPControlType.VIDEO_PLAY_PAUSE:
        if (param.status === PiPControlStatus.PLAY) {
          this.playVideo(); // 播放
          this.isPlaying = true;
          // 更新主窗口狀態(可選)
          this.updateMainWindowPlayState(true);
        } else {
          this.pauseVideo(); // 暫停
          this.isPlaying = false;
          this.updateMainWindowPlayState(false);
        }
        break;
      case PiPControlType.FAST_FORWARD:
        this.currentTime += 15; // 快進15秒
        this.seekTo(this.currentTime);
        this.localStorage.set('playbackTime', this.currentTime);
        break;
      case PiPControlType.FAST_BACKWARD:
        this.currentTime = Math.max(0, this.currentTime - 15); // 後退15秒(不小於0)
        this.seekTo(this.currentTime);
        this.localStorage.set('playbackTime', this.currentTime);
        break;
    }
  });
  
  // 3. 窗口尺寸變化監聽(API 15+):適配不同尺寸的視頻渲染
  this.pipController.on('pipWindowSizeChange', (size: PiPWindowSize) => {
    console.info(`PiP窗口變化:寬${size.width}px,高${size.height}px,縮放比${size.scale}`);
    // 調整視頻渲染比例,避免拉伸
    this.adjustVideoAspectRatio(size.width, size.height);
  });
}

// 移除監聽(避免內存泄漏)
private removePiPListeners() {
  if (!this.pipController) return;
  this.pipController.off('stateChange');
  this.pipController.off('controlEvent');
  this.pipController.off('pipWindowSizeChange');
}
4. 高級優化:自定義UI疊加

需求:在畫中畫窗口右上角顯示視頻時長和清晰度標識。通過customUIController實現自定義UI疊加:

// 1. 定義自定義UI構建器
@Builder
function CustomVideoOverlay(params: { duration: string, quality: string }) {
  Row() {
    Text(`${params.duration}`)
      .fontSize(12)
      .fontColor(Color.White)
      .backgroundColor(Color.Black.opacity(0.6))
      .padding(2)
      .borderRadius(2)
    
    Text(`${params.quality}`)
      .fontSize(12)
      .fontColor(Color.White)
      .backgroundColor(Color.Black.opacity(0.6))
      .padding(2)
      .borderRadius(2)
      .marginLeft(4)
  }
  .position({ right: 8, top: 8 })
}

// 2. 實現自定義NodeController
class VideoOverlayController extends NodeController {
  private overlayNode: BuilderNode<[{ duration: string, quality: string }]> | null = null;
  private params: { duration: string, quality: string };
  
  constructor(duration: string, quality: string) {
    super();
    this.params = { duration, quality };
  }
  
  makeNode(context: UIContext): FrameNode | null {
    this.overlayNode = new BuilderNode(context);
    this.overlayNode.build(wrapBuilder<[{ duration: string, quality: string }]>(CustomVideoOverlay), this.params);
    return this.overlayNode.getFrameNode();
  }
  
  // 更新自定義UI參數(如切換清晰度時)
  updateParams(newParams: { duration: string, quality: string }) {
    this.params = newParams;
    this.overlayNode?.update(newParams);
  }
}

// 3. 在PiP配置中添加自定義UI控制器
private initPiPConfig(): PiPWindow.PiPConfiguration {
  // 初始化自定義UI控制器
  const overlayController = new VideoOverlayController('01:32:45', '1080P');
  
  return {
    // ...其他配置
    customUIController: overlayController, // 添加自定義UI疊加
  };
}

場景二:視頻通話場景(辦公通訊App)

場景需求

用户在進行一對一視頻通話時,切換到郵件、文檔等應用查看內容,通話窗口縮小為畫中畫;支持在畫中畫窗口控制麥克風開關、攝像頭開關、靜音、掛斷;返回原App時恢復全屏通話狀態;通話結束時自動關閉畫中畫。

核心實現代碼

1. 通話場景PiP配置
@Component
struct VideoCallPage {
  private xComponentController: XComponentController = new XComponentController();
  private pipController: PiPWindow.PiPController | undefined;
  private isMicOpen: boolean = true; // 麥克風狀態
  private isCameraOpen: boolean = true; // 攝像頭狀態
  private isMuted: boolean = false; // 靜音狀態
  private callId: string = 'call_123456'; // 通話ID
  
  // 初始化通話場景PiP配置
  private initCallPiPConfig(): PiPWindow.PiPConfiguration {
    return {
      context: this.getUIContext().getHostContext() as Context,
      componentController: this.xComponentController,
      templateType: PiPTemplateType.VIDEO_CALL, // 通話模板
      contentWidth: 720,
      contentHeight: 1280, // 豎屏通話比例
      // 通話核心控制組
      controlGroups: [
        PiPWindow.VideoCallControlGroup.MICROPHONE_SWITCH,
        PiPWindow.VideoCallControlGroup.CAMERA_SWITCH,
        PiPWindow.VideoCallControlGroup.MUTE_SWITCH,
        PiPWindow.VideoCallControlGroup.HANG_UP_BUTTON
      ],
      defaultWindowSizeType: 1 // 小窗啓動
    };
  }
  
  // 啓動通話畫中畫
  async startCallPiP() {
    if (!PiPWindow.isPiPEnabled()) {
      Toast.show({ message: '當前設備不支持畫中畫通話' });
      return;
    }
    
    try {
      const config = this.initCallPiPConfig();
      this.pipController = await PiPWindow.create(config);
      await this.pipController.startPiP();
      this.registerCallPiPListeners();
    } catch (err) {
      const error = err as BusinessError;
      console.error(`通話PiP啓動失敗:${error.code} - ${error.message}`);
    }
  }
2. 通話控制事件處理
private registerCallPiPListeners() {
  if (!this.pipController) return;
  
  // 控制欄操作監聽
  this.pipController.on('controlEvent', (param) => {
    switch (param.controlType) {
      case PiPControlType.MICROPHONE_SWITCH:
        // 切換麥克風狀態
        this.isMicOpen = param.status === PiPControlStatus.OPEN;
        this.setMicrophoneState(this.isMicOpen); // 調用原生API控制麥克風
        break;
      case PiPControlType.CAMERA_SWITCH:
        // 切換攝像頭狀態
        this.isCameraOpen = param.status === PiPControlStatus.OPEN;
        this.setCameraState(this.isCameraOpen); // 調用原生API控制攝像頭
        break;
      case PiPControlType.MUTE_SWITCH:
        // 切換靜音狀態
        this.isMuted = param.status === PiPControlStatus.CLOSE;
        this.setMuteState(this.isMuted);
        break;
      case PiPControlType.HANG_UP_BUTTON:
        // 掛斷通話
        this.endCall();
        this.stopCallPiP();
        break;
    }
  });
  
  // 生命週期監聽
  this.pipController.on('stateChange', (state, reason) => {
    if (state === PiPState.STOPPED) {
      // PiP停止時,同步更新主窗口通話狀態
      this.syncCallState();
      this.removePiPListeners();
    }
  });
}

// 停止通話畫中畫
async stopCallPiP() {
  if (!this.pipController) return;
  try {
    await this.pipController.stopPiP();
  } catch (err) {
    const error = err as BusinessError;
    console.error(`通話PiP停止失敗:${error.code} - ${error.message}`);
  }
}

場景三:在線會議場景(協同辦公App)

場景需求

多人在線會議中,用户需要邊查看會議文檔邊參與討論,會議窗口縮小為畫中畫;支持靜音、關閉攝像頭、掛斷、打開麥克風等操作;會議主持人可強制關閉參會者的畫中畫(通過狀態同步);PC端支持調整畫中畫窗口大小。

關鍵實現要點

1. 會議模板配置與多實例同步
// 會議場景PiP配置
private initMeetingPiPConfig(): PiPWindow.PiPConfiguration {
  // 會議狀態存儲:用於多實例同步(如主持人控制)
  const meetingStorage = new LocalStorage({
    isHost: true,
    meetingStatus: 'ongoing'
  });
  
  return {
    context: this.getUIContext().getHostContext() as Context,
    componentController: this.xComponentController,
    templateType: PiPTemplateType.VIDEO_MEETING, // 會議模板
    contentWidth: 1920,
    contentHeight: 1080, // PC端會議比例
    controlGroups: [
      PiPWindow.VideoMeetingControlGroup.HANG_UP_BUTTON,
      PiPWindow.VideoMeetingControlGroup.MUTE_SWITCH,
      PiPWindow.VideoMeetingControlGroup.CAMERA_SWITCH,
      PiPWindow.VideoMeetingControlGroup.MICROPHONE_SWITCH
    ],
    localStorage: meetingStorage, // 同步會議狀態
    defaultWindowSizeType: 2 // PC端默認大窗
  };
}
2. 主持人控制邏輯(狀態同步)
// 主持人關閉參會者PiP
private closeAttendeePiP(attendeeId: string) {
  // 通過LocalStorage同步狀態
  this.meetingStorage.set('forceClosePiP', attendeeId);
  
  // 監聽參會者PiP狀態
  this.meetingStorage.on('change', (key) => {
    if (key === 'forceClosePiP' && this.attendeeId === this.meetingStorage.get<string>('forceClosePiP')) {
      this.stopMeetingPiP();
      Toast.show({ message: '主持人已關閉畫中畫模式' });
    }
  });
}

場景四:視頻直播場景(電商直播App)

場景需求

用户觀看電商直播時,可切換到商品詳情頁查看信息,直播窗口縮小為畫中畫;支持播放/暫停、靜音操作;畫中畫窗口顯示直播狀態(如"正在秒殺");返回直播頁面時恢復全屏。

核心實現代碼

@Component
struct LiveStreamingPage {
  private xComponentController: XComponentController = new XComponentController();
  private pipController: PiPWindow.PiPController | undefined;
  private liveStatus: string = '秒殺中'; // 直播狀態
  
  // 直播場景PiP配置
  private initLivePiPConfig(): PiPWindow.PiPConfiguration {
    // 自定義直播狀態UI
    const liveOverlayController = new LiveStatusOverlayController(this.liveStatus);
    
    return {
      context: this.getUIContext().getHostContext() as Context,
      componentController: this.xComponentController,
      templateType: PiPTemplateType.VIDEO_LIVE, // 直播模板
      contentWidth: 1280,
      contentHeight: 720,
      controlGroups: [
        PiPWindow.VideoLiveControlGroup.VIDEO_PLAY_PAUSE,
        PiPWindow.VideoLiveControlGroup.MUTE_SWITCH
      ],
      customUIController: liveOverlayController, // 直播狀態疊加
      defaultWindowSizeType: 1
    };
  }
  
  // 直播控制事件處理
  private registerLivePiPListeners() {
    if (!this.pipController) return;
    
    this.pipController.on('controlEvent', (param) => {
      switch (param.controlType) {
        case PiPControlType.VIDEO_PLAY_PAUSE:
          this.toggleLivePlay(param.status === PiPControlStatus.PLAY);
          break;
        case PiPControlType.MUTE_SWITCH:
          this.setLiveMute(param.status === PiPControlStatus.CLOSE);
          break;
      }
    });
    
    // 直播狀態更新(如從"秒殺中"改為"正常直播")
    this.updateLiveStatus = (newStatus: string) => {
      this.liveStatus = newStatus;
      this.liveOverlayController.updateParams({ status: newStatus });
    };
  }
}

三、進階技巧:性能優化與用户體驗提升

3.1 性能優化要點

  1. 資源釋放:所有監聽事件(stateChangecontrolEvent等)在PiP停止後必須通過off()移除,避免內存泄漏;
  2. 渲染優化:PiP啓動時暫停主窗口的視頻渲染和音頻播放,僅保留PiP窗口的媒體流;
  3. 尺寸適配:通過updateContentSize()方法同步媒體源尺寸變化,避免PiP窗口拉伸;
  4. 異步處理:所有PiP相關API(createstartPiPstopPiP等)均為異步操作,需通過Promise或async/await處理,避免阻塞主線程。

3.2 用户體驗優化技巧

  1. 自動啓動配置:通過setAutoStartEnabled(true)設置返回桌面時自動啓動PiP,但需先通過getPiPSettingSwitch()(API 20+)檢查系統開關狀態,避免配置失效;

    // 檢查系統畫中畫開關狀態(API 20+)
    async checkSystemPiPSwitch() {
      if (!this.pipController) return false;
      try {
     const isSwitchOpen = await this.pipController.getPiPSettingSwitch();
     return isSwitchOpen;
      } catch (err) {
     const error = err as BusinessError;
     console.error(`獲取系統PiP開關失敗:${error.code}`);
     return false;
      }
    }
  2. 狀態提示:PiP啓動、恢復、異常時通過Toast或通知提示用户,如"畫中畫已啓動,可拖動窗口調整位置";
  3. 窗口拖動:系統默認支持PiP窗口拖動,無需額外開發,避免在窗口邊緣添加遮擋元素;
  4. 恢復記憶:從PiP恢復到主窗口時,保留原播放進度、通話狀態、會議設置等,提升連貫性。

3.3 錯誤處理與兼容性適配

(1)常見錯誤碼處理

錯誤碼 含義 處理方案
401 參數錯誤 檢查contextcomponentController是否為空;驗證controlGroupstemplateType是否匹配;確保尺寸參數為正整數
801 設備不支持 提示用户當前設備不支持畫中畫功能,隱藏PiP入口
1300012 PiP狀態異常 調用stopPiP()重置狀態,重新創建控制器
1300013 創建窗口失敗 檢查XComponent配置是否正確;確保應用無懸浮窗權限限制

(2)跨API版本適配

  • API 11-12:基礎功能支持(創建、啓動、停止、基礎控制組),避免使用customUIControllerlocalStorage等高級特性;
  • API 15+:支持窗口尺寸監聽(pipWindowSizeChange)、PiPWindowInfo獲取,可實現更精細的尺寸適配;
  • API 20+:支持PC/2in1設備、getPiPSettingSwitch(),需適配橫屏場景的窗口比例。

四、實際開發避坑指南

  1. XComponent配置錯誤:XComponent的type必須設置為SURFACE,且controller必須與PiPConfiguration中的componentController一致,否則會導致內容無法渲染到PiP窗口;
  2. 控制組與模板不匹配:如視頻播放模板不能添加通話控制組(MICROPHONE_SWITCH),否則會導致控制欄不顯示,需嚴格按照場景-模板-控制組適配關係配置;
  3. 上下文獲取錯誤PiPConfiguration中的context必須通過getUIContext().getHostContext()獲取,不能直接使用AbilityContext,否則會導致權限異常;
  4. 內存泄漏:忘記移除監聽事件是最常見的內存泄漏原因,需在PiPState.STOPPED狀態或頁面銷燬時調用off()移除所有監聽;
  5. 音視頻衝突:PiP窗口和主窗口同時播放音頻會導致聲音重疊,需在PiP啓動時暫停主窗口音頻,停止時恢復。

總結

鴻蒙@ohos.PiPWindow模塊通過模板化設計、全生命週期管理、跨設備兼容等特性,為開發者提供了高效的畫中畫開發方案。無論是視頻播放、通話、會議還是直播場景,都能通過合理配置模板和控制組,快速實現核心功能,再結合自定義UI、狀態同步、性能優化等進階技巧,打造出貼合用户需求的優質體驗。在實際開發中,需重點關注場景與模板的適配、狀態同步、錯誤處理和跨版本兼容,同時遵循用户體驗最佳實踐,讓畫中畫功能真正成為提升應用競爭力的加分項。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.