1/* 2 * Copyright (c) 2023-2023 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 16import { KeyCode } from '@ohos.multimodalInput.keyCode' 17 18export declare type TabTitleBarMenuItem = { 19 value: ResourceStr 20 isEnabled: boolean 21 action?: () => void 22} 23 24export declare type TabTitleBarTabItem = { 25 title: ResourceStr 26 icon?: ResourceStr 27} 28 29const PUBLIC_MORE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAYAAABS3GwHAAAAIGNIUk0AAHomAACAhAAA+' + 30 'gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAEZ0FNQQAAsY58+1GTAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAO' + 31 'xAAADsQBlSsOGwAABEZJREFUeNrt3D1rFFEUBuAxhmAhFlYpUohYiYWFRcAmKAhWK2pjo1iKf8BCMIKFf8BarCyMhVj4VZhGSKEg2FqJyCKWIhY' + 32 'WnstMINgYsh+cmfs88BICydxw7jmzu2HvNg0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBN+3r6dx+LXIqsRpa7FF8j48hm5Fn3Peo9mAEYRd' + 33 'YjJ3f582Vj7nZfUe/eDsCRyMPI2h5/fyNyI/JDT6v3Tvt7sBllE15ETkxwjeORi5G3ke/6W737MgBnI68jh6ZwrcORq5HnhkC9+zAA5YXXy8jBK' + 34 'V5zKXIu8jjyS7+rd+YBeNVtyrSVO9PRyBM9r94LSTfjWuTUDK9/eYIXeENUbb0zDsBi5PYc1rmj79U74wCszuih+F/ljrSi/+uud8YBGA10rayq' + 35 'rnfGAVgb6FpZVV3vjAOwPNC1sqq63hkHYGWga2VVdb0XKt/8Rf1fd70zDsB4jmt5u3Tl9a59AMb6v+56ZxyArYGulVXV9c44ABtzXOup/q+73hk' + 36 'H4N2cHio/Rj7r/7rrnXEAfkfuz2Gddb2v3ln/DfpgxneLzaY9xE3l9c46AH8iVyI/Z3Dt8nB/Xc+rd5H5QMy3yJemPVs6zY0edc9HUe/0Z4I/dQ' + 37 '/N5Vjd0oTXKp9QcKFpD2qj3r0YgO1NeRM507TH6/bifeR85IMeV++d+vTBWOV9JDcjt5rdv6uw3M3uRR7pa/Xu+wBsOxA53bTnTP/3UX1b3fNQ1' + 38 'BsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqyr6d/97HIpchqZLlL8TUyjmxGnnXfo96DGYBRZD1ycpc/Xzbm' + 39 'bvcV9e7tAByJPIys7fH3NyI3Ij/0tHrvtL8Hm1E24UXkxATXOB65GHkb+a6/1bsvA3A28jpyaArXOhy5GnluCNS7DwNQXni9jByc4jWXIucijyO' + 40 '/9Lt6Zx6AV92mTFu5Mx2NPNHz6r2QdDOuRU7N8PqXJ3iBN0TV1jvjACxGbs9hnTv6Xr0zDsDqjB6K/1XuSCv6v+56ZxyA0UDXyqrqemccgLWBrp' + 41 'VV1fXOOADLA10rq6rrnXEAVga6VlZV13uh8s1f1P911zvjAIznuJa3S1de79oHYKz/6653xgHYGuhaWVVd74wDsDHHtZ7q/7rrnXEA3s3pofJj5' + 42 'LP+r7veGQfgd+T+HNZZ1/vqnfXfoA9mfLfYbNpD3FRe76wD8CdyJfJzBtcuD/fX9bx6F5kPxHyLfGnas6XT3OhR93wU9U5/JvhT99BcjtUtTXit' + 43 '8gkFF5r2oDbq3YsB2N6UN5EzTXu8bi/eR85HPuhx9d6pTx+MVd5HcjNyq9n9uwrL3exe5JG+Vu++D8C2A5HTTXvO9H8f1bfVPQ9FvQEAAAAAAAA' + 44 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAgCn7C9HjBtwWfXpKAAAAAElFTkSuQmCC' 45 46@Component 47export struct TabTitleBar { 48 tabItems: Array<TabTitleBarTabItem> 49 menuItems: Array<TabTitleBarMenuItem> 50 @BuilderParam swiperContent: () => void 51 52 @State tabWidth: number = 0 53 @State currentIndex: number = 0 54 55 static readonly totalHeight = 56 56 static readonly correctionOffset = -40.0 57 static readonly gradientMaskWidth = 24 58 private menuSectionWidth = 0 59 60 private scroller: Scroller = new Scroller() 61 private swiperController: SwiperController = new SwiperController() 62 private settings: RenderingContextSettings = new RenderingContextSettings(true) 63 private leftContext2D: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings) 64 private rightContext2D: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings) 65 66 @Builder GradientMask(context2D: CanvasRenderingContext2D, x0: number, y0: number, x1: number, y1: number) { 67 Column() { 68 Canvas(context2D) 69 .width(TabTitleBar.gradientMaskWidth) 70 .height(TabTitleBar.totalHeight) 71 .onReady(() => { 72 var grad = context2D.createLinearGradient(x0, y0, x1, y1) 73 grad.addColorStop(0.0, '#ffffffff') 74 grad.addColorStop(1, '#00ffffff') 75 context2D.fillStyle = grad 76 context2D.fillRect(0, 0, TabTitleBar.gradientMaskWidth, TabTitleBar.totalHeight) 77 }) 78 } 79 .width(TabTitleBar.gradientMaskWidth) 80 .height(TabTitleBar.totalHeight) 81 } 82 83 build() { 84 Column() { 85 Flex({ 86 justifyContent: FlexAlign.SpaceBetween, 87 alignItems: ItemAlign.Stretch 88 }) { 89 Stack({ alignContent: Alignment.End }) { 90 Stack({ alignContent: Alignment.Start }) { 91 Column() { 92 List({ initialIndex: 0, scroller: this.scroller, space: 0 }) { 93 ForEach(this.tabItems, (tabItem, index?: number) => { 94 ListItem() { 95 TabContentItem({ 96 item: tabItem, 97 index: index, 98 maxIndex: this.tabItems.length - 1, 99 currentIndex: this.currentIndex, 100 onCustomClick: () => this.currentIndex = index 101 }) 102 } 103 }) 104 } 105 .width('100%') 106 .height(TabTitleBar.totalHeight) 107 .constraintSize({ maxWidth: this.tabWidth }) 108 .edgeEffect(EdgeEffect.Spring) 109 .listDirection(Axis.Horizontal) 110 .scrollBar(BarState.Off) 111 } 112 this.GradientMask(this.leftContext2D, 0, TabTitleBar.totalHeight / 2, 113 TabTitleBar.gradientMaskWidth, TabTitleBar.totalHeight / 2) 114 } 115 this.GradientMask(this.rightContext2D, TabTitleBar.gradientMaskWidth, 116 TabTitleBar.totalHeight / 2, 0, TabTitleBar.totalHeight / 2) 117 } 118 119 if (this.menuItems !== undefined && this.menuItems.length > 0) { 120 CollapsibleMenuSection({ menuItems: this.menuItems }) 121 .height(TabTitleBar.totalHeight) 122 .onAreaChange((_oldValue: Area, newValue: Area) => { 123 this.menuSectionWidth = Number(newValue.width) 124 }) 125 } 126 } 127 .backgroundColor($r('sys.color.ohos_id_color_background')) 128 .margin({ right: $r('sys.float.ohos_id_max_padding_end') }) 129 .onAreaChange((_oldValue: Area, newValue: Area) => { 130 this.tabWidth = Number(newValue.width) - this.menuSectionWidth 131 }) 132 133 Column() { 134 Swiper(this.swiperController) { this.swiperContent() } 135 .index(this.currentIndex) 136 .itemSpace(0) 137 .indicator(false) 138 .width('100%') 139 .height('100%') 140 .curve(Curve.Friction) 141 .onChange((index) => { 142 this.currentIndex = index 143 this.scroller.scrollToIndex(this.currentIndex) 144 this.scroller.scrollBy(TabTitleBar.correctionOffset, 0) 145 }) 146 .onAppear(() => { 147 this.scroller.scrollToIndex(this.currentIndex) 148 this.scroller.scrollBy(TabTitleBar.correctionOffset, 0) 149 }) 150 } 151 } 152 } 153} 154 155@Component 156struct CollapsibleMenuSection { 157 menuItems: Array<TabTitleBarMenuItem> 158 159 static readonly maxCountOfVisibleItems = 1 160 private static readonly focusPadding = 4 161 private static readonly marginsNum = 2 162 163 @State isPopupShown: boolean = false 164 165 @State isMoreIconOnFocus: boolean = false 166 @State isMoreIconOnHover: boolean = false 167 @State isMoreIconOnClick: boolean = false 168 169 getMoreIconFgColor() { 170 return this.isMoreIconOnClick 171 ? $r('sys.color.ohos_id_color_titlebar_icon_pressed') 172 : $r('sys.color.ohos_id_color_titlebar_icon') 173 } 174 175 getMoreIconBgColor() { 176 if (this.isMoreIconOnClick) { 177 return $r('sys.color.ohos_id_color_click_effect') 178 } else if (this.isMoreIconOnHover) { 179 return $r('sys.color.ohos_id_color_hover') 180 } else { 181 return Color.Transparent 182 } 183 } 184 185 build() { 186 Column() { 187 Row() { 188 if (this.menuItems.length <= CollapsibleMenuSection.maxCountOfVisibleItems) { 189 ForEach(this.menuItems, (item) => { 190 ImageMenuItem({ item: item }) 191 }) 192 } else { 193 ForEach(this.menuItems.slice(0, CollapsibleMenuSection.maxCountOfVisibleItems - 1), (item) => { 194 ImageMenuItem({ item: item }) 195 }) 196 197 Row() { 198 Image(PUBLIC_MORE) 199 .width(ImageMenuItem.imageSize) 200 .height(ImageMenuItem.imageSize) 201 .focusable(true) 202 } 203 .width(ImageMenuItem.imageHotZoneWidth) 204 .height(ImageMenuItem.imageHotZoneWidth) 205 .borderRadius(ImageMenuItem.buttonBorderRadius) 206 .foregroundColor(this.getMoreIconFgColor()) 207 .backgroundColor(this.getMoreIconBgColor()) 208 .justifyContent(FlexAlign.Center) 209 .border(this.isMoreIconOnFocus ? 210 { width: ImageMenuItem.focusBorderWidth, 211 color: $r('sys.color.ohos_id_color_emphasize'), 212 style: BorderStyle.Solid 213 } : { width: 0 }) 214 .onFocus(() => this.isMoreIconOnFocus = true) 215 .onBlur(() => this.isMoreIconOnFocus = false) 216 .onHover((isOn) => this.isMoreIconOnHover = isOn) 217 .onKeyEvent((event) => { 218 if (event.keyCode !== KeyCode.KEYCODE_ENTER && event.keyCode !== KeyCode.KEYCODE_SPACE) { 219 return 220 } 221 if (event.type === KeyType.Down) { 222 this.isMoreIconOnClick = true 223 } 224 if (event.type === KeyType.Up) { 225 this.isMoreIconOnClick = false 226 } 227 }) 228 .onTouch((event) => { 229 if (event.type === TouchType.Down) { 230 this.isMoreIconOnClick = true 231 } 232 if (event.type === TouchType.Up) { 233 this.isMoreIconOnClick = false 234 } 235 }) 236 .onClick(() => this.isPopupShown = true) 237 .bindPopup(this.isPopupShown, { 238 builder: this.popupBuilder, 239 placement: Placement.Bottom, 240 popupColor: Color.White, 241 enableArrow: false, 242 onStateChange: (e) => this.isPopupShown = e.isVisible 243 }) 244 } 245 } 246 } 247 .height('100%') 248 // .margin({ right: $r('sys.float.ohos_id_default_padding_end') }) 249 .justifyContent(FlexAlign.Center) 250 } 251 252 @Builder popupBuilder() { 253 Column() { 254 ForEach(this.menuItems.slice(CollapsibleMenuSection.maxCountOfVisibleItems - 1, this.menuItems.length), (item, _index?) => { 255 ImageMenuItem({ item: item }) 256 }) 257 } 258 .width(ImageMenuItem.imageHotZoneWidth + CollapsibleMenuSection.focusPadding * CollapsibleMenuSection.marginsNum) 259 .margin({ top: CollapsibleMenuSection.focusPadding, bottom: CollapsibleMenuSection.focusPadding }) 260 } 261} 262 263@Component 264struct TabContentItem { 265 item: TabTitleBarTabItem 266 index: number 267 maxIndex: number 268 onCustomClick?: () => void 269 270 @Prop currentIndex: number 271 272 @State isOnFocus: boolean = false 273 @State isOnHover: boolean = false 274 @State isOnClick: boolean = false 275 276 static readonly imageSize = 24 277 static readonly imageHotZoneWidth = 48 278 static readonly imageMagnificationFactor = 1.4 279 static readonly buttonBorderRadius = 8 280 static readonly focusBorderWidth = 2 281 282 getBgColor() { 283 if (this.isOnClick) { 284 return $r('sys.color.ohos_id_color_click_effect') 285 } else if (this.isOnHover) { 286 return $r('sys.color.ohos_id_color_hover') 287 } else { 288 return Color.Transparent 289 } 290 } 291 292 build() { 293 Row() { 294 Column() { 295 if (this.item.icon === undefined) { 296 Text(this.item.title) 297 .fontSize(this.index === this.currentIndex 298 ? $r('sys.float.ohos_id_text_size_headline7') 299 : $r('sys.float.ohos_id_text_size_headline9')) 300 .fontColor(this.index === this.currentIndex 301 ? $r('sys.color.ohos_id_color_titlebar_text') 302 : $r('sys.color.ohos_id_color_titlebar_text_off')) 303 .fontWeight(FontWeight.Medium) 304 .focusable(true) 305 .padding({ top: this.index === this.currentIndex ? 6 : 10, left: 8, bottom: 2, right: 8 }) 306 .onFocus(() => this.isOnFocus = true) 307 .onBlur(() => this.isOnFocus = false) 308 .onHover((isOn) => this.isOnHover = isOn) 309 .onKeyEvent((event) => { 310 if (event.keyCode !== KeyCode.KEYCODE_ENTER && event.keyCode !== KeyCode.KEYCODE_SPACE) { 311 return 312 } 313 if (event.type === KeyType.Down) { 314 this.isOnClick = true 315 } 316 if (event.type === KeyType.Up) { 317 this.isOnClick = false 318 } 319 }) 320 .onTouch((event) => { 321 if (event.type === TouchType.Down) { 322 this.isOnClick = true 323 } 324 if (event.type === TouchType.Up) { 325 this.isOnClick = false 326 } 327 }) 328 .onClick(() => this.onCustomClick && this.onCustomClick()) 329 } else { 330 Row() { 331 Image(this.item.icon) 332 .alt(this.item.title) 333 .height(TabContentItem.imageSize) 334 .focusable(true) 335 .scale({ 336 x: this.index === this.currentIndex ? TabContentItem.imageMagnificationFactor : 1, 337 y: this.index === this.currentIndex ? TabContentItem.imageMagnificationFactor : 1 338 }) 339 } 340 .width(TabContentItem.imageHotZoneWidth) 341 .height(TabContentItem.imageHotZoneWidth) 342 .justifyContent(FlexAlign.Center) 343 .onFocus(() => this.isOnFocus = true) 344 .onBlur(() => this.isOnFocus = false) 345 .onHover((isOn) => this.isOnHover = isOn) 346 .onKeyEvent((event) => { 347 if (event.keyCode !== KeyCode.KEYCODE_ENTER && event.keyCode !== KeyCode.KEYCODE_SPACE) { 348 return 349 } 350 if (event.type === KeyType.Down) { 351 this.isOnClick = true 352 } 353 if (event.type === KeyType.Up) { 354 this.isOnClick = false 355 } 356 }) 357 .onTouch((event) => { 358 if (event.type === TouchType.Down) { 359 this.isOnClick = true 360 } 361 if (event.type === TouchType.Up) { 362 this.isOnClick = false 363 } 364 }) 365 .onClick(() => this.onCustomClick && this.onCustomClick()) 366 } 367 } 368 .justifyContent(FlexAlign.Center) 369 } 370 .height(TabTitleBar.totalHeight) 371 .alignItems(VerticalAlign.Center) 372 .justifyContent(FlexAlign.Center) 373 .margin({ 374 left: this.index === 0 ? 16 : 0, 375 right: this.index === this.maxIndex ? 12 : 0 376 }) // sys.float.ohos_id_max_padding_start - 8 377 .borderRadius(TabContentItem.buttonBorderRadius) 378 .backgroundColor(this.getBgColor()) 379 .border(this.isOnFocus ? 380 { width: TabContentItem.focusBorderWidth, 381 color: $r('sys.color.ohos_id_color_emphasize'), 382 style: BorderStyle.Solid 383 } : { width: 0 }) 384 } 385} 386 387@Component 388struct ImageMenuItem { 389 item: TabTitleBarMenuItem 390 391 static readonly imageSize = 24 392 static readonly imageHotZoneWidth = 48 393 static readonly buttonBorderRadius = 8 394 static readonly focusBorderWidth = 2 395 static readonly disabledImageOpacity = 0.4 396 397 @State isOnFocus: boolean = false 398 @State isOnHover: boolean = false 399 @State isOnClick: boolean = false 400 401 getFgColor() { 402 return this.isOnClick 403 ? $r('sys.color.ohos_id_color_titlebar_icon_pressed') 404 : $r('sys.color.ohos_id_color_titlebar_icon') 405 } 406 407 getBgColor() { 408 if (this.isOnClick) { 409 return $r('sys.color.ohos_id_color_click_effect') 410 } else if (this.isOnHover) { 411 return $r('sys.color.ohos_id_color_hover') 412 } else { 413 return Color.Transparent 414 } 415 } 416 417 build() { 418 Row() { 419 Image(this.item.value) 420 .width(ImageMenuItem.imageSize) 421 .height(ImageMenuItem.imageSize) 422 .focusable(this.item.isEnabled) 423 } 424 .width(ImageMenuItem.imageHotZoneWidth) 425 .height(ImageMenuItem.imageHotZoneWidth) 426 .borderRadius(ImageMenuItem.buttonBorderRadius) 427 .foregroundColor(this.getFgColor()) 428 .backgroundColor(this.getBgColor()) 429 .justifyContent(FlexAlign.Center) 430 .opacity(this.item.isEnabled ? 1 : ImageMenuItem.disabledImageOpacity) 431 .border(this.isOnFocus ? 432 { width: ImageMenuItem.focusBorderWidth, 433 color: $r('sys.color.ohos_id_color_emphasize'), 434 style: BorderStyle.Solid 435 } : { width: 0 }) 436 .onFocus(() => { 437 if (!this.item.isEnabled) { 438 return 439 } 440 this.isOnFocus = true 441 }) 442 .onBlur(() => this.isOnFocus = false) 443 .onHover((isOn) => { 444 if (!this.item.isEnabled) { 445 return 446 } 447 this.isOnHover = isOn 448 }) 449 .onKeyEvent((event) => { 450 if (!this.item.isEnabled) { 451 return 452 } 453 if (event.keyCode !== KeyCode.KEYCODE_ENTER && event.keyCode !== KeyCode.KEYCODE_SPACE) { 454 return 455 } 456 if (event.type === KeyType.Down) { 457 this.isOnClick = true 458 } 459 if (event.type === KeyType.Up) { 460 this.isOnClick = false 461 } 462 }) 463 .onTouch((event) => { 464 if (!this.item.isEnabled) { 465 return 466 } 467 if (event.type === TouchType.Down) { 468 this.isOnClick = true 469 } 470 if (event.type === TouchType.Up) { 471 this.isOnClick = false 472 } 473 }) 474 .onClick(() => this.item.isEnabled && this.item.action && this.item.action()) 475 } 476} 477 478export default { TabTitleBar }