| name | cocos-code-driven-scrollview-ui |
| description | Use when building code-driven scrollable UI panels in Cocos Creator 3.x. |
Cocos Creator 3.x Code-Driven ScrollView UI Panel
When To Use
When you need to build a complex UI panel (ScrollView + TabBar + Header + interactive cards) in Cocos Creator 3.x without any Editor manual binding. The user only needs to create one empty Node in the scene and attach the script.
Best for:
- Phase 7 content migration panels (Scripture Library, Story Engine, Recitation Hall)
- Any temporary test UI that shouldn't clutter the Editor scene
- Panels that need clean lifecycle management (create/destroy without leaks)
Architecture Pattern
File Structure (3-file module)
assets/scripts/ModuleName/
โโโ ModuleTypes.ts โ Constants, enums, state interfaces
โโโ ModuleManager.ts โ Singleton Cocos Component, data loading + query API
โโโ ModuleUI.ts โ Cocos Component, all dynamic node generation + rendering
Full Node Hierarchy (code-generated in onLoad)
ScripturePanel (ScriptureUI Component)
โโโ header (Node)
โ โโโ title (Label) โ "๐ ่็ถ้ฃ"
โโโ tabBar (Node)
โ โโโ tab_classic1 (Label) โ "๐ ็ก้ๅฃฝ็ถ"
โ โโโ tab_classic2 (Label) โ "๐ ่ง็ถ"
โ โโโ ...
โโโ scrollView (ScrollView)
โ โโโ view (UITransform + Mask Type.RECT)
โ โโโ content (UITransform + Layout Type.VERTICAL, ResizeMode.CONTAINER)
โ โโโ [dynamic child nodes: cards, headings, text, spacers]
โโโ closeBtn (Label) โ "โ"
ScrollView Construction (Critical)
The correct node hierarchy for a working Cocos 3.x ScrollView is:
const svNode = new Node('scrollView');
svNode.setPosition(x, y, 0);
parent.addChild(svNode);
const svTrans = svNode.addComponent(UITransform);
svTrans.width = PANEL_W;
svTrans.height = svH;
const scrollView = svNode.addComponent(ScrollView);
scrollView.horizontal = false;
scrollView.vertical = true;
scrollView.inertia = true;
scrollView.brake = 0.85;
scrollView.elastic = true;
scrollView.verticalScrollBar = null as any;
const viewNode = new Node('view');
viewNode.setPosition(0, 0, 0);
svNode.addChild(viewNode);
const viewTrans = viewNode.addComponent(UITransform);
viewTrans.width = PANEL_W;
viewTrans.height = svH;
const mask = viewNode.addComponent(Mask);
mask.type = Mask.Type.RECT;
const contentNode = new Node('content');
contentNode.setPosition(0, svH / 2, 0);
viewNode.addChild(contentNode);
const contentTrans = contentNode.addComponent(UITransform);
contentTrans.width = PANEL_W - PADDING * 2;
contentTrans.height = svH;
contentTrans.anchorX = 0.5;
contentTrans.anchorY = 1;
const layout = contentNode.addComponent(Layout);
layout.type = Layout.Type.VERTICAL;
layout.resizeMode = Layout.ResizeMode.CONTAINER;
layout.horizontalDirection = Layout.HorizontalDirection.LEFT_TO_RIGHT;
layout.verticalDirection = Layout.VerticalDirection.TOP_TO_BOTTOM;
layout.paddingTop = 30;
layout.paddingLeft = PADDING;
layout.paddingRight = PADDING;
layout.spacingY = 20;
scrollView.content = contentNode;
ScrollView Pitfalls
| Mistake | Symptom | Fix |
|---|
| No Mask on View node | Content overflows visible area | Add viewNode.addComponent(Mask) with type = Mask.Type.RECT |
| Content anchor (0.5, 1) wrong | Content starts from center instead of top | Set contentTrans.anchorX = 0.5; anchorY = 1 |
| Missing Layout CONTAINER | Content doesn't auto-grow | Add Layout, set resizeMode = CONTAINER |
| Setting content height manually | Layout CONTAINER + manual height conflict | Don't set content height โ Layout handles it |
| Not linking scrollView.content | ScrollView doesn't know what to scroll | scrollView.content = contentNode |
Forgetting as any on scrollBar | null assignment type error | scrollView.verticalScrollBar = null as any |
scrollView placed on wrong node | ScrollView doesn't scroll | ScrollView component goes on the OUTER node, NOT the view node |
Touch Dispatch Pattern
Since all nodes are code-generated, use node name string prefix for touch identification (no Editor event system):
const TAG_TAB = 'tab_';
const TAG_CARD = 'card_';
const TAG_BACK = 'btn_back';
const TAG_CLOSE = 'btn_close';
onLoad(): void {
this.node.on(Node.EventType.TOUCH_END, this._onTouchEnd, this);
}
const tabNode = new Node(TAG_TAB + tab.id);
const cardNode = new Node(TAG_CARD + classic.id);
backBtn.name = TAG_BACK;
closeBtn.name = TAG_CLOSE;
private _onTouchEnd(event: EventTouch): void {
const target = event.target;
if (!target) return;
const name = target.name;
if (name === TAG_CLOSE) { this.node.destroy(); return; }
if (name === TAG_BACK) { this._showCatalogue(); return; }
if (name.startsWith(TAG_TAB)) { ... }
if (name.startsWith(TAG_CARD)) { ... }
}
Lifecycle Management
private _clearContent(): void {
if (this._contentNode) {
this._contentNode.removeAllChildren();
}
}
cc.d.ts Declarations Needed
If your project has a custom types/cc.d.ts for tsc --noEmit, you need these declarations:
export class ScrollView extends Component {
public content: Node | null;
public horizontal: boolean;
public vertical: boolean;
public inertia: boolean;
public brake: number;
public elastic: boolean;
public verticalScrollBar: Node | null;
}
export namespace Layout {
enum Type { NONE = 0, HORIZONTAL = 1, VERTICAL = 2, GRID = 3 }
enum ResizeMode { NONE = 0, CONTAINER = 1, CHILDREN = 2 }
enum HorizontalDirection { LEFT_TO_RIGHT = 0, RIGHT_TO_LEFT = 1 }
enum VerticalDirection { TOP_TO_BOTTOM = 0, BOTTOM_TO_TOP = 1 }
}
export class Layout extends Component { ... }
export namespace Mask { enum Type { RECT = 0, ELLIPSE = 1, IMAGE_STENCIL = 2 } }
export class Mask extends Component { public type: Mask.Type; }
public setPosition(x: number | Vec3, y?: number, z?: number): void;
public setContentSize(w: number, h?: number): void;
public setContentSize(s: Size): void;
export class EventTouch { public target: Node; }
Label Creation Pattern (no isSystemFontUsed)
Cocos 3.x Label does NOT have isSystemFontUsed property in all versions. Use this safe pattern:
private _makeLabel(text: string, fontSize: number, hexColor: string): Node {
const node = new Node('label');
const label = node.addComponent(Label);
label.string = text;
label.fontSize = fontSize;
label.lineHeight = fontSize * 1.6;
label.color = this._hexToColor(hexColor);
label.horizontalAlign = Label.HorizontalAlign.CENTER;
const trans = node.getComponent(UITransform);
if (trans) trans.width = CARD_W;
return node;
}
private _hexToColor(hex: string): Color {
return Color.fromHEX(new Color(), hex);
}
private _wrapText(text: string, charsPerLine: number): string {
let result = '', count = 0;
for (const ch of text) { result += ch; count++; if (count >= charsPerLine) { result += '\n'; count = 0; } }
return result;
}
Dark Mode Color Constants
Place at module top-level for easy global adjustment:
const C_BG = '#1A1A2E';
const C_SURFACE = '#16213E';
const C_TEXT_PRIMARY = '#F0EAD6';
const C_TEXT_SECONDARY = '#B8B0A0';
const C_GOLD = '#E8C96A';
const C_PINK = '#D98C7A';
const C_TAB_INACTIVE = '#4A5A7A';
const C_TAB_ACTIVE = '#E8C96A';
Steps to Add a New Code-Driven Panel
- Create
Types.ts โ Tab definitions, UI state interface, defaults
- Create
Manager.ts โ Singleton Component with data loading (async) + query API
- Create
UI.ts โ Component with:
onLoad(): buildHeader(), buildTabBar(), buildScrollView(), buildCloseButton()
- Display methods: showCatalogue(), showClassicContent(), showDailySutra()
- Helper methods: makeLabel(), makeSpacer(), wrapText(), hexToColor()
- Touch handler: _onTouchEnd() with name-based dispatch
- Update Bootstrap.ts โ Add import + switch case
- Update cc.d.ts โ Add any missing Cocos API declarations
- Run
tsc --noEmit โ Confirm zero errors on new files
- Git commit
- User test: Open Creator โ Preview โ click button