Using Component Default Animations and Customizing Scroll Effects
Component Default Animations
Component default animations provide the following functionalities:
- Indicating Current State: For example, when a user clicks a Button component, it default grays out, confirming to the user that the selection operation is complete.
- Enhancing UI Elegance and Liveliness: Default animations add a polished and dynamic feel to the interface.
- Reducing Developer Workload: For instance, list-scrolling components come with built-in scrolling animations that developers can use directly without additional implementation.
For more effects, refer to the component documentation.
Example Code and Effect
@Entry
@Component
struct ComponentDemo {
build() {
Row() {
Checkbox({ name: 'checkbox1', group: 'checkboxGroup' })
.select(true)
.shape(CheckBoxShape.CIRCLE)
.size({ width: 50, height: 50 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
Customizing Component Animations
Some components allow customization of animation effects for child items through property animations and transition animations.
For example, in a Scroll component, you can customize the animation effects of child components during scrolling.
Customizing Scroll Component Animation
To customize the scrolling animation of a Scroll component, you can monitor the scroll distance in the onScroll callback and calculate the affine properties of each component. Alternatively, you can define custom gestures, monitor position changes, and manually invoke ScrollTo to adjust the scroll position.
You can also fine-tune the final scroll position in the onScrollStop callback or gesture end callback.
Example Code and Effect
import { curves, window, display, mediaquery } from '@kit.ArkUI';
import { UIAbility } from '@kit.AbilityKit';
export default class GlobalContext extends AppStorage {
static mainWin: window.Window | undefined = undefined;
static mainWindowSize: window.Size | undefined = undefined;
}
/**
* Class for managing window and display-related information
*/
export class WindowManager {
private static instance: WindowManager | null = null;
private displayInfo: display.Display | null = null;
private orientationListener = mediaquery.matchMediaSync('(orientation: landscape)');
constructor() {
this.orientationListener.on('change', (mediaQueryResult: mediaquery.MediaQueryResult) => { this.onPortrait(mediaQueryResult) });
this.loadDisplayInfo();
}
/**
* Sets the main window
* @param win Current app window
*/
setMainWin(win: window.Window) {
if (win == null) {
return;
}
GlobalContext.mainWin = win;
win.on("windowSizeChange", (data: window.Size) => {
if (GlobalContext.mainWindowSize == undefined || GlobalContext.mainWindowSize == null) {
GlobalContext.mainWindowSize = data;
} else {
if (GlobalContext.mainWindowSize.width == data.width && GlobalContext.mainWindowSize.height == data.height) {
return;
}
GlobalContext.mainWindowSize = data;
}
let winWidth = this.getMainWindowWidth();
AppStorage.setOrCreate<number>('mainWinWidth', winWidth);
let winHeight = this.getMainWindowHeight();
AppStorage.setOrCreate<number>('mainWinHeight', winHeight);
let context: UIAbility = new UIAbility();
context.context.eventHub.emit("windowSizeChange", winWidth, winHeight);
});
}
static getInstance(): WindowManager {
if (WindowManager.instance == null) {
WindowManager.instance = new WindowManager();
}
return WindowManager.instance;
}
private onPortrait(mediaQueryResult: mediaquery.MediaQueryResult) {
if (mediaQueryResult.matches == AppStorage.get<boolean>('isLandscape')) {
return;
}
AppStorage.setOrCreate<boolean>('isLandscape', mediaQueryResult.matches);
this.loadDisplayInfo();
}
/**
* Changes screen orientation
* @param ori Enum value: window.Orientation
*/
changeOrientation(ori: window.Orientation) {
if (GlobalContext.mainWin != null) {
GlobalContext.mainWin.setPreferredOrientation(ori);
}
}
private loadDisplayInfo() {
this.displayInfo = display.getDefaultDisplaySync();
AppStorage.setOrCreate<number>('displayWidth', this.getDisplayWidth());
AppStorage.setOrCreate<number>('displayHeight', this.getDisplayHeight());
}
/**
* Gets main window width in vp units
*/
getMainWindowWidth(): number {
return GlobalContext.mainWindowSize != null ? px2vp(GlobalContext.mainWindowSize.width) : 0;
}
/**
* Gets main window height in vp units
*/
getMainWindowHeight(): number {
return GlobalContext.mainWindowSize != null ? px2vp(GlobalContext.mainWindowSize.height) : 0;
}
/**
* Gets screen width in vp units
*/
getDisplayWidth(): number {
return this.displayInfo != null ? px2vp(this.displayInfo.width) : 0;
}
/**
* Gets screen height in vp units
*/
getDisplayHeight(): number {
return this.displayInfo != null ? px2vp(this.displayInfo.height) : 0;
}
/**
* Releases resources
*/
release() {
if (this.orientationListener) {
this.orientationListener.off('change', (mediaQueryResult: mediaquery.MediaQueryResult) => { this.onPortrait(mediaQueryResult) });
}
if (GlobalContext.mainWin != null) {
GlobalContext.mainWin.off('windowSizeChange');
}
WindowManager.instance = null;
}
}
/**
* Data class for task card information
*/
export class TaskData {
bgColor: Color | string | Resource = Color.White;
index: number = 0;
taskInfo: string = 'music';
constructor(bgColor: Color | string | Resource, index: number, taskInfo: string) {
this.bgColor = bgColor;
this.index = index;
this.taskInfo = taskInfo;
}
}
export const taskDataArr: Array<TaskData> = [
new TaskData('#317AF7', 0, 'music'),
new TaskData('#D94838', 1, 'mall'),
new TaskData('#DB6B42', 2, 'photos'),
new TaskData('#5BA854', 3, 'setting'),
new TaskData('#317AF7', 4, 'call'),
new TaskData('#D94838', 5, 'music'),
new TaskData('#DB6B42', 6, 'mall'),
new TaskData('#5BA854', 7, 'photos'),
new TaskData('#D94838', 8, 'setting'),
new TaskData('#DB6B42', 9, 'call'),
new TaskData('#5BA854', 10, 'music')
];
@Entry
@Component
export struct TaskSwitchMainPage {
displayWidth: number = WindowManager.getInstance().getDisplayWidth();
scroller: Scroller = new Scroller();
cardSpace: number = 0; // Space between cards
cardWidth: number = this.displayWidth / 2 - this.cardSpace / 2; // Card width
cardHeight: number = 400; // Card height
cardPosition: Array<number> = []; // Initial positions of cards
clickIndex: boolean = false;
@State taskViewOffsetX: number = 0;
@State cardOffset: number = this.displayWidth / 4;
lastCardOffset: number = this.cardOffset;
startTime: number | undefined = undefined;
// Initial positions of each card
aboutToAppear() {
for (let i = 0; i < taskDataArr.length; i++) {
this.cardPosition[i] = i * (this.cardWidth + this.cardSpace);
}
}
// Positions of each card
getProgress(index: number): number {
let progress = (this.cardOffset + this.cardPosition[index] - this.taskViewOffsetX + this.cardWidth / 2) / this.displayWidth;
return progress;
}
build() {
Stack({ alignContent: Alignment.Bottom }) {
// Background
Column()
.width('100%')
.height('100%')
.backgroundColor(0xF0F0F0);
// Scroll component
Scroll(this.scroller) {
Row({ space: this.cardSpace }) {
ForEach(taskDataArr, (item: TaskData, index) => {
Column()
.width(this.cardWidth)
.height(this.cardHeight)
.backgroundColor(item.bgColor)
.borderStyle(BorderStyle.Solid)
.borderWidth(1)
.borderColor(0xAFEEEE)
.borderRadius(15)
// Calculate affine properties for child components
.scale((this.getProgress(index) >= 0.4 && this.getProgress(index) <= 0.6) ?
{
x: 1.1 - Math.abs(0.5 - this.getProgress(index)),
y: 1.1 - Math.abs(0.5 - this.getProgress(index))
} :
{ x: 1, y: 1 })
.animation({ curve: Curve.Smooth })
// Scroll animation
.translate({ x: this.cardOffset })
.animation({ curve: curves.springMotion() })
.zIndex((this.getProgress(index) >= 0.4 && this.getProgress(index) <= 0.6) ? 2 : 1);
}, (item: TaskData) => item.toString());
}
.width((this.cardWidth + this.cardSpace) * (taskDataArr.length + 1))
.height('100%');
}
.gesture(
GestureGroup(GestureMode.Parallel,
PanGesture({ direction: PanDirection.Horizontal, distance: 5 })
.onActionStart((event: GestureEvent | undefined) => {
if (event) {
this.startTime = event.timestamp;
}
})
.onActionUpdate((event: GestureEvent | undefined) => {
if (event) {
this.cardOffset = this.lastCardOffset + event.offsetX;
}
})
.onActionEnd((event: GestureEvent | undefined) => {
if (event) {
let time = 0;
if (this.startTime) {
time = event.timestamp - this.startTime;
}
let speed = event.offsetX / (time / 1000000000);
let moveX = Math.pow(speed, 2) / 7000 * (speed > 0 ? 1 : -1);
this.cardOffset += moveX;
// If scrolling left beyond the rightmost position
let cardOffsetMax = -(taskDataArr.length - 1) * (this.displayWidth / 2);
if (this.cardOffset < cardOffsetMax) {
this.cardOffset = cardOffsetMax;
}
// If scrolling right beyond the leftmost position
if (this.cardOffset > this.displayWidth / 4) {
this.cardOffset = this.displayWidth / 4;
}
// Calculate remaining margin and adjust position
let remainMargin = this.cardOffset % (this.displayWidth / 2);
if (remainMargin < 0) {
remainMargin = this.cardOffset % (this.displayWidth / 2) + this.displayWidth / 2;
}
if (remainMargin <= this.displayWidth / 4) {
this.cardOffset += this.displayWidth / 4 - remainMargin;
} else {
this.cardOffset -= this.displayWidth / 4 - (this.displayWidth / 2 - remainMargin);
}
// Update last card offset
this.lastCardOffset = this.cardOffset;
}
})
), GestureMask.IgnoreInternal)
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off);
// Button to scroll to first/last position
Button('Move to first/last')
.backgroundColor(0x888888)
.margin({ bottom: 30 })
.onClick(() => {
this.clickIndex = !this.clickIndex;
if (this.clickIndex) {
this.cardOffset = this.displayWidth / 4;
} else {
this.cardOffset = this.displayWidth / 4 - (taskDataArr.length - 1) * this.displayWidth / 2;
}
this.lastCardOffset = this.cardOffset;
});
}
.width('100%')
.height('100%');
}
}
Top comments (0)