DEV Community

Cover image for player controller, layout rounded corner nested images, input space background color filling, creating rendering nodes
kouwei qing
kouwei qing

Posted on

player controller, layout rounded corner nested images, input space background color filling, creating rendering nodes

【Daily HarmonyOS Next Knowledge】TAB effects, player controller, layout rounded corner nested images, input space background color filling, creating rendering nodes

1. HarmonyOS Tabs Official Example 9

It supports the underline animation moving to the clicked tab under BarMode.Fixed. However, does it not support the underline animation moving interaction under BarMode.Scrollable mode?

import curves from '@ohos.curves';
import display from '@ohos.display';
import { BusinessError } from '@ohos.base';
import componentUtils from '@ohos.ArkUI.componentUtils';


class MyDataSource implements IDataSource {
  private list: number[] = []

  constructor(list: number[]) {
    this.list = list
  }

  totalCount(): number {
    return this.list.length
  }

  getData(index: number): number {
    return this.list[index]
  }

  registerDataChangeListener(listener: DataChangeListener): void {
  }

  unregisterDataChangeListener() {
  }
}

@Entry
@Component
struct swiperTab {
  private displayInfo: display.Display | null = null;
  private controller: TabsController = new TabsController()
  private data: MyDataSource = new MyDataSource([]);
  private initialTabMargin: number = 5; // Initial tabbar margin
  private animationDuration: number = 300; // Animation duration
  private animationCurve: ICurve = curves.interpolatingSpring(7, 1, 328, 34); // Animation curve
  @State currentIndex: number = 0; // Current index of the content area
  @State tabsWidth: number = 0; // vp Content area width
  @State indicatorWidth: number = 0; // vp Tab width
  @State indicatorMarginLeft: number = 5; // vp Current tab margin from the left
  @State indicatorIndex: number = 0; // Current tab index
  @State nextIndicatorIndex: number = 0; // Next target tab index
  @State swipeRatio: number = 0; // Determine if page flipping occurs. When the page slides more than half, the tabBar switches to the next page.
  private scroller: Scroller = new Scroller();
  private arr: string[] = ['关注', '推荐', '热点', '上海', '视频', '新时代', '新歌', '新碟', '新片'];
  private textLength: number[] = [2, 2, 2, 2, 2, 3, 2, 2, 2] // Control the width of the parent container where the tab is located to avoid calculation errors in the underline target position

  aboutToAppear(): void {
    this.displayInfo = display.getDefaultDisplaySync(); // Get the screen instance
    let list: number[] = [];
    for (let i = 1; i <= this.arr.length; i++) {
      list.push(i);
    }
    this.data = new MyDataSource(list)
  }

  // Get the screen width in vp
  private getDisplayWidth(): number {
    return this.displayInfo != null ? px2vp(this.displayInfo.width) : 0;
  }

  // Get component size, position, translation, scaling, rotation, and affine matrix attribute information.
  private getTextInfo(index: number): Record<string, number> {
    let modePosition: componentUtils.ComponentInfo = componentUtils.getRectangleById(index.toString());
    try {
      return { 'left': px2vp(modePosition.windowOffset.x), 'width': px2vp(modePosition.size.width) }
    } catch (error) {
      return { 'left': 0, 'width': 0 }
    }
  }

  // Current underline animation
  private getCurrentIndicatorInfo(index: number, event: SwiperAnimationEvent): Record<string, number> {
    let nextIndex = index;
    // Swipe range limit. Swiper cannot loop, and Scroll remains non-looping
    if (index > 0 && event.currentOffset > 0) {
      nextIndex--; // Swipe left
    } else if (index < this.data.totalCount() - 1 && event.currentOffset < 0) {
      nextIndex++; // Swipe right
    }
    this.nextIndicatorIndex = nextIndex;
    // Get the attribute information of the current tabbar
    let indexInfo = this.getTextInfo(index);
    // Get the attribute information of the target tabbar
    let nextIndexInfo = this.getTextInfo(nextIndex);
    // Switch the page when the swiped page exceeds half
    this.swipeRatio = Math.abs(event.currentOffset / this.tabsWidth);
    let currentIndex = this.swipeRatio > 0.5 ? nextIndex : index; // When the page slides more than half, the tabBar switches to the next page.
    let currentLeft = indexInfo.left + (nextIndexInfo.left - indexInfo.left) * this.swipeRatio;
    let currentWidth = indexInfo.width + (nextIndexInfo.width - indexInfo.width) * this.swipeRatio;
    this.indicatorIndex = currentIndex;
    return { 'index': currentIndex, 'left': currentLeft, 'width': currentWidth };
  }

