Tabs in ArkUI are container components that facilitate content view switching through tabs. Each tab corresponds to a specific content view. The content is presented using the TabContent
component, which is a child component of Tabs
. You can customize the style of the navigation bar by setting the tabBar
property on TabContent
. Now, let's implement the effect shown in the following UI design based on the reference image:
Analysis and Implementation Steps
Based on the analysis of the above image, achieving the desired effect requires the following steps:
- Set the background color of the tabBar and the selected tab style.
- Customize the navigation bar indicator.
- Configure the indicator to slide smoothly with the content view during transitions.
Setting TabBar Background Color and Selected Tab Style
- First, use the
@Builder
decorator to define a custom component. - Set the background image and color based on the currently selected tab index. Note that the top-left and top-right corners should have rounded edges, which need to be determined based on the tab's position.
- Since the selected tab style is a trapezoid with rounded corners, we use three different trapezoid images for this effect.
@Builder tabBuilder(title: string, targetIndex: number, selectImage: ResourceStr) { // Create a Column layout Column() { // Create a Text component to display the title Text(title) // Set font color based on whether the tab is selected .fontColor(this.currentIndex === targetIndex ? $r("app.color.text_one") : $r("app.color.text_two")) // Set font size to 14 .fontSize(14) // Set font weight based on selection state .fontWeight(this.currentIndex === targetIndex ? 600 : 400) } // Set Column width to 100% .width('100%') // Set Column height to 100% .height("100%") // Vertically center the child component .justifyContent(FlexAlign.Center) // Set background image if the tab is selected .backgroundImage(this.currentIndex == targetIndex ? selectImage : null) // Set background color .backgroundColor($r("app.color.bg_data_color")) // Set rounded corners for top-left and top-right based on tab position .borderRadius({ topLeft: targetIndex == 0 ? 8 : 0, topRight: targetIndex == 2 ? 8 : 0 }) // Set background image to fill the container .backgroundImageSize(ImageSize.FILL) }
Customizing the Navigation Bar Indicator
Since the indicator needs to slide smoothly with the content view, it cannot be set within the individual tabBuilder
. Instead:
- Use a
Column
component to define the bottom indicator with a blue bar that matches the text width and has a height of 3. - The indicator width can be dynamically set to match the text width or a fixed value.
- The left margin of the indicator should be dynamically adjusted with animation to achieve smooth sliding effects.
Stack() { Tabs({ barPosition: BarPosition.Start }) { TabContent() { this.tripPage() }.tabBar(this.tabBuilder("Property", 0, $r("app.media.trip_data_start_bg"))) .align(Alignment.TopStart).margin({ top: 54 }) // Additional TabContent components would follow... // ... // ... } .backgroundColor($r("app.color.white")) .borderRadius(8) .barHeight(44) .width("93.6%") .height(380) .onChange((index) => { this.currentIndex = index }) // Custom indicator: a blue bar with dynamic width and position Column() .width(this.indicatorWidth) .height(3) .backgroundColor($r("app.color.main_color")) .margin({ left: this.indicatorLeftMargin, top: 42 }) .borderRadius(1) }
Adding Indicator Animation
To make the indicator slide smoothly with finger gestures during tab transitions, we need to add animations and listen for tab animation events and gesture interactions.
/** * Starts an animation to move the indicator to a specified position * * @param duration Animation duration in milliseconds * @param leftMargin Left margin after animation completes * @param width Width after animation completes */ private startAnimateTo(duration: number, leftMargin: number, width: number) { // Set animation start flag this.isStartAnimateTo = true animateTo({ // Animation duration duration: duration, // Animation curve curve: Curve.Linear, // Number of iterations iterations: 1, // Play mode playMode: PlayMode.Normal, // Callback when animation finishes onFinish: () => { // Reset animation flag this.isStartAnimateTo = false // Log animation completion console.info('Animation finished') } }, () => { // Update indicator position and width this.indicatorLeftMargin = leftMargin this.indicatorWidth = width }) }
1. Animation Start Listener
When the tab switching animation starts, set the target index as the current index and start the indicator animation to dynamically adjust its left margin.
Tabs({ barPosition: BarPosition.Start }) // ... .onAnimationStart((index: number, targetIndex: number, event: TabsAnimationEvent) => { // Triggered when tab animation starts. Move indicator with the page transition. this.currentIndex = targetIndex this.startAnimateTo(this.animationDuration, this.textInfos[targetIndex][0], this.textInfos[targetIndex][1]) })
2. Animation End Listener
When the tab switching animation ends, the onAnimationEnd
callback is triggered to finalize the indicator position.
Tabs({ barPosition: BarPosition.Start }) // ... .onAnimationEnd((index: number, event: TabsAnimationEvent) => { // Triggered when tab animation ends. Stop the indicator animation. let currentIndicatorInfo = this.getCurrentIndicatorInfo(index, event) this.startAnimateTo(0, currentIndicatorInfo.left, currentIndicatorInfo.width) })
3. Gesture Swipe Listener
This callback is triggered frame by frame during the page swipe gesture, allowing the indicator to follow the user's finger movement.
Tabs({ barPosition: BarPosition.Start }) // ... .onGestureSwipe((index: number, event: TabsAnimationEvent) => { // Triggered frame by frame during page swipe. let currentIndicatorInfo = this.getCurrentIndicatorInfo(index, event) // Update current index this.currentIndex = currentIndicatorInfo.index // Update indicator left margin this.indicatorLeftMargin = currentIndicatorInfo.left // Update indicator width this.indicatorWidth = currentIndicatorInfo.width })
Encapsulating Indicator Information Retrieval
We encapsulate the logic to calculate the indicator's position and width in a separate method. This method is called during gesture swipes to dynamically update the indicator's properties, ensuring it follows the user's gesture smoothly and achieves the desired UI effect.
/** * Calculates the current indicator information based on the swipe event * * @param index Current tab index * @param event Tabs animation event containing swipe information * @returns Object containing indicator index, left margin, and width */ private getCurrentIndicatorInfo(index: number, event: TabsAnimationEvent): Record<string, number> { // Determine next tab index based on swipe direction let nextIndex = index // Swiping left (from user perspective) if (index > 0 && event.currentOffset > 0) { nextIndex-- } // Swiping right (from user perspective) else if (index < 3 && event.currentOffset < 0) { nextIndex++ } // Get information about current and next tabs let indexInfo = this.textInfos[index] let nextIndexInfo = this.textInfos[nextIndex] // Calculate swipe ratio let swipeRatio = Math.abs(event.currentOffset / this.tabsWidth) // Determine current index based on swipe progress // Switch to next tab's style when swiped more than halfway let currentIndex = swipeRatio > 0.5 ? nextIndex : index // Interpolate left margin and width based on swipe ratio let currentLeft = indexInfo[0] + (nextIndexInfo[0] - indexInfo[0]) * swipeRatio let currentWidth = indexInfo[1] + (nextIndexInfo[1] - indexInfo[1]) * swipeRatio // Return calculated properties return { 'index': currentIndex, 'left': currentLeft, 'width': currentWidth } }
This implementation effectively creates a smooth and visually appealing tab navigation system that closely matches modern UI design standards.
Top comments (0)