Files
cocos-engine/cocos/ui/scroll-view.ts
Santy-Wang 23de9188ba V3.5 docs (#10898)
* adjust api docs and delete useless annotation

* delete useless annotation
2022-04-27 15:22:22 +08:00

1918 lines
63 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
Copyright (c) 2013-2016 Chukong Technologies Inc.
Copyright (c) 2017-2020 Xiamen Yaji Software Co., Ltd.
http://www.cocos.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated engine source code (the "Software"), a limited,
worldwide, royalty-free, non-assignable, revocable and non-exclusive license
to use Cocos Creator solely to develop games on your target platforms. You shall
not use Cocos Creator software for developing other software or tools that's
used for developing games. You are not granted to publish, distribute,
sublicense, and/or sell copies of Cocos Creator.
The software or tools in this License Agreement are licensed, not sold.
Xiamen Yaji Software Co., Ltd. reserves all rights not expressly granted to you.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
import { ccclass, help, executionOrder, menu, requireComponent, tooltip, displayOrder, range, type, serializable } from 'cc.decorator';
import { EDITOR } from 'internal:constants';
import { EventHandler as ComponentEventHandler } from '../core/components/component-event-handler';
import { UITransform } from '../2d/framework';
import { Event, EventMouse, EventTouch, Touch, SystemEventType } from '../input/types';
import { logID } from '../core/platform/debug';
import { Size, Vec2, Vec3 } from '../core/math';
import { Layout } from './layout';
import { ScrollBar } from './scroll-bar';
import { ViewGroup } from './view-group';
import { Node } from '../core/scene-graph/node';
import { director, Director } from '../core/director';
import { TransformBit } from '../core/scene-graph/node-enum';
import { legacyCC } from '../core/global-exports';
import { NodeEventType } from '../core/scene-graph/node-event';
const NUMBER_OF_GATHERED_TOUCHES_FOR_MOVE_SPEED = 5;
const OUT_OF_BOUNDARY_BREAKING_FACTOR = 0.05;
const EPSILON = 1e-4;
const TOLERANCE = 1e4;
const MOVEMENT_FACTOR = 0.7;
const _tempVec3 = new Vec3();
const _tempVec3_1 = new Vec3();
const _tempVec2 = new Vec2();
const _tempVec2_1 = new Vec2();
const quintEaseOut = (time: number) => {
time -= 1;
return (time * time * time * time * time + 1);
};
const getTimeInMilliseconds = () => {
const currentTime = new Date();
return currentTime.getMilliseconds();
};
const eventMap = {
'scroll-to-top': 0,
'scroll-to-bottom': 1,
'scroll-to-left': 2,
'scroll-to-right': 3,
scrolling: 4,
'bounce-bottom': 6,
'bounce-left': 7,
'bounce-right': 8,
'bounce-top': 5,
'scroll-ended': 9,
'touch-up': 10,
'scroll-ended-with-threshold': 11,
'scroll-began': 12,
};
/**
* @en
* Enum for ScrollView event type.
*
* @zh
* 滚动视图事件类型
*/
export enum EventType {
/**
* @en
* The event emitted when ScrollView scroll to the top boundary of inner container.
*
* @zh
* 滚动视图滚动到顶部边界事件。
*/
SCROLL_TO_TOP = 'scroll-to-top',
/**
* @en
* The event emitted when ScrollView scroll to the bottom boundary of inner container.
*
* @zh
* 滚动视图滚动到底部边界事件。
*/
SCROLL_TO_BOTTOM = 'scroll-to-bottom',
/**
* @en
* The event emitted when ScrollView scroll to the left boundary of inner container.
*
* @zh
* 滚动视图滚动到左边界事件。
*/
SCROLL_TO_LEFT = 'scroll-to-left',
/**
* @en
* The event emitted when ScrollView scroll to the right boundary of inner container.
*
* @zh
* 滚动视图滚动到右边界事件。
*/
SCROLL_TO_RIGHT = 'scroll-to-right',
/**
* @en
* The event emitted when ScrollView scroll began.
*
* @zh
* 滚动视图滚动开始时发出的事件。
*/
SCROLL_BEGAN = 'scroll-began',
/**
* @en
* The event emitted when ScrollView auto scroll ended.
*
* @zh
* 滚动视图滚动结束的时候发出的事件。
*/
SCROLL_ENDED = 'scroll-ended',
/**
* @en
* The event emitted when ScrollView scroll to the top boundary of inner container and start bounce.
*
* @zh
* 滚动视图滚动到顶部边界并且开始回弹时发出的事件。
*/
BOUNCE_TOP = 'bounce-top',
/**
* @en
* The event emitted when ScrollView scroll to the bottom boundary of inner container and start bounce.
*
* @zh
* 滚动视图滚动到底部边界并且开始回弹时发出的事件。
*/
BOUNCE_BOTTOM = 'bounce-bottom',
/**
* @en
* The event emitted when ScrollView scroll to the left boundary of inner container and start bounce.
*
* @zh
* 滚动视图滚动到左边界并且开始回弹时发出的事件。
*/
BOUNCE_LEFT = 'bounce-left',
/**
* @en
* The event emitted when ScrollView scroll to the right boundary of inner container and start bounce.
*
* @zh
* 滚动视图滚动到右边界并且开始回弹时发出的事件。
*/
BOUNCE_RIGHT = 'bounce-right',
/**
* @en
* The event emitted when ScrollView is scrolling.
*
* @zh
* 滚动视图正在滚动时发出的事件。
*/
SCROLLING = 'scrolling',
/**
* @en
* The event emitted when ScrollView auto scroll ended with a threshold.
*
* @zh
* 滚动视图自动滚动快要结束的时候发出的事件。
*/
SCROLL_ENG_WITH_THRESHOLD = 'scroll-ended-with-threshold',
/**
* @en
* The event emitted when user release the touch.
*
* @zh
* 当用户松手的时候会发出一个事件。
*/
TOUCH_UP = 'touch-up',
}
/**
* @en
* Layout container for a view hierarchy that can be scrolled by the user,
* allowing it to be larger than the physical display.
*
* @zh
* 滚动视图组件。
*/
@ccclass('cc.ScrollView')
@help('i18n:cc.ScrollView')
@executionOrder(110)
@menu('UI/ScrollView')
@requireComponent(UITransform)
export class ScrollView extends ViewGroup {
public static EventType = EventType;
/**
* @en
* The elapse time of bouncing back. A value of 0 will bounce back immediately.
*
* @zh
* 回弹持续的时间,0 表示将立即反弹。
*/
@serializable
@range([0, 10])
@displayOrder(5)
@tooltip('i18n:scrollview.bounceDuration')
public bounceDuration = 1;
/**
* @en
* It determines how quickly the content stop moving. A value of 1 will stop the movement immediately.
* A value of 0 will never stop the movement until it reaches to the boundary of scrollview.
*
* @zh
* 开启惯性后,在用户停止触摸后滚动多快停止,0表示永不停止,1表示立刻停止。
*/
@serializable
@range([0, 1, 0.1])
@displayOrder(3)
@tooltip('i18n:scrollview.brake')
public brake = 0.5;
/**
* @en
* When elastic is set, the content will be bounce back when move out of boundary.
*
* @zh
* 是否允许滚动内容超过边界,并在停止触摸后回弹。
*/
@serializable
@displayOrder(3)
@tooltip('i18n:scrollview.elastic')
public elastic = true;
/**
* @en
* When inertia is set, the content will continue to move when touch ended.
*
* @zh
* 是否开启滚动惯性。
*/
@serializable
@displayOrder(2)
@tooltip('i18n:scrollview.inertia')
public inertia = true;
/**
* @en
* This is a reference to the UI element to be scrolled.
*
* @zh
* 可滚动展示内容的节点。
*/
@type(Node)
@displayOrder(5)
@tooltip('i18n:scrollview.content')
get content () {
return this._content;
}
set content (value) {
if (this._content === value) {
return;
}
const viewTrans = value && value.parent && value.parent._uiProps.uiTransformComp;
if (value && (!value || !viewTrans)) {
logID(4302);
return;
}
this._content = value;
this._calculateBoundary();
}
/**
* @en
* Enable horizontal scroll.
*
* @zh
* 是否开启水平滚动。
*/
@serializable
@displayOrder(0)
@tooltip('i18n:scrollview.horizontal')
public horizontal = true;
/**
* @en
* The horizontal scrollbar reference.
* @zh
* 水平滚动的 ScrollBar。
*/
@type(ScrollBar)
@displayOrder(0)
@tooltip('i18n:scrollview.horizontal_bar')
get horizontalScrollBar () {
return this._horizontalScrollBar;
}
set horizontalScrollBar (value: ScrollBar | null) {
if (this._horizontalScrollBar === value) {
return;
}
this._horizontalScrollBar = value;
if (this._horizontalScrollBar) {
this._horizontalScrollBar.setScrollView(this);
this._updateScrollBar(Vec2.ZERO);
}
}
/**
* @en
* Enable vertical scroll.
*
* @zh
* 是否开启垂直滚动。
*/
@serializable
@displayOrder(1)
@tooltip('i18n:scrollview.vertical')
public vertical = true;
/**
* @en
* The vertical scrollbar reference.
*
* @zh
* 垂直滚动的 ScrollBar。
*/
@type(ScrollBar)
@displayOrder(1)
@tooltip('i18n:scrollview.vertical_bar')
get verticalScrollBar () {
return this._verticalScrollBar;
}
set verticalScrollBar (value: ScrollBar | null) {
if (this._verticalScrollBar === value) {
return;
}
this._verticalScrollBar = value;
if (this._verticalScrollBar) {
this._verticalScrollBar.setScrollView(this);
this._updateScrollBar(Vec2.ZERO);
}
}
/**
* @en
* If cancelInnerEvents is set to true, the scroll behavior will cancel touch events on inner content nodes
* It's set to true by default.
*
* @zh
* 如果这个属性被设置为 true,那么滚动行为会取消子节点上注册的触摸事件,默认被设置为 true。<br/>
* 注意,子节点上的 touchstart 事件仍然会触发,触点移动距离非常短的情况下 touchmove 和 touchend 也不会受影响。
*/
@serializable
@displayOrder(9)
@tooltip('i18n:scrollview.cancelInnerEvents')
public cancelInnerEvents = true;
/**
* @en
* ScrollView events callback.
*
* @zh
* 滚动视图的事件回调函数。
*/
@type([ComponentEventHandler])
@serializable
@displayOrder(10)
@tooltip('i18n:scrollview.scrollEvents')
public scrollEvents: ComponentEventHandler[] = [];
get view () {
const parent = this._content && this._content.parent;
if (!parent) {
return null;
}
return parent._uiProps.uiTransformComp;
}
protected _autoScrolling = false;
protected _scrolling = false;
@serializable
protected _content: Node | null = null;
@serializable
protected _horizontalScrollBar: ScrollBar | null = null;
@serializable
protected _verticalScrollBar: ScrollBar | null = null;
protected _topBoundary = 0;
protected _bottomBoundary = 0;
protected _leftBoundary = 0;
protected _rightBoundary = 0;
protected _touchMoveDisplacements: Vec3[] = [];
protected _touchMoveTimeDeltas: number[] = [];
protected _touchMovePreviousTimestamp = 0;
protected _touchMoved = false;
protected _autoScrollAttenuate = false;
protected _autoScrollStartPosition = new Vec3();
protected _autoScrollTargetDelta = new Vec3();
protected _autoScrollTotalTime = 0;
protected _autoScrollAccumulatedTime = 0;
protected _autoScrollCurrentlyOutOfBoundary = false;
protected _autoScrollBraking = false;
protected _autoScrollBrakingStartPosition = new Vec3();
protected _outOfBoundaryAmount = new Vec3();
protected _outOfBoundaryAmountDirty = true;
protected _stopMouseWheel = false;
protected _mouseWheelEventElapsedTime = 0.0;
protected _isScrollEndedWithThresholdEventFired = false;
// use bit wise operations to indicate the direction
protected _scrollEventEmitMask = 0;
protected _isBouncing = false;
protected _contentPos = new Vec3();
protected _deltaPos = new Vec3();
/**
* @en
* Scroll the content to the bottom boundary of ScrollView.
*
* @zh
* 视图内容将在规定时间内滚动到视图底部。
*
* @param timeInSecond - 滚动时间(s)。 如果超时,内容将立即跳到底部边界。
* @param attenuated - 滚动加速是否衰减,默认为 true。
* @example
* ```ts
* // Scroll to the bottom of the view.
* scrollView.scrollToBottom(0.1);
* ```
*/
public scrollToBottom (timeInSecond?: number, attenuated = true) {
const moveDelta = this._calculateMovePercentDelta({
anchor: new Vec2(0, 0),
applyToHorizontal: false,
applyToVertical: true,
});
if (timeInSecond) {
this._startAutoScroll(moveDelta, timeInSecond, attenuated !== false);
} else {
this._moveContent(moveDelta, true);
}
}
/**
* @en
* Scroll the content to the top boundary of ScrollView.
*
* @zh
* 视图内容将在规定时间内滚动到视图顶部。
*
* @param timeInSecond - 滚动时间(s)。 如果超时,内容将立即跳到顶部边界。
* @param attenuated - 滚动加速是否衰减,默认为 true。
* @example
* ```ts
* // Scroll to the top of the view.
* scrollView.scrollToTop(0.1);
* ```
*/
public scrollToTop (timeInSecond?: number, attenuated = true) {
const moveDelta = this._calculateMovePercentDelta({
anchor: new Vec2(0, 1),
applyToHorizontal: false,
applyToVertical: true,
});
if (timeInSecond) {
this._startAutoScroll(moveDelta, timeInSecond, attenuated !== false);
} else {
this._moveContent(moveDelta);
}
}
/**
* @en
* Scroll the content to the left boundary of ScrollView.
*
* @zh
* 视图内容将在规定时间内滚动到视图左边。
*
* @param timeInSecond - 滚动时间(s)。 如果超时,内容将立即跳到左边边界。
* @param attenuated - 滚动加速是否衰减,默认为 true。
* @example
* ```ts
* // Scroll to the left of the view.
* scrollView.scrollToLeft(0.1);
* ```
*/
public scrollToLeft (timeInSecond?: number, attenuated = true) {
const moveDelta = this._calculateMovePercentDelta({
anchor: new Vec2(0, 0),
applyToHorizontal: true,
applyToVertical: false,
});
if (timeInSecond) {
this._startAutoScroll(moveDelta, timeInSecond, attenuated !== false);
} else {
this._moveContent(moveDelta);
}
}
/**
* @en
* Scroll the content to the right boundary of ScrollView.
*
* @zh
* 视图内容将在规定时间内滚动到视图右边。
*
* @param timeInSecond - 滚动时间(s)。 如果超时,内容将立即跳到右边边界。
* @param attenuated - 滚动加速是否衰减,默认为 true。
* @example
* ```ts
* // Scroll to the right of the view.
* scrollView.scrollToRight(0.1);
* ```
*/
public scrollToRight (timeInSecond?: number, attenuated = true) {
const moveDelta = this._calculateMovePercentDelta({
anchor: new Vec2(1, 0),
applyToHorizontal: true,
applyToVertical: false,
});
if (timeInSecond) {
this._startAutoScroll(moveDelta, timeInSecond, attenuated !== false);
} else {
this._moveContent(moveDelta);
}
}
/**
* @en
* Scroll the content to the top left boundary of ScrollView.
*
* @zh
* 视图内容将在规定时间内滚动到视图左上角。
*
* @param timeInSecond - 滚动时间(s)。 如果超时,内容将立即跳到左上边边界。
* @param attenuated - 滚动加速是否衰减,默认为 true。
* @example
* ```ts
* // Scroll to the upper left corner of the view.
* scrollView.scrollToTopLeft(0.1);
* ```
*/
public scrollToTopLeft (timeInSecond?: number, attenuated = true) {
const moveDelta = this._calculateMovePercentDelta({
anchor: new Vec2(0, 1),
applyToHorizontal: true,
applyToVertical: true,
});
if (timeInSecond) {
this._startAutoScroll(moveDelta, timeInSecond, attenuated !== false);
} else {
this._moveContent(moveDelta);
}
}
/**
* @en
* Scroll the content to the top right boundary of ScrollView.
*
* @zh
* 视图内容将在规定时间内滚动到视图右上角。
*
* @param timeInSecond - 滚动时间(s)。 如果超时,内容将立即跳到右上边界。
* @param attenuated - 滚动加速是否衰减,默认为 true。
* @example
* ```ts
* // Scroll to the top right corner of the view.
* scrollView.scrollToTopRight(0.1);
* ```
*/
public scrollToTopRight (timeInSecond?: number, attenuated = true) {
const moveDelta = this._calculateMovePercentDelta({
anchor: new Vec2(1, 1),
applyToHorizontal: true,
applyToVertical: true,
});
if (timeInSecond) {
this._startAutoScroll(moveDelta, timeInSecond, attenuated !== false);
} else {
this._moveContent(moveDelta);
}
}
/**
* @en
* Scroll the content to the bottom left boundary of ScrollView.
*
* @zh
* 视图内容将在规定时间内滚动到视图左下角。
*
* @param timeInSecond - 滚动时间(s)。 如果超时,内容将立即跳到左下边界。
* @param attenuated - 滚动加速是否衰减,默认为 true。
* @example
* ```ts
* // Scroll to the lower left corner of the view.
* scrollView.scrollToBottomLeft(0.1);
* ```
*/
public scrollToBottomLeft (timeInSecond?: number, attenuated = true) {
const moveDelta = this._calculateMovePercentDelta({
anchor: new Vec2(0, 0),
applyToHorizontal: true,
applyToVertical: true,
});
if (timeInSecond) {
this._startAutoScroll(moveDelta, timeInSecond, attenuated !== false);
} else {
this._moveContent(moveDelta);
}
}
/**
* @en
* Scroll the content to the bottom right boundary of ScrollView.
*
* @zh
* 视图内容将在规定时间内滚动到视图右下角。
*
* @param timeInSecond - 滚动时间(s)。 如果超时,内容将立即跳到右边下边界。
* @param attenuated - 滚动加速是否衰减,默认为 true。
* @example
* ```ts
* // Scroll to the lower right corner of the view.
* scrollView.scrollToBottomRight(0.1);
* ```
*/
public scrollToBottomRight (timeInSecond?: number, attenuated = true) {
const moveDelta = this._calculateMovePercentDelta({
anchor: new Vec2(1, 0),
applyToHorizontal: true,
applyToVertical: true,
});
if (timeInSecond) {
this._startAutoScroll(moveDelta, timeInSecond, attenuated !== false);
} else {
this._moveContent(moveDelta);
}
}
/**
* @en
* Scroll with an offset related to the ScrollView's top left origin, if timeInSecond is omitted, then it will jump to the specific offset immediately.
*
* @zh
* 视图内容在规定时间内将滚动到 ScrollView 相对左上角原点的偏移位置, 如果 timeInSecond 参数不传,则立即滚动到指定偏移位置。
*
* @param offset - 滚动视图后,视图内容(content)相对于视图窗口(viewport)的位置。
* @param timeInSecond - 滚动时间(s)。 如果超时,内容将立即跳到指定偏移量处。
* @param attenuated - 滚动加速是否衰减,默认为 true。
* @example
* ```ts
* // Scroll to middle position in 0.1 second in x-axis
* let maxScrollOffset = this.getMaxScrollOffset();
* scrollView.scrollToOffset(new Vec2(maxScrollOffset.x / 2, 0), 0.1);
* ```
*/
public scrollToOffset (offset: Vec2, timeInSecond?: number, attenuated = true) {
const maxScrollOffset = this.getMaxScrollOffset();
const anchor = new Vec2(0, 0);
// if maxScrollOffset is 0, then always align the content's top left origin to the top left corner of its parent
if (maxScrollOffset.x === 0) {
anchor.x = 0;
} else {
anchor.x = offset.x / maxScrollOffset.x;
}
if (maxScrollOffset.y === 0) {
anchor.y = 1;
} else {
anchor.y = (maxScrollOffset.y - offset.y) / maxScrollOffset.y;
}
this.scrollTo(anchor, timeInSecond, attenuated);
}
/**
* @en
* Get the position of the scrolling view relative to the origin in the upper-left corner of the viewport.
*
* @zh
* 获取滚动视图相对于视图窗口左上角原点的位置。
*
* @return - 当前滚动偏移量。
*/
public getScrollOffset () {
const topDelta = this._getContentTopBoundary() - this._topBoundary;
const leftDelta = this._getContentLeftBoundary() - this._leftBoundary;
return new Vec2(leftDelta, topDelta);
}
/**
* @en
* Get the maximize available scroll offset.
*
* @zh
* 获取滚动视图最大可以滚动的偏移量。
*
* @return - 最大可滚动偏移量。
*/
public getMaxScrollOffset () {
if (!this._content || !this.view) {
return Vec2.ZERO;
}
const contentSize = this._content._uiProps.uiTransformComp!.contentSize;
let horizontalMaximizeOffset = contentSize.width - this.view.width;
let verticalMaximizeOffset = contentSize.height - this.view.height;
horizontalMaximizeOffset = horizontalMaximizeOffset >= 0 ? horizontalMaximizeOffset : 0;
verticalMaximizeOffset = verticalMaximizeOffset >= 0 ? verticalMaximizeOffset : 0;
return new Vec2(horizontalMaximizeOffset, verticalMaximizeOffset);
}
/**
* @en
* Scroll the content to the horizontal percent position of ScrollView.
*
* @zh
* 视图内容在规定时间内将滚动到 ScrollView 水平方向的百分比位置上。
*
* @param percent - 0 - 之间的百分比。
* @param timeInSecond - 滚动时间(s)。 如果超时,内容将立即跳到指定水平百分比位置。
* @param attenuated - 滚动加速是否衰减,默认为 true。
* @example
* ```ts
* // Scroll to middle position.
* scrollView.scrollToBottomRight(0.5, 0.1);
* ```
*/
public scrollToPercentHorizontal (percent: number, timeInSecond: number, attenuated: boolean) {
const moveDelta = this._calculateMovePercentDelta({
anchor: new Vec2(percent, 0),
applyToHorizontal: true,
applyToVertical: false,
});
if (timeInSecond) {
this._startAutoScroll(moveDelta, timeInSecond, attenuated !== false);
} else {
this._moveContent(moveDelta);
}
}
/**
* @en
* Scroll the content to the percent position of ScrollView in any direction.
*
* @zh
* 视图内容在规定时间内进行垂直方向和水平方向的滚动,并且滚动到指定百分比位置上。
*
* @param anchor - 在 new Vec2(0,0) and new Vec2(1,1) 上取差值的一个点。
* @param timeInSecond - 滚动时间(s)。 如果超时,内容将立即跳到指定水平或垂直百分比位置。
* @param attenuated - 滚动加速是否衰减,默认为 true。
* @example
* ```ts
* // Vertical scroll to the bottom of the view.
* scrollView.scrollTo(new Vec2(0, 1), 0.1);
*
* // Horizontal scroll to view right.
* scrollView.scrollTo(new Vec2(1, 0), 0.1);
* ```
*/
public scrollTo (anchor: Vec2, timeInSecond?: number, attenuated?: boolean) {
const moveDelta = this._calculateMovePercentDelta({
anchor: new Vec2(anchor),
applyToHorizontal: true,
applyToVertical: true,
});
if (timeInSecond) {
this._startAutoScroll(moveDelta, timeInSecond, attenuated);
} else {
this._moveContent(moveDelta);
}
}
/**
* @en
* Scroll the content to the vertical percent position of ScrollView.
*
* @zh
* 视图内容在规定时间内滚动到 ScrollView 垂直方向的百分比位置上。
*
* @param percent - 0 - 1 之间的百分比。
* @param timeInSecond - 滚动时间(s)。 如果超时,内容将立即跳到指定垂直百分比位置。
* @param attenuated - 滚动加速是否衰减,默认为 true。
* @example
* ```ts
* scrollView.scrollToPercentVertical(0.5, 0.1);
* ```
*/
public scrollToPercentVertical (percent: number, timeInSecond?: number, attenuated?: boolean) {
const moveDelta = this._calculateMovePercentDelta({
anchor: new Vec2(0, percent),
applyToHorizontal: false,
applyToVertical: true,
});
if (timeInSecond) {
this._startAutoScroll(moveDelta, timeInSecond, attenuated);
} else {
this._moveContent(moveDelta);
}
}
/**
* @en
* Stop auto scroll immediately.
*
* @zh
* 停止自动滚动, 调用此 API 可以让 ScrollView 立即停止滚动。
*/
public stopAutoScroll () {
this._autoScrolling = false;
this._autoScrollAccumulatedTime = this._autoScrollTotalTime;
}
/**
* @en
* Modify the content position.
*
* @zh
* 设置当前视图内容的坐标点。
*
* @param position - current content position.
* @deprecated Since 3.1.0, setContentPosition is deprecated, please use scrollToOffset instead.
*/
public setContentPosition (position: Vec3) {
this._setContentPosition(position);
}
private _setContentPosition (position: Vec3) {
if (!this._content) {
return;
}
const contentPos = this._getContentPosition();
if (Math.abs(position.x - contentPos.x) < EPSILON && Math.abs(position.y - contentPos.y) < EPSILON) {
return;
}
this._content.setPosition(position);
this._outOfBoundaryAmountDirty = true;
}
/**
* @en
* Query the content's position in its parent space.
*
* @zh
* 获取当前视图内容的坐标点。
*
* @returns - current content position.
* @deprecated Since 3.1.0, getContentPosition is deprecated.
*/
public getContentPosition () {
return this._getContentPosition();
}
private _getContentPosition (): Vec3 {
if (!this._content) {
return Vec3.ZERO.clone();
}
this._contentPos.set(this._content.position);
return this._contentPos;
}
/**
* @en
* Query whether the user is currently dragging the ScrollView to scroll it.
*
* @zh
* 用户是否在拖拽当前滚动视图。
*
* @returns - 是否在拖拽当前滚动视图。
*/
public isScrolling () {
return this._scrolling;
}
/**
* @en
* Query whether the ScrollView is currently scrolling because of a bounceback or inertia slowdown.
*
* @zh
* 当前滚动视图是否在惯性滚动。
*
* @returns - 滚动视图是否在惯性滚动。
*/
public isAutoScrolling () {
return this._autoScrolling;
}
public getScrollEndedEventTiming () {
return EPSILON;
}
public start () {
this._calculateBoundary();
// Because widget component will adjust content position and scrollView position is correct after visit
// So this event could make sure the content is on the correct position after loading.
if (this._content) {
director.once(Director.EVENT_BEFORE_DRAW, this._adjustContentOutOfBoundary, this);
}
}
public onEnable () {
if (!EDITOR || legacyCC.GAME_VIEW) {
this._registerEvent();
if (this._content) {
this._content.on(NodeEventType.SIZE_CHANGED, this._calculateBoundary, this);
this._content.on(NodeEventType.TRANSFORM_CHANGED, this._scaleChanged, this);
if (this.view) {
this.view.node.on(NodeEventType.TRANSFORM_CHANGED, this._scaleChanged, this);
this.view.node.on(NodeEventType.SIZE_CHANGED, this._calculateBoundary, this);
}
}
this._calculateBoundary();
}
this._updateScrollBarState();
}
public update (dt: number) {
if (this._autoScrolling) {
this._processAutoScrolling(dt);
}
}
public onDisable () {
if (!EDITOR || legacyCC.GAME_VIEW) {
this._unregisterEvent();
if (this._content) {
this._content.off(NodeEventType.SIZE_CHANGED, this._calculateBoundary, this);
this._content.off(NodeEventType.TRANSFORM_CHANGED, this._scaleChanged, this);
if (this.view) {
this.view.node.off(NodeEventType.TRANSFORM_CHANGED, this._scaleChanged, this);
this.view.node.off(NodeEventType.SIZE_CHANGED, this._calculateBoundary, this);
}
}
}
this._hideScrollBar();
this.stopAutoScroll();
}
// private methods
protected _registerEvent () {
this.node.on(NodeEventType.TOUCH_START, this._onTouchBegan, this, true);
this.node.on(NodeEventType.TOUCH_MOVE, this._onTouchMoved, this, true);
this.node.on(NodeEventType.TOUCH_END, this._onTouchEnded, this, true);
this.node.on(NodeEventType.TOUCH_CANCEL, this._onTouchCancelled, this, true);
this.node.on(NodeEventType.MOUSE_WHEEL, this._onMouseWheel, this, true);
}
protected _unregisterEvent () {
this.node.off(NodeEventType.TOUCH_START, this._onTouchBegan, this, true);
this.node.off(NodeEventType.TOUCH_MOVE, this._onTouchMoved, this, true);
this.node.off(NodeEventType.TOUCH_END, this._onTouchEnded, this, true);
this.node.off(NodeEventType.TOUCH_CANCEL, this._onTouchCancelled, this, true);
this.node.off(NodeEventType.MOUSE_WHEEL, this._onMouseWheel, this, true);
}
protected _onMouseWheel (event: EventMouse, captureListeners?: Node[]) {
if (!this.enabledInHierarchy) {
return;
}
if (this._hasNestedViewGroup(event, captureListeners)) {
return;
}
const deltaMove = new Vec3();
const wheelPrecision = -0.1;
const scrollY = event.getScrollY();
if (this.vertical) {
deltaMove.set(0, scrollY * wheelPrecision, 0);
} else if (this.horizontal) {
deltaMove.set(scrollY * wheelPrecision, 0, 0);
}
this._mouseWheelEventElapsedTime = 0;
this._processDeltaMove(deltaMove);
if (!this._stopMouseWheel) {
this._handlePressLogic();
this.schedule(this._checkMouseWheel, 1.0 / 60, NaN, 0);
this._stopMouseWheel = true;
}
this._stopPropagationIfTargetIsMe(event);
}
protected _onTouchBegan (event: EventTouch, captureListeners?: Node[]) {
if (!this.enabledInHierarchy || !this._content) { return; }
if (this._hasNestedViewGroup(event, captureListeners)) { return; }
this._handlePressLogic();
this._touchMoved = false;
this._stopPropagationIfTargetIsMe(event);
}
protected _onTouchMoved (event: EventTouch, captureListeners?: Node[]) {
if (!this.enabledInHierarchy || !this._content) { return; }
if (this._hasNestedViewGroup(event, captureListeners)) { return; }
const touch = event.touch!;
this._handleMoveLogic(touch);
// Do not prevent touch events in inner nodes
if (!this.cancelInnerEvents) {
return;
}
const deltaMove = touch.getUILocation(_tempVec2);
deltaMove.subtract(touch.getUIStartLocation(_tempVec2_1));
// FIXME: touch move delta should be calculated by DPI.
if (deltaMove.length() > 7) {
if (!this._touchMoved && event.target !== this.node) {
// Simulate touch cancel for target node
const cancelEvent = new EventTouch(event.getTouches(), event.bubbles, SystemEventType.TOUCH_CANCEL);
cancelEvent.touch = event.touch;
cancelEvent.simulate = true;
(event.target as Node).dispatchEvent(cancelEvent);
this._touchMoved = true;
}
}
this._stopPropagationIfTargetIsMe(event);
}
protected _onTouchEnded (event: EventTouch, captureListeners?: Node[]) {
if (!this.enabledInHierarchy || !this._content || !event) { return; }
if (this._hasNestedViewGroup(event, captureListeners)) { return; }
this._dispatchEvent(EventType.TOUCH_UP);
const touch = event.touch!;
this._handleReleaseLogic(touch);
if (this._touchMoved) {
event.propagationStopped = true;
} else {
this._stopPropagationIfTargetIsMe(event);
}
}
protected _onTouchCancelled (event: EventTouch, captureListeners?: Node[]) {
if (!this.enabledInHierarchy || !this._content) { return; }
if (this._hasNestedViewGroup(event, captureListeners)) { return; }
// Filter touch cancel event send from self
if (event && !event.simulate) {
const touch = event.touch!;
this._handleReleaseLogic(touch);
}
this._stopPropagationIfTargetIsMe(event);
}
protected _calculateBoundary () {
if (this._content && this.view) {
// refresh content size
const layout = this._content.getComponent(Layout);
if (layout && layout.enabledInHierarchy) {
layout.updateLayout();
}
const viewTrans = this.view;
const anchorX = viewTrans.width * viewTrans.anchorX;
const anchorY = viewTrans.height * viewTrans.anchorY;
this._leftBoundary = -anchorX;
this._bottomBoundary = -anchorY;
this._rightBoundary = this._leftBoundary + viewTrans.width;
this._topBoundary = this._bottomBoundary + viewTrans.height;
this._moveContentToTopLeft(viewTrans.contentSize);
}
}
protected _hasNestedViewGroup (event: Event, captureListeners?: Node[]) {
if (!event || event.eventPhase !== Event.CAPTURING_PHASE) {
return false;
}
if (captureListeners) {
// captureListeners are arranged from child to parent
for (const listener of captureListeners) {
const item = listener;
if (this.node === item) {
if (event.target && (event.target as Node).getComponent(ViewGroup)) {
return true;
}
return false;
}
if (item.getComponent(ViewGroup)) {
return true;
}
}
}
return false;
}
protected _startInertiaScroll (touchMoveVelocity: Vec3) {
const inertiaTotalMovement = new Vec3(touchMoveVelocity);
inertiaTotalMovement.multiplyScalar(MOVEMENT_FACTOR);
this._startAttenuatingAutoScroll(inertiaTotalMovement, touchMoveVelocity);
}
protected _calculateAttenuatedFactor (distance: number) {
if (this.brake <= 0) {
return (1 - this.brake);
}
// attenuate formula from: http://learnopengl.com/#!Lighting/Light-casters
return (1 - this.brake) * (1 / (1 + distance * 0.000014 + distance * distance * 0.000000008));
}
protected _startAttenuatingAutoScroll (deltaMove: Vec3, initialVelocity: Vec3) {
const targetDelta = deltaMove.clone();
targetDelta.normalize();
if (this._content && this.view) {
const contentSize = this._content._uiProps.uiTransformComp!.contentSize;
const scrollViewSize = this.view.contentSize;
const totalMoveWidth = (contentSize.width - scrollViewSize.width);
const totalMoveHeight = (contentSize.height - scrollViewSize.height);
const attenuatedFactorX = this._calculateAttenuatedFactor(totalMoveWidth);
const attenuatedFactorY = this._calculateAttenuatedFactor(totalMoveHeight);
targetDelta.x = targetDelta.x * totalMoveWidth * (1 - this.brake) * attenuatedFactorX;
targetDelta.y = targetDelta.y * totalMoveHeight * attenuatedFactorY * (1 - this.brake);
targetDelta.z = 0;
}
const originalMoveLength = deltaMove.length();
let factor = targetDelta.length() / originalMoveLength;
targetDelta.add(deltaMove);
if (this.brake > 0 && factor > 7) {
factor = Math.sqrt(factor);
const clonedDeltaMove = deltaMove.clone();
clonedDeltaMove.multiplyScalar(factor);
targetDelta.set(clonedDeltaMove);
targetDelta.add(deltaMove);
}
let time = this._calculateAutoScrollTimeByInitialSpeed(initialVelocity.length());
if (this.brake > 0 && factor > 3) {
factor = 3;
time *= factor;
}
if (this.brake === 0 && factor > 1) {
time *= factor;
}
this._startAutoScroll(targetDelta, time, true);
}
protected _calculateAutoScrollTimeByInitialSpeed (initialSpeed: number) {
return Math.sqrt(Math.sqrt(initialSpeed / 5));
}
protected _startAutoScroll (deltaMove: Vec3, timeInSecond: number, attenuated = false) {
const adjustedDeltaMove = this._flattenVectorByDirection(deltaMove);
this._autoScrolling = true;
this._autoScrollTargetDelta = adjustedDeltaMove;
this._autoScrollAttenuate = attenuated;
Vec3.copy(this._autoScrollStartPosition, this._getContentPosition());
this._autoScrollTotalTime = timeInSecond;
this._autoScrollAccumulatedTime = 0;
this._autoScrollBraking = false;
this._isScrollEndedWithThresholdEventFired = false;
this._autoScrollBrakingStartPosition.set(0, 0, 0);
const currentOutOfBoundary = this._getHowMuchOutOfBoundary();
if (!currentOutOfBoundary.equals(Vec3.ZERO, EPSILON)) {
this._autoScrollCurrentlyOutOfBoundary = true;
}
}
protected _calculateTouchMoveVelocity () {
const out = new Vec3();
let totalTime = 0;
totalTime = this._touchMoveTimeDeltas.reduce((a, b) => a + b, totalTime);
if (totalTime <= 0 || totalTime >= 0.5) {
out.set(Vec3.ZERO);
} else {
let totalMovement = new Vec3();
totalMovement = this._touchMoveDisplacements.reduce((a, b) => {
a.add(b);
return a;
}, totalMovement);
out.set(totalMovement.x * (1 - this.brake) / totalTime,
totalMovement.y * (1 - this.brake) / totalTime, totalMovement.z);
}
return out;
}
protected _flattenVectorByDirection (vector: Vec3) {
const result = vector;
result.x = this.horizontal ? result.x : 0;
result.y = this.vertical ? result.y : 0;
return result;
}
protected _moveContent (deltaMove: Vec3, canStartBounceBack?: boolean) {
const adjustedMove = this._flattenVectorByDirection(deltaMove);
_tempVec3.set(this._getContentPosition());
_tempVec3.add(adjustedMove);
_tempVec3.set(Math.round(_tempVec3.x * TOLERANCE) * EPSILON, Math.round(_tempVec3.y * TOLERANCE) * EPSILON, _tempVec3.z);
this._setContentPosition(_tempVec3);
const outOfBoundary = this._getHowMuchOutOfBoundary();
_tempVec2.set(outOfBoundary.x, outOfBoundary.y);
this._updateScrollBar(_tempVec2);
if (this.elastic && canStartBounceBack) {
this._startBounceBackIfNeeded();
}
}
protected _getContentLeftBoundary () {
if (!this._content) {
return -1;
}
const contentPos = this._getContentPosition();
const uiTrans = this._content._uiProps.uiTransformComp!;
return contentPos.x - uiTrans.anchorX * uiTrans.width;
}
protected _getContentRightBoundary () {
if (!this._content) {
return -1;
}
const uiTrans = this._content._uiProps.uiTransformComp!;
return this._getContentLeftBoundary() + uiTrans.width;
}
protected _getContentTopBoundary () {
if (!this._content) {
return -1;
}
const uiTrans = this._content._uiProps.uiTransformComp!;
return this._getContentBottomBoundary() + uiTrans.height;
}
protected _getContentBottomBoundary () {
if (!this._content) {
return -1;
}
const contentPos = this._getContentPosition();
const uiTrans = this._content._uiProps.uiTransformComp!;
return contentPos.y - uiTrans.anchorY * uiTrans.height;
}
protected _getHowMuchOutOfBoundary (addition?: Vec3) {
addition = addition || new Vec3();
if (addition.equals(Vec3.ZERO, EPSILON) && !this._outOfBoundaryAmountDirty) {
return this._outOfBoundaryAmount;
}
const outOfBoundaryAmount = new Vec3();
const tempLeftBoundary: number = this._getContentLeftBoundary();
const tempRightBoundary: number = this._getContentRightBoundary();
if (tempLeftBoundary + addition.x > this._leftBoundary) {
outOfBoundaryAmount.x = this._leftBoundary - (tempLeftBoundary + addition.x);
} else if (tempRightBoundary + addition.x < this._rightBoundary) {
outOfBoundaryAmount.x = this._rightBoundary - (tempRightBoundary + addition.x);
}
const tempTopBoundary: number = this._getContentTopBoundary();
const tempBottomBoundary: number = this._getContentBottomBoundary();
if (tempTopBoundary + addition.y < this._topBoundary) {
outOfBoundaryAmount.y = this._topBoundary - (tempTopBoundary + addition.y);
} else if (tempBottomBoundary + addition.y > this._bottomBoundary) {
outOfBoundaryAmount.y = this._bottomBoundary - (tempBottomBoundary + addition.y);
}
if (addition.equals(Vec3.ZERO, EPSILON)) {
this._outOfBoundaryAmount = outOfBoundaryAmount;
this._outOfBoundaryAmountDirty = false;
}
this._clampDelta(outOfBoundaryAmount);
return outOfBoundaryAmount;
}
protected _updateScrollBar (outOfBoundary: Vec2 | Readonly<Vec2>) {
if (this._horizontalScrollBar) {
this._horizontalScrollBar.onScroll(outOfBoundary);
}
if (this.verticalScrollBar) {
this.verticalScrollBar.onScroll(outOfBoundary);
}
}
protected _onScrollBarTouchBegan () {
if (this._horizontalScrollBar) {
this._horizontalScrollBar.onTouchBegan();
}
if (this.verticalScrollBar) {
this.verticalScrollBar.onTouchBegan();
}
}
protected _onScrollBarTouchEnded () {
if (this._horizontalScrollBar) {
this._horizontalScrollBar.onTouchEnded();
}
if (this.verticalScrollBar) {
this.verticalScrollBar.onTouchEnded();
}
}
protected _dispatchEvent (event: string) {
if (event === EventType.SCROLL_ENDED) {
this._scrollEventEmitMask = 0;
} else if (event === EventType.SCROLL_TO_TOP
|| event === EventType.SCROLL_TO_BOTTOM
|| event === EventType.SCROLL_TO_LEFT
|| event === EventType.SCROLL_TO_RIGHT) {
const flag = (1 << eventMap[event]);
if (this._scrollEventEmitMask & flag) {
return;
} else {
this._scrollEventEmitMask |= flag;
}
}
ComponentEventHandler.emitEvents(this.scrollEvents, this, eventMap[event]);
this.node.emit(event, this);
}
protected _adjustContentOutOfBoundary () {
if (!this._content) {
return;
}
this._outOfBoundaryAmountDirty = true;
if (this._isOutOfBoundary()) {
const outOfBoundary = this._getHowMuchOutOfBoundary();
_tempVec3.set(this._getContentPosition());
_tempVec3.add(outOfBoundary);
this._content.setPosition(_tempVec3);
this._updateScrollBar(Vec2.ZERO);
}
}
protected _hideScrollBar () {
if (this._horizontalScrollBar) {
this._horizontalScrollBar.hide();
}
if (this._verticalScrollBar) {
this._verticalScrollBar.hide();
}
}
protected _updateScrollBarState () {
if (!this._content || !this.view) {
return;
}
const viewTrans = this.view;
const uiTrans = this._content._uiProps.uiTransformComp!;
if (this.verticalScrollBar) {
if (uiTrans.height < viewTrans.height) {
this.verticalScrollBar.hide();
} else {
this.verticalScrollBar.show();
}
}
if (this.horizontalScrollBar) {
if (uiTrans.width < viewTrans.width) {
this.horizontalScrollBar.hide();
} else {
this.horizontalScrollBar.show();
}
}
}
// This is for ScrollView as children of a Button
protected _stopPropagationIfTargetIsMe (event: Event) {
if (event.eventPhase === Event.AT_TARGET && event.target === this.node) {
event.propagationStopped = true;
}
}
protected _processDeltaMove (deltaMove: Vec3) {
this._scrollChildren(deltaMove);
this._gatherTouchMove(deltaMove);
}
protected _handleMoveLogic (touch: Touch) {
this._getLocalAxisAlignDelta(this._deltaPos, touch);
this._processDeltaMove(this._deltaPos);
}
protected _handleReleaseLogic (touch: Touch) {
this._getLocalAxisAlignDelta(this._deltaPos, touch);
this._gatherTouchMove(this._deltaPos);
this._processInertiaScroll();
if (this._scrolling) {
this._scrolling = false;
if (!this._autoScrolling) {
this._dispatchEvent(EventType.SCROLL_ENDED);
}
}
}
protected _getLocalAxisAlignDelta (out: Vec3, touch: Touch) {
const uiTransformComp = this.node._uiProps.uiTransformComp;
const vec = new Vec3();
if (uiTransformComp) {
touch.getUILocation(_tempVec2);
touch.getUIPreviousLocation(_tempVec2_1);
_tempVec3.set(_tempVec2.x, _tempVec2.y, 0);
_tempVec3_1.set(_tempVec2_1.x, _tempVec2_1.y, 0);
uiTransformComp.convertToNodeSpaceAR(_tempVec3, _tempVec3);
uiTransformComp.convertToNodeSpaceAR(_tempVec3_1, _tempVec3_1);
Vec3.subtract(vec, _tempVec3, _tempVec3_1);
}
out.set(vec);
}
protected _scrollChildren (deltaMove: Vec3) {
this._clampDelta(deltaMove);
const realMove = deltaMove;
let outOfBoundary: Vec3;
if (this.elastic) {
outOfBoundary = this._getHowMuchOutOfBoundary();
realMove.x *= (outOfBoundary.x === 0 ? 1 : 0.5);
realMove.y *= (outOfBoundary.y === 0 ? 1 : 0.5);
}
if (!this.elastic) {
outOfBoundary = this._getHowMuchOutOfBoundary(realMove);
realMove.add(outOfBoundary);
}
let verticalScrollEventType = '';
let horizontalScrollEventType = '';
if (this._content) {
const { anchorX, anchorY, width, height } = this._content._uiProps.uiTransformComp!;
const pos = this._content.position || Vec3.ZERO;
if (this.vertical) {
if (realMove.y > 0) { // up
const icBottomPos = pos.y - anchorY * height;
if (icBottomPos + realMove.y >= this._bottomBoundary) {
verticalScrollEventType = EventType.SCROLL_TO_BOTTOM;
}
} else if (realMove.y < 0) { // down
const icTopPos = pos.y - anchorY * height + height;
if (icTopPos + realMove.y <= this._topBoundary) {
verticalScrollEventType = EventType.SCROLL_TO_TOP;
}
}
}
if (this.horizontal) {
if (realMove.x < 0) { // left
const icRightPos = pos.x - anchorX * width + width;
if (icRightPos + realMove.x <= this._rightBoundary) {
horizontalScrollEventType = EventType.SCROLL_TO_RIGHT;
}
} else if (realMove.x > 0) { // right
const icLeftPos = pos.x - anchorX * width;
if (icLeftPos + realMove.x >= this._leftBoundary) {
horizontalScrollEventType = EventType.SCROLL_TO_LEFT;
}
}
}
}
this._moveContent(realMove, false);
if ((this.horizontal && realMove.x !== 0) || (this.vertical && realMove.y !== 0)) {
if (!this._scrolling) {
this._scrolling = true;
this._dispatchEvent(EventType.SCROLL_BEGAN);
}
this._dispatchEvent(EventType.SCROLLING);
}
if (verticalScrollEventType !== '') {
this._dispatchEvent(verticalScrollEventType);
}
if (horizontalScrollEventType !== '') {
this._dispatchEvent(horizontalScrollEventType);
}
}
protected _handlePressLogic () {
if (this._autoScrolling) {
this._dispatchEvent(EventType.SCROLL_ENDED);
}
this._autoScrolling = false;
this._isBouncing = false;
this._touchMovePreviousTimestamp = getTimeInMilliseconds();
this._touchMoveDisplacements.length = 0;
this._touchMoveTimeDeltas.length = 0;
this._onScrollBarTouchBegan();
}
protected _clampDelta (out: Vec3) {
if (this._content && this.view) {
const scrollViewSize = this.view.contentSize;
const uiTrans = this._content._uiProps.uiTransformComp!;
if (uiTrans.width < scrollViewSize.width) {
out.x = 0;
}
if (uiTrans.height < scrollViewSize.height) {
out.y = 0;
}
}
}
protected _gatherTouchMove (delta: Vec3) {
const clampDt = delta.clone();
this._clampDelta(clampDt);
while (this._touchMoveDisplacements.length >= NUMBER_OF_GATHERED_TOUCHES_FOR_MOVE_SPEED) {
this._touchMoveDisplacements.shift();
this._touchMoveTimeDeltas.shift();
}
this._touchMoveDisplacements.push(clampDt);
const timeStamp = getTimeInMilliseconds();
this._touchMoveTimeDeltas.push((timeStamp - this._touchMovePreviousTimestamp) / 1000);
this._touchMovePreviousTimestamp = timeStamp;
}
protected _startBounceBackIfNeeded () {
if (!this.elastic) {
return false;
}
const bounceBackAmount = this._getHowMuchOutOfBoundary();
this._clampDelta(bounceBackAmount);
if (bounceBackAmount.equals(Vec3.ZERO, EPSILON)) {
return false;
}
const bounceBackTime = Math.max(this.bounceDuration, 0);
this._startAutoScroll(bounceBackAmount, bounceBackTime, true);
if (!this._isBouncing) {
if (bounceBackAmount.y > 0) { this._dispatchEvent(EventType.BOUNCE_TOP); }
if (bounceBackAmount.y < 0) { this._dispatchEvent(EventType.BOUNCE_BOTTOM); }
if (bounceBackAmount.x > 0) { this._dispatchEvent(EventType.BOUNCE_RIGHT); }
if (bounceBackAmount.x < 0) { this._dispatchEvent(EventType.BOUNCE_LEFT); }
this._isBouncing = true;
}
return true;
}
protected _processInertiaScroll () {
const bounceBackStarted = this._startBounceBackIfNeeded();
if (!bounceBackStarted && this.inertia) {
const touchMoveVelocity = this._calculateTouchMoveVelocity();
if (!touchMoveVelocity.equals(_tempVec3, EPSILON) && this.brake < 1) {
this._startInertiaScroll(touchMoveVelocity);
}
}
this._onScrollBarTouchEnded();
}
protected _isOutOfBoundary () {
const outOfBoundary = this._getHowMuchOutOfBoundary();
return !outOfBoundary.equals(Vec3.ZERO, EPSILON);
}
protected _isNecessaryAutoScrollBrake () {
if (this._autoScrollBraking) {
return true;
}
if (this._isOutOfBoundary()) {
if (!this._autoScrollCurrentlyOutOfBoundary) {
this._autoScrollCurrentlyOutOfBoundary = true;
this._autoScrollBraking = true;
Vec3.copy(this._autoScrollBrakingStartPosition, this._getContentPosition());
return true;
}
} else {
this._autoScrollCurrentlyOutOfBoundary = false;
}
return false;
}
protected _processAutoScrolling (dt) {
const isAutoScrollBrake = this._isNecessaryAutoScrollBrake();
const brakingFactor = isAutoScrollBrake ? OUT_OF_BOUNDARY_BREAKING_FACTOR : 1;
this._autoScrollAccumulatedTime += dt * (1 / brakingFactor);
let percentage = Math.min(1, this._autoScrollAccumulatedTime / this._autoScrollTotalTime);
if (this._autoScrollAttenuate) {
percentage = quintEaseOut(percentage);
}
const clonedAutoScrollTargetDelta = this._autoScrollTargetDelta.clone();
clonedAutoScrollTargetDelta.multiplyScalar(percentage);
const clonedAutoScrollStartPosition = this._autoScrollStartPosition.clone();
clonedAutoScrollStartPosition.add(clonedAutoScrollTargetDelta);
let reachedEnd = Math.abs(percentage - 1) <= EPSILON;
const fireEvent = Math.abs(percentage - 1) <= this.getScrollEndedEventTiming();
if (fireEvent && !this._isScrollEndedWithThresholdEventFired) {
this._dispatchEvent(EventType.SCROLL_ENG_WITH_THRESHOLD);
this._isScrollEndedWithThresholdEventFired = true;
}
if (this.elastic) {
const brakeOffsetPosition = clonedAutoScrollStartPosition.clone();
brakeOffsetPosition.subtract(this._autoScrollBrakingStartPosition);
if (isAutoScrollBrake) {
brakeOffsetPosition.multiplyScalar(brakingFactor);
}
clonedAutoScrollStartPosition.set(this._autoScrollBrakingStartPosition);
clonedAutoScrollStartPosition.add(brakeOffsetPosition);
} else {
const moveDelta = clonedAutoScrollStartPosition.clone();
moveDelta.subtract(this.getContentPosition());
const outOfBoundary = this._getHowMuchOutOfBoundary(moveDelta);
if (!outOfBoundary.equals(Vec3.ZERO, EPSILON)) {
clonedAutoScrollStartPosition.add(outOfBoundary);
reachedEnd = true;
}
}
if (reachedEnd) {
this._autoScrolling = false;
}
const deltaMove = clonedAutoScrollStartPosition.clone();
deltaMove.subtract(this._getContentPosition());
this._clampDelta(deltaMove);
this._moveContent(deltaMove, reachedEnd);
this._dispatchEvent(EventType.SCROLLING);
if (!this._autoScrolling) {
this._isBouncing = false;
this._scrolling = false;
this._dispatchEvent(EventType.SCROLL_ENDED);
}
}
protected _checkMouseWheel (dt: number) {
const currentOutOfBoundary = this._getHowMuchOutOfBoundary();
const maxElapsedTime = 0.1;
if (!currentOutOfBoundary.equals(Vec3.ZERO, EPSILON)) {
this._processInertiaScroll();
this.unschedule(this._checkMouseWheel);
this._dispatchEvent(EventType.SCROLL_ENDED);
this._stopMouseWheel = false;
return;
}
this._mouseWheelEventElapsedTime += dt;
// mouse wheel event is ended
if (this._mouseWheelEventElapsedTime > maxElapsedTime) {
this._onScrollBarTouchEnded();
this.unschedule(this._checkMouseWheel);
this._dispatchEvent(EventType.SCROLL_ENDED);
this._stopMouseWheel = false;
}
}
protected _calculateMovePercentDelta (options) {
const anchor = options.anchor;
const applyToHorizontal = options.applyToHorizontal;
const applyToVertical = options.applyToVertical;
this._calculateBoundary();
anchor.clampf(Vec2.ZERO, Vec2.ONE);
let bottomDelta = this._getContentBottomBoundary() - this._bottomBoundary;
bottomDelta = -bottomDelta;
let leftDelta = this._getContentLeftBoundary() - this._leftBoundary;
leftDelta = -leftDelta;
const moveDelta = new Vec3();
if (this._content && this.view) {
let totalScrollDelta = 0;
const uiTrans = this._content._uiProps.uiTransformComp!;
const contentSize = uiTrans.contentSize;
const scrollSize = this.view.contentSize;
if (applyToHorizontal) {
totalScrollDelta = contentSize.width - scrollSize.width;
moveDelta.x = leftDelta - totalScrollDelta * anchor.x;
}
if (applyToVertical) {
totalScrollDelta = contentSize.height - scrollSize.height;
moveDelta.y = bottomDelta - totalScrollDelta * anchor.y;
}
}
return moveDelta;
}
protected _moveContentToTopLeft (scrollViewSize: Size) {
let bottomDelta = this._getContentBottomBoundary() - this._bottomBoundary;
bottomDelta = -bottomDelta;
const moveDelta = new Vec3();
let totalScrollDelta = 0;
let leftDelta = this._getContentLeftBoundary() - this._leftBoundary;
leftDelta = -leftDelta;
// 是否限制在上视区上边
if (this._content) {
const uiTrans = this._content._uiProps.uiTransformComp!;
const contentSize = uiTrans.contentSize;
if (contentSize.height < scrollViewSize.height) {
totalScrollDelta = contentSize.height - scrollViewSize.height;
moveDelta.y = bottomDelta - totalScrollDelta;
}
// 是否限制在上视区左边
if (contentSize.width < scrollViewSize.width) {
totalScrollDelta = contentSize.width - scrollViewSize.width;
moveDelta.x = leftDelta;
}
}
this._updateScrollBarState();
this._moveContent(moveDelta);
this._adjustContentOutOfBoundary();
}
protected _scaleChanged (value: TransformBit) {
if (value === TransformBit.SCALE) {
this._calculateBoundary();
}
}
}
/**
* @en
* Note: This event is emitted from the node to which the component belongs.
* @zh
* 注意:此事件是从该组件所属的 Node 上面派发出来的,需要用 node.on 来监听。
* @event scroll-to-top
* @param {Event.EventCustom} event
* @param {ScrollView} scrollView - The ScrollView component.
*/
/**
* @en
* Note: This event is emitted from the node to which the component belongs.
* @zh
* 注意:此事件是从该组件所属的 Node 上面派发出来的,需要用 node.on 来监听。
* @event scroll-to-bottom
* @param {Event.EventCustom} event
* @param {ScrollView} scrollView - The ScrollView component.
*/
/**
* @en
* Note: This event is emitted from the node to which the component belongs.
* @zh
* 注意:此事件是从该组件所属的 Node 上面派发出来的,需要用 node.on 来监听。
* @event scroll-to-left
* @param {Event.EventCustom} event
* @param {ScrollView} scrollView - The ScrollView component.
*/
/**
* @en
* Note: This event is emitted from the node to which the component belongs.
* @zh
* 注意:此事件是从该组件所属的 Node 上面派发出来的,需要用 node.on 来监听。
* @event scroll-to-right
* @param {Event.EventCustom} event
* @param {ScrollView} scrollView - The ScrollView component.
*/
/**
* @en
* Note: This event is emitted from the node to which the component belongs.
* @zh
* 注意:此事件是从该组件所属的 Node 上面派发出来的,需要用 node.on 来监听。
* @event scrolling
* @param {Event.EventCustom} event
* @param {ScrollView} scrollView - The ScrollView component.
*/
/**
* @en
* Note: This event is emitted from the node to which the component belongs.
* @zh
* 注意:此事件是从该组件所属的 Node 上面派发出来的,需要用 node.on 来监听。
* @event bounce-bottom
* @param {Event.EventCustom} event
* @param {ScrollView} scrollView - The ScrollView component.
*/
/**
* @en
* Note: This event is emitted from the node to which the component belongs.
* @zh
* 注意:此事件是从该组件所属的 Node 上面派发出来的,需要用 node.on 来监听。
* @event bounce-top
* @param {Event.EventCustom} event
* @param {ScrollView} scrollView - The ScrollView component.
*/
/**
* @en
* Note: This event is emitted from the node to which the component belongs.
* @zh
* 注意:此事件是从该组件所属的 Node 上面派发出来的,需要用 node.on 来监听。
* @event bounce-left
* @param {Event.EventCustom} event
* @param {ScrollView} scrollView - The ScrollView component.
*/
/**
* @en
* Note: This event is emitted from the node to which the component belongs.
* @zh
* 注意:此事件是从该组件所属的 Node 上面派发出来的,需要用 node.on 来监听。
* @event bounce-right
* @param {Event.EventCustom} event
* @param {ScrollView} scrollView - The ScrollView component.
*/
/**
* @en
* Note: This event is emitted from the node to which the component belongs.
* @zh
* 注意:此事件是从该组件所属的 Node 上面派发出来的,需要用 node.on 来监听。
* @event scroll-ended
* @param {Event.EventCustom} event
* @param {ScrollView} scrollView - The ScrollView component.
*/
/**
* @en
* Note: This event is emitted from the node to which the component belongs.
* @zh
* 注意:此事件是从该组件所属的 Node 上面派发出来的,需要用 node.on 来监听。
* @event touch-up
* @param {Event.EventCustom} event
* @param {ScrollView} scrollView - The ScrollView component.
*/
/**
* @en
* Note: This event is emitted from the node to which the component belongs.
* @zh
* 注意:此事件是从该组件所属的 Node 上面派发出来的,需要用 node.on 来监听。
* @event scroll-began
* @param {Event.EventCustom} event
* @param {ScrollView} scrollView - The ScrollView component.
*/
legacyCC.ScrollView = ScrollView;