• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2025 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16const TAG = 'AVInputCastPicker_component';
17
18/**
19 * menuItem device info
20 */
21interface AVInputCastPickerDeviceInfo {
22  deviceId: number | String,
23  deviceType: number,
24  deviceName: string,
25  deviceIconName: string,
26  isConnected: boolean,
27  selectedIconName: string,
28}
29
30/**
31 * Definition of av cast picker state.
32 */
33export enum AVCastPickerState {
34  /**
35   * The picker starts showing.
36   */
37  STATE_APPEARING,
38
39  /**
40   * The picker finishes presenting.
41   */
42  STATE_DISAPPEARING
43}
44
45@Component
46export struct AVInputCastPicker {
47  /**
48   * Custom builder from application.
49   */
50  @BuilderParam customPicker: (() => void);
51  /**
52   * Device list data.
53   */
54  @State deviceInfoList: Array<AVInputCastPickerDeviceInfo> = [];
55  /**
56   * Touch item index.
57   */
58  @State touchMenuItemIndex: number = -1;
59  /**
60   * Configuration color mode.
61   */
62  @State isDarkMode: boolean = false;
63  /**
64   * Mirrored or not.
65   */
66  @State isRTL: boolean = false;
67  /**
68   * Picker state change callback.
69   */
70  private onStateChange?: (state: AVCastPickerState) => void;
71  /**
72   * UIExtensionProxy.
73   */
74  private extensionProxy: UIExtensionProxy | null = null;
75  /**
76   * show menu start time.
77   */
78  private pickerClickTime: number = -1;
79  /**
80   * picker count on creation.
81   */
82  private pickerCountOnCreation: number = 0;
83  /**
84   * current picker count.
85   */
86  private static currentPickerCount: number = 0;
87
88  aboutToAppear(): void {
89    AVInputCastPicker.currentPickerCount += 1;
90    this.pickerCountOnCreation = AVInputCastPicker.currentPickerCount;
91  }
92
93  aboutToDisappear(): void {
94    AVInputCastPicker.currentPickerCount -= 1;
95  }
96
97  @Builder
98  iconBuilder(item: AVInputCastPickerDeviceInfo, isSelected: boolean): void {
99    SymbolGlyph(!isSelected ? $r(item.deviceIconName) : $r(item.selectedIconName))
100      .fontSize('24vp')
101      .fontColor((isSelected && !this.isDarkMode) ?
102        [$r('sys.color.comp_background_emphasize')] : [$r('sys.color.icon_primary')])
103      .renderingStrategy(SymbolRenderingStrategy.SINGLE)
104  }
105
106  @Builder
107  textBuilder(item: AVInputCastPickerDeviceInfo) {
108    Text(item.deviceName)
109      .fontSize($r('sys.float.ohos_id_text_size_body2'))
110      .fontColor(item.isConnected ? (!this.isDarkMode ? $r('sys.color.font_emphasize') : $r('sys.color.font_primary')) :
111        (!this.isDarkMode ? $r('sys.color.font_primary') : $r('sys.color.font_secondary')))
112      .width(254)
113      .padding({
114        left: 8,
115        top: 11,
116        right: 8,
117        bottom: 11
118      })
119      .textOverflow({ overflow: TextOverflow.Ellipsis })
120      .maxLines(2)
121      .wordBreak(WordBreak.BREAK_WORD)
122      .direction(this.isRTL ? Direction.Rtl : Direction.Ltr)
123  }
124
125  @Builder
126  deviceMenu() {
127    Column() {
128      ForEach(this.deviceInfoList, (item: AVInputCastPickerDeviceInfo, index) => {
129        Flex({
130          direction: FlexDirection.Column,
131          justifyContent: FlexAlign.SpaceBetween,
132          alignItems: ItemAlign.End
133        }) {
134          Flex({
135            direction: FlexDirection.Row,
136            justifyContent: FlexAlign.SpaceBetween,
137            alignItems: ItemAlign.Center
138          }) {
139            Row() {
140              this.iconBuilder(item, false)
141
142              this.textBuilder(item)
143            }
144            .alignItems(VerticalAlign.Center)
145
146            if (item.isConnected && item.selectedIconName !== null && item.selectedIconName !== undefined) {
147              Row() {
148                this.iconBuilder(item, true)
149              }
150              .alignItems(VerticalAlign.Center)
151            }
152          }
153          .constraintSize({ minHeight: 40 })
154          .padding({ left: 12, right: 12 })
155          .onTouch((event) => {
156            if (event.type === TouchType.Down) {
157              this.touchMenuItemIndex = index;
158            } else if (event.type === TouchType.Up) {
159              this.touchMenuItemIndex = -1;
160            }
161          })
162          .backgroundColor(this.touchMenuItemIndex === index ? $r('sys.color.interactive_click') : '#00FFFFFF')
163          .borderRadius(this.touchMenuItemIndex === index ? $r('sys.float.corner_radius_level2') : 0)
164
165          if (index !== this.deviceInfoList.length - 1) {
166            Row()
167              .width('100%')
168              .height(2)
169          }
170        }
171        .width('100%')
172        .onClick(() => {
173          console.info(TAG, ` item click ${item.isConnected}`)
174          if (this.extensionProxy !== null && !item.isConnected) {
175            this.extensionProxy.send({ 'selectedDeviceInfo': item })
176          }
177        })
178      })
179    }
180    .width(326)
181    .borderRadius(8)
182  }
183
184  @Builder
185  private buildDefaultPicker(isCustomPicker: boolean) {
186    UIExtensionComponent({
187      abilityName: 'AVInputCastPickerAbility',
188      bundleName: 'com.hmos.mediacontroller',
189      parameters: {
190        'ability.want.params.uiExtensionType': 'sysPicker/mediaControl',
191        'isCustomPicker': isCustomPicker,
192        'currentPickerCount': this.pickerCountOnCreation,
193      }
194    })
195      .size({ width: '100%', height: '100%' })
196      .bindMenu(this.deviceMenu(), {
197        onDisappear: () => {
198          this.touchMenuItemIndex = -1;
199          if (this.onStateChange !== null && this.onStateChange !== undefined) {
200            this.onStateChange(AVCastPickerState.STATE_DISAPPEARING);
201          }
202        },
203        onAppear: () => {
204          if (this.onStateChange !== null && this.onStateChange !== undefined) {
205            this.onStateChange(AVCastPickerState.STATE_APPEARING);
206          }
207          if (this.extensionProxy !== null && this.pickerClickTime !== -1) {
208            this.extensionProxy.send({ 'timeCost': new Date().getTime() - this.pickerClickTime });
209            this.pickerClickTime = -1;
210          }
211        }
212      })
213      .onClick(() => {
214        this.pickerClickTime = new Date().getTime();
215      })
216      .onRemoteReady((proxy: UIExtensionProxy) => {
217        console.info(TAG, 'onRemoteReady');
218        this.extensionProxy = proxy;
219      })
220      .onReceive((data) => {
221        if (JSON.stringify(data['deviceInfoList']) !== undefined) {
222          this.deviceInfoList = JSON.parse(JSON.stringify(data['deviceInfoList']));
223        }
224
225        if (JSON.stringify(data['isDarkMode']) !== undefined) {
226          console.info(TAG, `isDarkMode : ${JSON.stringify(data['isDarkMode'])}`);
227          this.isDarkMode = data['isDarkMode'] as boolean;
228        }
229
230        if (JSON.stringify(data['isRTL']) !== undefined) {
231          console.info(TAG, `isRTL : ${JSON.stringify(data['isRTL'])}`);
232          this.isRTL = data['isRTL'] as boolean;
233        }
234      })
235      .onTerminated((info) => {
236        console.info(TAG, ` onTerminated code: ${info?.code}`);
237      })
238      .onError((error) => {
239        console.info(TAG, ` onError code: ${error?.code} message: ${error?.message}`);
240      })
241  }
242
243  @Builder
244  private buildCustomPicker() {
245    Stack({ alignContent: Alignment.Center }) {
246      Column() {
247        this.customPicker();
248      }
249      .alignItems(HorizontalAlign.Center)
250      .justifyContent(FlexAlign.Center)
251      .size({ width: '100%', height: '100%' })
252      .zIndex(0)
253
254      Column() {
255        this.buildDefaultPicker(true);
256      }
257      .alignItems(HorizontalAlign.Center)
258      .justifyContent(FlexAlign.Center)
259      .size({ width: '100%', height: '100%' })
260      .zIndex(1)
261    }
262    .size({ width: '100%', height: '100%' })
263  }
264
265  build() {
266    Column() {
267      if (this.customPicker === undefined) {
268        this.buildDefaultPicker(false);
269      } else {
270        this.buildCustomPicker();
271      }
272    }
273    .size({ width: '100%', height: '100%' })
274  }
275}