  private scrollIntoView(currentIndex: number): void {
    const indexInfo = this.getTextInfo(currentIndex);
    let tabPositionLeft = indexInfo.left;
    let tabWidth = indexInfo.width;
    // Get the screen width in vp
    const screenWidth = this.getDisplayWidth();
    const currentOffsetX: number = this.scroller.currentOffset().xOffset; // Current scrolling offset
    this.scroller.scrollTo({
      // Position the tabbar in the center when it can be scrolled
      xOffset: currentOffsetX + tabPositionLeft - screenWidth / 2 + tabWidth / 2,
      yOffset: 0,
      animation: {
        duration: this.animationDuration,
        curve: this.animationCurve, // Animation curve
      }
    });
    this.underlineScrollAuto(this.animationDuration, currentIndex);
  }

  private startAnimateTo(duration: number, marginLeft: number, width: number): void {
    animateTo({
      duration: duration, // Animation duration
      curve: this.animationCurve, // Animation curve
      onFinish: () => {
        console.info('play end')
      }
    }, () => {
      this.indicatorMarginLeft = marginLeft;
      this.indicatorWidth = width;
    })
  }

  // Underline animation
  private underlineScrollAuto(duration: number, index: number): void {
    let indexInfo = this.getTextInfo(index);
    this.startAnimateTo(duration, indexInfo.left, indexInfo.width);
  }

  getStringFromResource(source: Resource): string {
    try {
      getContext(this).resourceManager.getStringSync(source.id);
      let str = getContext(this).resourceManager.getStringSync(source.id);
      return str
    } catch (error) {
      let code = (error as BusinessError).code;
      let message = (error as BusinessError).message;
      console.error(`getStringSync failed, error code: ${code}, message: ${message}.`);
      return ''
    }
  }

