timeline
title 前端自适应布局实战时间线
自适应组件功能解析 : 0-12分钟
底层代码解密过程 : 12-25分钟
容器与节点关系剖析 : 25-40分钟
多列布局问题定位 : 40-55分钟
坐标计算算法优化 : 55-70分钟
动画效果调试实战 : 70-85分钟
布局逻辑重构方案 : 85-100分钟

编程如弈棋,每一步都需要缜密思考。今天这堂实战课带大家深入前端自适应布局的奥秘:
- 演示了如何通过GUI库实现横向/纵向自适应布局。就像搭积木,父容器与子节点的关系决定了整体结构的稳定性。
- 解密底层lua文件的过程犹如考古,使用专业工具解开加密脚本。记住:官方源码是最好的老师,其代码结构蕴含着经过千锤百炼的设计智慧。
- 节点操作是GUI编程的核心。通过getChildren获取子节点列表时,要注意可见性判断和锚点设置,这就像给每个组件分配专属座位。
- 多列布局bug的排查过程展示了程序员思维:通过打印关键变量值,逐步缩小问题范围。发现原代码仅实现了单行布局逻辑,这正是我们需要补全的部分。
- 坐标计算算法需要数学思维。通过分析x/y偏移量和容器宽高的关系,我们重构了位置计算公式,让元素能按预期排列成多行多列。
- 动画效果调试就像导演说戏。控制delay时间和移动轨迹,让元素依次淡入飞出,创造流畅的视觉体验。注意cocos2d动作系统的使用技巧。
- 最终采用模块化重构方案,将布局逻辑与动画逻辑解耦。好的代码应该像乐高积木,每个模块都能独立运作又完美契合。
特别提示:当遇到cocos2d特有API时,可以借助AI工具自动转换为GUI库等效实现。这种"代码翻译"能力将成为你的秘密武器。
建议学员们课后用游戏中的技能面板、背包系统作为练习素材,尝试实现不同方向的自适应布局。记住:看得见的特效背后,是看不见的精密计算。
SL:print(12345)
local parent = GUI:Win_Create("Win_1", 200, 300, 1136, 640)
local Layout = GUI:Layout_Create(parent, "Layout", 50,50, 500.00, 200.00, false)
for i = 1 , 40 do
local button = GUI:Button_Create(Layout, "button_"..i, 100.00, 0.00, "res/public/1900000660.png")
GUI:Win_SetParam(button, i)
GUI:Button_setTitleText(button, "按键"..i)
end
-- 布局参数说明
-- param = {
-- dir: 1:垂直; 2: 水平; 3: 两者
-- gap: x: 左右间距; y: 上下间距; l: 左边距; t: 上边距
-- addDir: 动画增长方式 1: 从上到下(从左到右)(多行从左上角), 2:中间到两边(多行从右上角), 3:从下到上(从右到左)
-- colnum: 多行列数(dir必须是3)
-- autosize: 根据内容自适应容器
-- sortfunc: 排序函数
-- interval:增长方式动画播放时间间隔, 不传值则不播放动画
-- rownums : 每一行的数量table ; 例如:rownums = {3, 2} (第一行3个元素,第二行2个元素)
-- }
-- 自适应布局
-- pNode: ScrollView(常用)、Panel(pNode 中的子控件不显示,则不参与排版)
-- param = {
-- dir: 1:垂直; 2: 水平; 3: 两者
-- gap: x: 左右间距; y: 上下间距; l: 左边距; t: 上边距
-- addDir: 动画增长方式 1: 从上到下(从左到右)(多行从左上角), 2:中间到两边(多行从右上角), 3:从下到上(从右到左)
-- colnum: 多行列数(dir必须是2)
-- autosize: 根据内容自适应容器
-- sortfunc: 排序函数
-- interval:增长方式动画播放时间间隔, 不传值则不播放动画
-- rownums : 每一行的数量table ; 例如:rownums = {3, 2} (第一行3个元素,第二行2个元素)
-- }
function GUI:UserUILayout2(pNode, param)
if not pNode then
return
end
local isScrollView = tolua.type(pNode) == "ccui.ScrollView"
pNode:stopAllActions() -- 停止对象的全部动作
--初始化默认值
param = param or {}
local dir = param.dir and param.dir or (isScrollView and pNode:getDirection() or 3)
dir = math.min(dir, 3)
local gap = param.gap
local addDir = param.addDir or 1
local colnum = param.colnum or 0
local autosize = param.autosize or false
local sortfunc = param.sortfunc
local interval = param.play and 0.01 or param.interval
local rownums = param.rownums or {}
local loadStyle = param.loadStyle or 1
local xGap = (gap and gap.x) and gap.x or (param.x or 0) -- 控件左右间距
local yGap = (gap and gap.y) and gap.y or (param.y or 0) -- 控件上下间距
local xMar = (gap and gap.l) and gap.l or (param.l or 0) -- 左边距
local yMar = (gap and gap.t) and gap.t or (param.t or 0) -- 上边距
--水平和垂直方向只能有一个
local visibleChildren = {} -- 初始化一个table 对象的所有子节点!
-- pNode:getChildren() 获取父节点的所有子节点
-- GUI:getChildren(pNode)
for i,v in ipairs(GUI:getChildren(pNode)) do -- 把对象的子节点 变成table
if v and v:isVisible() then
v:setAnchorPoint({x = 0.5, y = 0.5}) -- 设置锚点
table.insert(visibleChildren, v) -- 深层拷贝
end
end
local num = #visibleChildren -- 获取子节点数量!
if num == 0 then
return
end
if isScrollView then
-- pNode:setDirection(dir)
-- GUI:setDirection(pNode,dir)
pNode.setDirection(pNode,dir)
end
local cSize = visibleChildren[1]:getContentSize() -- 子控件大小
local pSize = pNode:getContentSize() -- 容器大小
local width = xMar * 2
local height = yMar * 2
local offX = 0
local offY = 0
if dir == 1 then -- 垂直
height = height + num * (cSize.height + yGap) - yGap
width = pSize.width
if width > cSize.width then
width = cSize.width
end
elseif dir == 2 then -- 水平
width = width + num * (cSize.width + xGap) - xGap
height = pSize.height
if height > cSize.height then
height = cSize.height
end
else -- 多行多列
local rownum = 0
for i,cnt in ipairs(rownums) do
if cnt and tonumber(cnt) then
colnum = math.max(colnum, cnt)
if autosize then
if cnt > 0 then
rownum = rownum + 1
end
else
rownum = rownum + 1
end
end
end
if colnum < 1 then
colnum = math.max(1, math.floor(pSize.width / cSize.width))
end
if rownum == 0 then
rownum = math.ceil(num / colnum)
end
width = width + colnum * (cSize.width + xGap) - xGap
height = height + rownum * (cSize.height + yGap) - yGap
end
-- 设置容器的尺寸
if autosize then
pNode:setContentSize({width = width, height = height})
if isScrollView then
pNode:setInnerContainerSize({width = width, height = height})
end
else
if pSize.width > width then
offX = (pSize.width - width) / 2
end
if pSize.height > height then
offY = (pSize.height - height) / 2
end
width = math.max(pSize.width, width)
height = math.max(pSize.height, height)
if isScrollView then
pNode:setInnerContainerSize({width = width, height = height})
else
pNode:setContentSize({width = width, height = height})
end
end
-- 自己排序
if sortfunc then
sortfunc(visibleChildren)
end
local scrollFunc = {
[1] = function ()
if addDir == 2 then
pNode:scrollToPercentVertical(50, 0.01, false)
elseif addDir == 3 then
pNode:scrollToPercentVertical(100, 0.01, false)
end
end,
[2] = function ()
if addDir == 2 then
pNode:scrollToPercentHorizontal(50, 0.01, false)
elseif addDir == 3 then
pNode:scrollToPercentHorizontal(100, 0.01, false)
end
end
}
-- 水平垂直滚动指定位置
if isScrollView and (dir == 1 or dir == 2) then
local func = scrollFunc[dir]
if func then
func()
end
end
if dir == 3 then -- 双方向
local rows = {}
local cnum = 0
for i,cnt in ipairs(rownums) do
if cnt and tonumber(cnt) then
cnum = cnum + cnt
if autosize then
if cnt > 0 then
rows[#rows+1] = cnum
end
else
rows[i] = cnum
end
end
end
local t = 1
for i,item in ipairs(visibleChildren) do
local hang = math.ceil(i / colnum)
local k = i
for r,v in ipairs(rows) do
if i <= v then
hang = r
if rows[r-1] then
k = i - rows[r-1]
end
break
end
end
local x = 0
local y = 0
local mod = k % colnum
if addDir == 2 then
if autosize then
x = mod == 0 and xMar + offX + cSize.width/2 or (xMar + (colnum - mod + 1-0.5) * (cSize.width + xGap) - xGap/2) + offX
else
x = mod == 0 and xMar + offX * 2 + cSize.width/2 or (xMar + (colnum - mod + 1-0.5) * (cSize.width + xGap) - xGap/2) + offX * 2
end
else
if autosize then
x = mod == 0 and (width - xMar - cSize.width/2) - offX or (xMar + (mod-0.5) * (cSize.width + xGap) - xGap/2) + offX
else
x = mod == 0 and (width - xMar - cSize.width/2) - offX * 2 or (xMar + (mod-0.5) * (cSize.width + xGap) - xGap/2)
end
end
if loadStyle == 3 then
y = yMar + (hang - 0.5) * cSize.height + (hang - 1) * yGap
elseif loadStyle == 2 then
y = height - yMar - (hang - 0.5) * cSize.height - (hang - 1) * yGap - offY
else
y = height - yMar - (hang - 0.5) * cSize.height - (hang - 1) * yGap
end
item.__pos = clone({x = x, y = y})
item:setPosition({x = x, y = y})
if interval then
t = t + 1
SL:print("打印BC:",t)
item:setVisible(true)
-- item:runAction(cc.Sequence:create(cc.DelayTime:create(i*interval), cc.Show:create()))
-- item:runAction(cc.Spawn:create(cc.FadeTo:create(interval * t, 255), cc.EaseExponentialOut:create(cc.MoveTo:create(interval * t, item.__pos))))
item:setOpacity(0)
item:runAction(
GUI:ActionSpawn(
GUI:ActionFadeTo(interval * t*0.02, 255), -- 渐变动画
GUI:ActionEaseExponentialOut(
GUI:ActionMoveTo(interval *t*1, item.__pos) -- 移动动画
)
)
)
else
item:setVisible(true)
end
end
else -- 水平、垂直
for i,item in ipairs(visibleChildren) do
local x = 0
local y = 0
if dir == 1 then
x = width / 2
if addDir == 1 then -- 上到下
y = height - yMar - cSize.height*(i-0.5) - (i-1) * yGap
elseif addDir == 3 then -- 下到上
y = yMar + cSize.height*(i-0.5) + (i-1) * yGap
else -- 居中
y = height - yMar - cSize.height*(i-0.5) - (i-1) * yGap - offY
item.__pos = clone({x = x, y = y})
y = height / 2
end
else
y = height / 2
if addDir == 1 then -- 左到右
x = xMar + cSize.width*(i-0.5) + (i-1) * xGap
elseif addDir == 3 then -- 右到左
x = width - xMar - cSize.width*(i-0.5) - (i-1) * xGap
else -- 居中
-- x = width - xMar - cSize.width*(i-0.5) - (i-1) * xGap - offX
x = xMar + cSize.width*i
item.__pos = clone({x = x, y = y})
-- x = width / 2
end
end
item:setPosition({x = x, y = y})
if interval then
item:setVisible(false)
else
item:setVisible(true)
end
end
if interval then
if addDir == 2 then
local r = math.floor(num / 2)
local minR = num % 2 == 0 and r or r + 1
local maxR = r + 1
for i=1,num do
local item = visibleChildren[i]
if item then
local t = 1
t = t + 1
item:setLocalZOrder(t)
item:setVisible(true)
item:setOpacity(0)
SL:print("哈哈",interval)
SL:dump(item.__pos)
item:runAction(
GUI:ActionSpawn(
GUI:ActionFadeTo(interval * t, 255), -- 渐变动画
GUI:ActionEaseExponentialOut(
GUI:ActionMoveTo(interval * t*0.5, item.__pos) -- 移动动画
)
)
)
-- item:runAction(cc.Spawn:create(cc.FadeTo:create(interval * t, 255), cc.EaseExponentialOut:create(cc.MoveTo:create(interval*2 * t, {item.__pos}))))
-- item:runAction(cc.Sequence:create(cc.DelayTime:create(i*interval), cc.Show:create()))
end
end
else
for i=1,num do
local item = visibleChildren[i]
if item then
item:runAction(cc.Sequence:create(cc.DelayTime:create(i*interval), cc.Show:create()))
end
end
end
end
end
return cc.size(width, height)
end
GUI:UserUILayout2(Layout, {
dir=3,
-- 3没写完
addDir=2,
interval=1,
gap = {x=1},
sortfunc = function (lists)
table.sort(lists, function (a, b)
return GUI:Win_GetParam(a) > GUI:Win_GetParam(b)
end)
end
})