滚动视图
滚动视图仅显示较大内容的一部分,用户可拖动或滚动鼠标滚轮查看其余内容。列表、背包、长文本和地图都可以使用,它由多个实体组成:
scroll view 组元素,并带滚动视图组件
├── viewport 作为遮罩的图像元素:可见区域
│ └── content 大于视口的元素,启用输入
│ └── … 滚动的内容项
└── scrollbar 图像元素,并带滚动条组件(可选)
└── handle 启用输入的图像元素,可沿滚动条拖动
视口是遮罩,使内容仅在其内绘制。内容应启用输入,便于从空白区域拖动。
滚动视图通过修改内容局部位置进行滚动。滚到左上角时,位置为 0, 0。因此内容应锚定视口左上角或沿顶部横向锚定,轴心设为左上 [0, 1]。其他轴心会使初始内容部分超出视口。
创建滚动视图
以下 300 × 400 的滚动视图垂直滚动更高的一列内容,右侧带滚动条:
- 引擎
- 编辑器
- React
- Web Components
// The scroll view, 300 × 400 in the middle of the screen
const scrollView = new pc.Entity('scroll view');
scrollView.addComponent('element', {
type: pc.ELEMENTTYPE_GROUP,
anchor: [0.5, 0.5, 0.5, 0.5],
pivot: [0.5, 0.5],
width: 300,
height: 400
});
screen.addChild(scrollView);
// The viewport fills the scroll view, apart from 20 units on the right for the scrollbar
const viewport = new pc.Entity('viewport');
viewport.addComponent('element', {
type: pc.ELEMENTTYPE_IMAGE,
anchor: [0, 0, 1, 1],
margin: [0, 0, 20, 0],
mask: true
});
scrollView.addChild(viewport);
// The content hangs from the top-left corner of the viewport
const content = new pc.Entity('content');
content.addComponent('element', {
type: pc.ELEMENTTYPE_GROUP,
anchor: [0, 1, 0, 1],
pivot: [0, 1],
width: 280,
height: 1200,
useInput: true
});
viewport.addChild(content);
// A scrollbar along the right edge, and its handle
const scrollbar = new pc.Entity('scrollbar');
scrollbar.addComponent('element', {
type: pc.ELEMENTTYPE_IMAGE,
anchor: [1, 0, 1, 1],
pivot: [1, 1],
margin: [0, 0, 0, 0],
width: 20,
color: new pc.Color(0.16, 0.18, 0.23)
});
scrollView.addChild(scrollbar);
const handle = new pc.Entity('handle');
handle.addComponent('element', {
type: pc.ELEMENTTYPE_IMAGE,
anchor: [0, 1, 1, 1],
pivot: [1, 1],
margin: [0, 0, 0, 0],
color: new pc.Color(0.5, 0.55, 0.65),
useInput: true
});
scrollbar.addChild(handle);
scrollbar.addComponent('scrollbar', {
orientation: pc.ORIENTATION_VERTICAL,
handleEntity: handle
});
scrollView.addComponent('scrollview', {
viewportEntity: viewport,
contentEntity: content,
verticalScrollbarEntity: scrollbar,
horizontal: false,
vertical: true,
scrollMode: pc.SCROLL_MODE_BOUNCE,
bounceAmount: 0.1,
friction: 0.05
});
创建应用时注册 pc.ScrollViewComponentSystem 和 pc.ScrollbarComponentSystem,并提供 pc.Mouse 以支持鼠标拖动。代码创建的滚动视图没有完整默认设置:需设置 horizontal 或 vertical 才启用对应轴;未设置 scrollMode、bounceAmount 和 friction 时会记录警告、无法回弹,释放后立即停止。应如示例一样全部传入。
在层级中选择 + › User Interface › Scroll View,会创建完整结构:带 Scroll View 组件的 ScrollView、作为遮罩的 Viewport、启用 Use Input 的 Content,以及各自带 Handle 的 HorizontalScrollbar 和 VerticalScrollbar。组件自动引用这些实体。
将条目放在 Content 下,并设置内容尺寸以容纳它们。只需要单轴滚动时,取消 Horizontal 或 Vertical,删除不需要的滚动条,并清空组件对应字段。
@playcanvas/react 尚无滚动视图和滚动条组件,因此此处的 ScrollView 与 Scrollbar 辅助组件会添加引擎组件,并通过 ref 引用所需实体:
import { useEffect, useRef } from 'react';
import { ORIENTATION_VERTICAL, SCROLL_MODE_BOUNCE } from 'playcanvas';
import { Entity } from '@playcanvas/react';
import { Element } from '@playcanvas/react/components';
import { useParent } from '@playcanvas/react/hooks';
function ScrollView({ viewport, content, verticalScrollbar, ...options }) {
const entity = useParent();
useEffect(() => {
entity.addComponent('scrollview', {
viewportEntity: viewport.current,
contentEntity: content.current,
verticalScrollbarEntity: verticalScrollbar?.current ?? null,
...options
});
return () => entity.removeComponent('scrollview');
}, [entity]);
return null;
}
function Scrollbar({ handle, ...options }) {
const entity = useParent();
useEffect(() => {
entity.addComponent('scrollbar', { handleEntity: handle.current, ...options });
return () => entity.removeComponent('scrollbar');
}, [entity]);
return null;
}
export function ItemList({ items }) {
const viewport = useRef(null);
const content = useRef(null);
const scrollbar = useRef(null);
const handle = useRef(null);
return (
<Entity name="scroll view">
<Element type="group" anchor={[0.5, 0.5, 0.5, 0.5]} pivot={[0.5, 0.5]} width={300} height={400} />
<Entity name="viewport" ref={viewport}>
<Element type="image" anchor={[0, 0, 1, 1]} margin={[0, 0, 20, 0]} mask />
<Entity name="content" ref={content}>
<Element type="group" anchor={[0, 1, 0, 1]} pivot={[0, 1]} width={280} height={items.length * 70} useInput />
{items.map((item, i) => (
<Entity key={item.id} name={item.name} position={[10, -10 - i * 70, 0]}>
<Element type="image" anchor={[0, 1, 0, 1]} pivot={[0, 1]} width={260} height={60} color="#3a8cff" />
</Entity>
))}
</Entity>
</Entity>
<Entity name="scrollbar" ref={scrollbar}>
<Element type="image" anchor={[1, 0, 1, 1]} pivot={[1, 1]} margin={[0, 0, 0, 0]} width={20} color="#292e3b" />
<Entity name="handle" ref={handle}>
<Element type="image" anchor={[0, 1, 1, 1]} pivot={[1, 1]} margin={[0, 0, 0, 0]} color="#808ca6" useInput />
</Entity>
<Scrollbar handle={handle} orientation={ORIENTATION_VERTICAL} />
</Entity>
<ScrollView viewport={viewport} content={content} verticalScrollbar={scrollbar}
horizontal={false} vertical scrollMode={SCROLL_MODE_BOUNCE} bounceAmount={0.1} friction={0.05} />
</Entity>
);
}
ItemList 为每个 items 创建一行,并让内容高度等于这些行的总高度。
<pc-entity name="scroll view">
<pc-element type="group" anchor="0.5 0.5 0.5 0.5" pivot="0.5 0.5" width="300" height="400"></pc-element>
<pc-scroll-view viewport="viewport" content="content" vertical-scrollbar="scrollbar" horizontal="false"></pc-scroll-view>
<pc-entity name="viewport">
<pc-element type="image" anchor="0 0 1 1" margin="0 0 20 0" mask></pc-element>
<pc-entity name="content">
<pc-element type="group" anchor="0 1 0 1" pivot="0 1" width="280" height="1200" use-input></pc-element>
</pc-entity>
</pc-entity>
<pc-entity name="scrollbar">
<pc-element type="image" anchor="1 0 1 1" pivot="1 1" margin="0 0 0 0" width="20" color="#292e3b"></pc-element>
<pc-scrollbar orientation="vertical" handle="handle"></pc-scrollbar>
<pc-entity name="handle">
<pc-element type="image" anchor="0 1 1 1" pivot="1 1" margin="0 0 0 0" color="#808ca6" use-input></pc-element>
</pc-entity>
</pc-entity>
</pc-entity>
<pc-scroll-view> 默认双轴滚动,采用 Bounce 模式。设置 horizontal="false" 可限制为垂直滚动,它通过名称查找所需实体。
滚动行为
| 属性 | 效果 |
|---|---|
horizontal, vertical | 允许滚动的轴 |
scrollMode | Clamp 在边界停止;Bounce 允许超出并弹回;Infinite 允许无限滚动 |
bounceAmount | Bounce 的回弹缓慢程度,0 为立即返回,0.1 类似手机滚动,更大值回弹更慢 |
friction | 甩动后减速快慢,范围为 0 到 1,1 为立即停止 |
dragThreshold | 拖动多少屏幕单位后停止内部元素输入,默认为 10 |
useMouseWheel, mouseWheelSensitivity | 是否使用滚轮,以及各轴灵敏度 |
拖动开始时,内容立即跟随指针。移动超过 dragThreshold 后,内部元素停止接收输入直到拖动结束,避免滚列表时误按按钮;几乎不移动的按下仍可触发按钮。指针位于视图内启用输入的元素,例如内容区域时,滚轮会滚动内容,且不产生回弹。
滚动条
滚动条由作为轨道的图像元素、Scrollbar 组件和滑块子项组成。视图根据内容可见比例设置滑块大小,根据滚动位置设置滑块位置,拖动滑块即可滚动。Visibility,即 horizontalScrollbarVisibility 和 verticalScrollbarVisibility,可在内容放得下时隐藏滚动条:
pc.SCROLLBAR_VISIBILITY_SHOW_ALWAYS始终显示,默认值。pc.SCROLLBAR_VISIBILITY_SHOW_WHEN_REQUIRED在内容不大于视口时隐藏。
滚动条也能独立作为滑块使用,详见滑块。
内容尺寸
内容尺寸决定可滚范围,滚动视图不会自动调整。如果内容实体比条目总高度短,就无法滚到末尾。使用布局组排列时,可监听 reflow,根据每次布局后返回的子项边界设置尺寸:
content.addComponent('layoutgroup', {
orientation: pc.ORIENTATION_VERTICAL,
spacing: [0, 10],
padding: [10, 10, 10, 10],
widthFitting: pc.FITTING_STRETCH
});
content.layoutgroup.on('reflow', ({ bounds }) => {
// Make the content as tall as its rows, plus the top and bottom padding
content.element.height = bounds.w + 20;
});
按上述方式将内容锚定顶部并使用顶部轴心后,内容变高会向下增长,首行位置保持不变。
通过代码滚动
scroll 是 pc.Vec2 滚动位置,各轴范围为 0 到 1。垂直方向上,0 为顶部,1 为底部:
// Scroll to the end of a chat log
scrollView.scrollview.scroll = new pc.Vec2(0, 1);
// React to the user scrolling
scrollView.scrollview.on('set:scroll', (scroll) => {
loadMoreButton.enabled = scroll.y > 0.95;
});
另请参阅
- 遮罩:视口如何裁切内容。
- 布局组:排列列表条目。
- 动态滚动视图:运行时添加和移除条目。
- Scroll View 组件、
<pc-scroll-view>和 ScrollViewComponent:全部滚动视图属性参考。 - Scrollbar 组件、
<pc-scrollbar>和 ScrollbarComponent:全部滚动条属性参考。