【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%')
}
}
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)
}
}
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')
}
}
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)