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
@Builderdecorator 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
Columncomponent 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)