  build() {
    Column() {
      // tabbar
      Row() {
        Column() {
          Scroll() {
            Column() {
              Scroll(this.scroller) {
                Row() {
                  ForEach(this.arr, (item: string, index: number) => {
                    Column() {
                      Text(item)
                        .fontSize(16)
                        .borderRadius(5)
                        .fontColor(this.indicatorIndex === index ? Color.Red : Color.Black)
                        .fontWeight(this.indicatorIndex === index ? FontWeight.Bold : FontWeight.Normal)
                        .margin({ left: this.initialTabMargin, right: this.initialTabMargin })
                        .id(index.toString())
                        .onAreaChange((oldValue: Area, newValue: Area) => {
                          if (this.indicatorIndex === index &&
                            (this.indicatorMarginLeft === 0 || this.indicatorWidth === 0)) {
                            if (newValue.globalPosition.x != undefined) {
                              let positionX = Number.parseFloat(newValue.globalPosition.x.toString());
                              this.indicatorMarginLeft = Number.isNaN(positionX) ? 0 : positionX;
                            }
                            let width = Number.parseFloat(newValue.width.toString());
                            this.indicatorWidth = Number.isNaN(width) ? 0 : width;
                          }
                        })
                        .onClick(() => {
                          this.indicatorIndex = index;
                          // Underline animation effect when clicking tabbar
                          this.underlineScrollAuto(this.animationDuration, index);
                          this.scrollIntoView(index);
                          // Link with tabs
                          this.controller.changeIndex(index)
                        })
                    }
                    .width(this.textLength[index] * 28)
                  }, (item: string) => item)
                }
                .height(32)
              }
              .width('100%')
              .scrollable(ScrollDirection.Horizontal)
              .scrollBar(BarState.Off)
              .edgeEffect(EdgeEffect.None)
              // Scroll event callback, returning horizontal and vertical offset during scrolling
              .onScroll((xOffset: number, yOffset: number) => {
                console.info(xOffset + ' ' + yOffset)
                this.indicatorMarginLeft -= xOffset;
              })
              // Scroll stop event callback
              .onScrollStop(() => {
                console.info('Scroll Stop')
                this.underlineScrollAuto(0, this.indicatorIndex);
              })

              Column()
                .width(this.indicatorWidth)
                .height(2)
                .borderRadius(2)
                .backgroundColor(Color.Red)
                .alignSelf(ItemAlign.Start)
                .margin({ left: this.indicatorMarginLeft, top: 5 })
            }
          }
        }
        .width('100%')
        .margin({ top: 15, bottom: 10 })

      }

      Tabs({ controller: this.controller }) {
        LazyForEach(this.data, (item: number, index: number) => {
          TabContent() {
            List({ space: 10 }) {
              ListItem() {
                Text(item.toString())
              }
            }
            .padding({ left: 10, right: 10 })
            .width("100%")
            .height('95%')
          }
          .onAreaChange((oldValue: Area, newValue: Area) => {
            let width = Number.parseFloat(newValue.width.toString());
            this.tabsWidth = Number.isNaN(width) ? 0 : width;
          })
        }, (item: string) => item)
      }
      .onChange((index: number) => {
        this.currentIndex = index;
      })

      .onAnimationStart((index: number, targetIndex: number, event: TabsAnimationEvent) => {
        // This callback is triggered when the switching animation starts. The underline slides with the page while the width gradually changes.
        this.indicatorIndex = targetIndex;
        this.underlineScrollAuto(this.animationDuration, targetIndex);
      })
      .onAnimationEnd((index: number, event: TabsAnimationEvent) => {
        // This callback is triggered when the switching animation ends. The underline animation stops.
        let currentIndicatorInfo = this.getCurrentIndicatorInfo(index, event);
        this.startAnimateTo(0, currentIndicatorInfo.left, currentIndicatorInfo.width);
        this.scrollIntoView(index);
      })
      .onGestureSwipe((index: number, event: TabsAnimationEvent) => {
        // This callback is triggered frame by frame during the page follow-hand sliding process.
        let currentIndicatorInfo = this.getCurrentIndicatorInfo(index, event);
        this.indicatorIndex = currentIndicatorInfo.index; // Current tab index
        this.indicatorMarginLeft = currentIndicatorInfo.left; // Current tab margin from the left
        this.indicatorWidth = currentIndicatorInfo.width; // Current tab width
      })
    }
    .width('100%')
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Can't inherit HarmonyOS VideoController?

After inheriting VideoController, calling methods in VideoController no longer takes effect. The phenomenon is that clicking pause doesn't pause, and debugging and logs show that the method has been called.

// xxx.ets
@Entry
@Component
struct VideoCreateComponent {
  @State videoSrc: Resource = $rawfile('video1.mp4')
  @State previewUri: Resource = $r('app.media.APEX')
  @State curRate: PlaybackSpeed = PlaybackSpeed.Speed_Forward_1_00_X
  @State isAutoPlay: boolean = false
  @State showControls: boolean = true
  controller: myVideoCol = new myVideoCol();

  build() {
    Column() {
      Video({
        src: this.videoSrc,
        previewUri: this.previewUri,
        currentProgressRate: this.curRate,
        controller: this.controller
      })
        .width('100%')
        .height(600)
        .autoPlay(this.isAutoPlay)
        .controls(this.showControls)
        .onStart(() => {
          console.info('onStart')
        })
        .onPause(() => {
          console.info('onPause')
        })
        .onUpdate((e?: TimeObject) => {
          if (e != undefined) {
            console.info('onUpdate is ' + e.time)
          }
        })

      Row() {
        Button('start').onClick(() => {
          this.controller.myStart() // Start playing
        }).margin(5)
        Button('pause').onClick(() => {
          this.controller.pause() // Pause playing
        }).margin(5)
        Button('stop').onClick(() => {
          this.controller.myStop() // End playing
        }).margin(5)
        Button('setTime').onClick(() => {
          this.controller.mySeekTo(10) // Accurately jump to the 10s position of the video
          // this.controller.setCurrentTime(10, SeekMode.Accurate) // Accurately jump to the 10s position of the video
        }).margin(5)
      }

    }
  }
}

interface DurationObject {
  duration: number;
}

interface TimeObject {
  time: number;
}

class myVideoCol extends VideoController {
  myStart(): void {
    console.log("myVideoCol === myStart")
    super.start()
  }

  pause(): void {
    console.log("myVideoCol === myPause")
    super.pause()
  }

  myStop(): void {
    console.log("myVideoCol === myStop")
    super.stop()
  }

  mySeekTo(time: number): void {
    console.log("myVideoCol === mySeekTo")
    super.setCurrentTime(time)
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Problem of borderRadius nesting Image in HarmonyOS layout?

For example, the problem of Stack nesting Image. The outer borderRadius(50) and the Image exceeding the rounded corner. Is there a way similar to CardView to set the outer radius so that the inner layout does not exceed the rounded corner (RowSplit and ColumnSplit can do it, but nesting Swiper will have gesture sliding conflicts)?

@Component
export struct TestView {
  build() {
    Stack() {
      Image('http://dimg04.c-ctrip.com/images/0zg0x12000cog7bwrDEC4.jpg')
        .width(290)
        .height(300)
    }
    .backgroundColor(Color.Black)
    .borderRadius(50)
    .width(300)
    .height(300)
  }

  aboutToAppear(): void {
    console.debug('TestView', 'aboutToAppear')
  }
}
Enter fullscreen mode Exit fullscreen mode

The borderRadius attribute does not support inheritance of child components. If the view of the child component needs to follow the container, the parent container needs to add clip.

Solution: Add .clip(true) to the Stack.

4. How to set the full background color for HarmonyOS TextInput component?

When adapting to dark mode, when using the TextInput component, it is found that the set background color (backgroundColor) is filled into an area with semi-circles on the left and right, and the remaining surrounding area is not filled with the background color. How can the set background color fill the entire TextInput area?

The Input component can be placed in a Column, and the background color of the Column can be set to fill the background color outside the Input component.

5. Can HarmonyOS ArkUI_NativeModule create RenderNode?

RenderNode can only be created on the TS side, and ArkUI_NativeModule is used to dock with third-party frameworks.

Top comments (0)