动态加载滑动列表
采用了类似分片加载的策略,仅加载出当前可以看到的图片以及一定的预加载部分,随着滑动逐渐的加载出下边的内容,但大量的UI节点会造成内存的紧张,直播平台500个直播间的图片量不能导入所有贴图,大量的贴图残留在内存中会造成不必要的问题, 因此加载策略为仅显示当前看到的ITEM以及上下的预加载部分, 向上滑动删除下边多余的UI节点, 向下滑动删除上边多余的UI节点, 总之维持列表中的节点数为常量。
略图
如图所示是以上滑动过快的情况下显示的加载动画, 由于UI节点是异步动态加载的, 因此存在滑动过快造成加载不及时的情况,游戏中采用加载动画进行过度。此外由于直播列表数信息量较大, 因此在从服务端拉取直播列表信息上做了些优化, 仅在点击对应直播列表的标签页时从服务端进行数据请求, 数据回来后进行列表的显示,回来之前会有加载动画的显示,并且数据会在客户端进行90秒的缓存, 避免直播列表数据不必要的刷新造成的流量浪费。后期会进一步的优化,只从服务端返回显示需要的那一部分直播列表的信息, 在向下滑动时逐步加载出所有直播列表信息。
以下为代码实现样例
class ScrollViewAdapter(object):
"""
自定义的滑动列表控件, 实现分片加载, 需要找UI制作列表节点两个Layout为父子关系,
父Layout勾选剪裁, 子Layout与父Layout大小相同左上角为锚点, 都勾选交互。Item 锚点在中心
加载采用定时加载的策略, 每隔一定时间加载一个Item, 滑动到没有加载完成的行,
就显示加载中的图标, 直到加载完预加载的内容。
"""
# 列表的状态
STOP = 0
DRAG_MOVE = 1
INERTIA_MOVE = 2
BOUND_MOVE = 3
##########################################################################
# 自定义滑动列表的初始化
##########################################################################
def __init__(self, scroll_view, item_node, row_count, item_data_list, create_item_func):
"""
@ scroll_view: 滑动列表控件
@ item_node: Item Node
@ row_count: 每行Item个数
@ item_data_list: 滑动列表Item的数据
@ create_item_func 创建Item的接口
"""
# 列表及其数据初始化
self._scroll_view = scroll_view
self._scroll_view.setVisible(True)
self._scroll_size = self._scroll_view.getContentSize()
self._scroll_inner = self._scroll_view.getChildren()[0]
self._scroll_inner.setAnchorPoint(cc.Vec2(0, 1))
self._scroll_inner.setVisible(True)
self._scroll_inner.setSwallowTouches(False)
self._scroll_org_pos = self._scroll_inner.getPosition()
self._item_data_list = item_data_list
self._item_dict = {}
self._create_item_func = create_item_func
self.item_count = len(self._item_data_list)
# 设置Item的参数
self._item_node = item_node
self._item_size = item_node.getContentSize()
self._x_extern = self._scroll_size.width / 4.0
self._y_extern = self._scroll_size.height / 4.0
self.BATCH_COL = int(row_count)
self.BATCH_ROW = int(self._scroll_size.height / self._item_size.height)
self.BATCH_NUM = self.BATCH_ROW * self.BATCH_COL
self._up_index = 0 # 显示的Item上索引
self._down_index = 2 * self.BATCH_NUM - 1 # 显示的Item下索引
self._item_up_index = 0 # 加载的Item上索引
self._item_down_index = -1 # 加载的Item上索引
self._has_all_load = False # 是否所有都加载过了
# 列表事件监听
self.last_tick_time = time.time()
self._scroll_view.addTouchEventListener(self._on_scroll_view)
# Item加载参数
self.loading_widget = None
self.loading_widget_size = cc.Size(self._x_extern / 2.0, self._x_extern / 2.0)
self.loading_interval = 0.04
self.last_loading_time = 0
self.load_ani_time = 0
self.is_up_loading_show = False
self.is_down_loading_show = False
self.stop_loading_index = 0 # 惯性滑动时停止加载的位置, 仅为美观
# 列表状态机
self.state_machine = {
ScrollViewAdapter.STOP: self.on_stop,
ScrollViewAdapter.INERTIA_MOVE: self.on_inertia_move,
ScrollViewAdapter.BOUND_MOVE: self.on_bound_move,
}
# 列表滑动的相关参数
self._move_state = ScrollViewAdapter.STOP
self._acc_cur = 1000.0 # 当前加速度
self._acc_inertia = 6000.0 # 惯性阻力值
self._speed_inertia = 0.0 # 当前滑行速度
self._min_speed = 200 # 最小速度
self._max_speed = 1500 # 最大速度
self._move_dis = 0.0 # 回弹距离
self._speed_rebound = self._y_extern / 0.25
self.touch_pos = cc.Vec2(0, 0)
self.start_time = time.time()
def destroy(self):
"""
清空数据
"""
self.loading_widget = None
self.change_move_state(ScrollViewAdapter.STOP)
self._scroll_inner.removeAllChildren()
self._scroll_inner.setPosition(self._scroll_org_pos)
self._item_data_list = []
self._item_dict.clear()
self.item_count = 0
self._create_item_func = None
self._up_index = 0
self._down_index = 2 * self.BATCH_NUM - 1
self._item_up_index = 0
self._item_down_index = -1
self._has_all_load = False
def _on_scroll_view(self, widget, event):
"""
滑动列表事件监听
"""
if event == ccui.WIDGET_TOUCHEVENTTYPE_BEGAN:
# 开始点击, 记录参数, 进入拖动状态
self.on_touch = True
self._move_dis = 0
self._speed_inertia = self._min_speed
self.touch_pos = utils.vec2_multi_scale(widget.getTouchBeganPosition())
self.start_pos = self.touch_pos
self.start_time = time.time()
elif event == ccui.WIDGET_TOUCHEVENTTYPE_MOVED:
# 拖动位移, 移动Inner Layout
touch_pos = utils.vec2_multi_scale(widget.getTouchMovePosition())
diff_x = touch_pos.x - self.touch_pos.x
diff_y = touch_pos.y - self.touch_pos.y
self.touch_pos = touch_pos
self.on_slide_move(diff_x, diff_y, True)
else:
self.on_touch = False
if self.check_bound_limit():
# 进入边界回弹状态
self.change_move_state(ScrollViewAdapter.BOUND_MOVE)
else:
self.calculate_scroll_speed(widget)
self.change_move_state(ScrollViewAdapter.INERTIA_MOVE)
def set_loading_interval(self, time_interval):
"""
设置加载一个Item的时间间隔, 依据每行Item数目进行调整
"""
self.loading_interval = time_interval
def set_loading_widget(self, loading_widget, play_handler):
"""
设置加载节点, 节点用于展示加载动画, 放置于列表的底部
"""
self.loading_widget = loading_widget
self.loading_widget.setAnchorPoint(cc.Vec2(0.5, 0.5))
self.loading_widget_size = loading_widget.getContentSize()
self._scroll_inner.addChild(self.loading_widget)
self.loading_widget.setPosition(cc.Vec2(0.5, 0.5))
self.loading_widget.setLocalZOrder(100)
play_handler and play_handler(loading_widget)
def tick(self):
"""
滚动列表的驱动TICK
"""
cur_time = time.time()
delta_time = cur_time - self.last_tick_time
self.last_tick_time = cur_time
# 定时加载Item
if cur_time - self.last_loading_time > self.loading_interval:
self.update_item()
self.last_loading_time = cur_time
# 更新加载状态
self.on_loading_state(delta_time)
# 状态机的更新
self.update_state_mechine(delta_time)
##########################################################################
# 滑动列表的 核心功能函数
##########################################################################
def get_item_position(self, index):
"""
获取Item的位置坐标
"""
base_diff = cc.Vec2(self._item_size.width / 2.0, -self._item_size.height / 2.0)
row_delta_num = index / self.BATCH_COL
coloum_delta_num = index % self.BATCH_COL
x = coloum_delta_num * self._item_size.width + base_diff.x
y = -row_delta_num * self._item_size.height + base_diff.y
return cc.Vec2(x, y + self._scroll_size.height)
def _fix_slide_move(self, diff_x, diff_y):
"""
修正位移避免过多的边界外漏
"""
# 计算新的位置并加载新的内容
org_pos = self._scroll_inner.getPosition()
show_height_up = org_pos.y - self._scroll_org_pos.y
show_height_down = org_pos.y - self._scroll_org_pos.y + self._scroll_size.height
up_load_height = (self._item_up_index / self.BATCH_COL) * self._item_size.height
down_load_height = (self._item_down_index / self.BATCH_COL + 1) * self._item_size.height
item_height = ((self.item_count - 1) / self.BATCH_COL + 1) * self._item_size.height
item_height = max(self._scroll_size.height, item_height)
load_w_h = self.loading_widget_size.height
if org_pos.y + diff_y - self._scroll_org_pos.y < -self._y_extern:
diff_y = -self._y_extern + self._scroll_org_pos.y - org_pos.y
self._speed_inertia = 0
elif org_pos.y + diff_y > item_height + self._y_extern:
diff_y = item_height + self._y_extern - org_pos.y
self._speed_inertia = 0
elif self.is_bound_move_state():
return diff_x, diff_y
elif self.is_up_loading_show and up_load_height > show_height_up + diff_y + load_w_h:
diff_y = up_load_height - show_height_up - load_w_h
self._speed_inertia = 0
elif self.is_down_loading_show and show_height_down + diff_y - down_load_height > load_w_h:
diff_y = load_w_h + down_load_height - show_height_down
self._speed_inertia = 0
return diff_x, diff_y
def _fix_inertia_move(self, diff_x, diff_y):
"""
惯性滑动的滑动距离修正, 加载项的设置等
"""
# 计算滑动的位置与预加载内容的位置
org_pos = self._scroll_inner.getPosition()
show_height = org_pos.y - self._scroll_org_pos.y + self._scroll_size.height
load_height = (self._item_down_index / self.BATCH_COL + 1) * self._item_size.height
# 修正距离
if show_height + diff_y - load_height > self.loading_widget_size.height:
self.change_move_state(ScrollViewAdapter.STOP)
diff_y = self.loading_widget_size.height + load_height - show_height
return diff_x, diff_y
return diff_x, diff_y
def check_bound_limit(self):
"""
检测限位, 避免超出边界
"""
if self.is_bound_move_state():
return True
scroll_cur_pos = self._scroll_inner.getPosition()
if scroll_cur_pos.y < self._scroll_org_pos.y:
self._move_dis = self._scroll_org_pos.y - scroll_cur_pos.y
return True
else:
item_height = ((self.item_count - 1) / self.BATCH_COL + 1) * self._item_size.height
item_height = max(item_height, self._scroll_size.height)
if scroll_cur_pos.y > item_height:
self._move_dis = item_height - scroll_cur_pos.y
return True
self._move_dis = 0
return False
def _check_loading_hard(self):
"""
检测Item是否滑动过快, 来不及加载Item
"""
# 计算相对与列表的位置
org_pos = self._scroll_inner.getPosition()
load_item_x = self._scroll_size.width / 2.0
delta_y = (self._item_size.height + self.loading_widget_size.height) / 2.0
# 计算上边界是否超出加载的范围
item_pos = self.get_item_position(self._item_up_index)
show_height = org_pos.y - self._scroll_org_pos.y
load_height = (self._item_up_index / self.BATCH_COL) * self._item_size.height
if load_height > show_height and not self.is_up_loading_show:
self.up_stop_loading_index = self._item_up_index / self.BATCH_COL * self.BATCH_COL
self.is_up_loading_show = True
self._show_loading_item(load_item_x, item_pos.y + delta_y)
# 计算下边界是否超出加载范围
item_pos = self.get_item_position(self._item_down_index)
show_height = org_pos.y - self._scroll_org_pos.y + self._scroll_size.height
load_height = (self._item_down_index / self.BATCH_COL + 1) * self._item_size.height
if show_height > load_height and not self.is_down_loading_show:
self.down_stop_loading_index = (self._item_down_index / self.BATCH_COL + 1) * self.BATCH_COL-1
self.is_down_loading_show = True
self._show_loading_item(load_item_x, item_pos.y - delta_y)
def _show_loading_item(self, load_item_x, load_item_y):
"""
加载控件的显示
"""
self.loading_widget.setPosition(cc.Vec2(load_item_x, load_item_y))
self.loading_widget.setVisible(True)
self.load_ani_time = 0
def calculate_scroll_speed(self, widget):
"""
计算松手后的滑动速度
"""
# 进入惯性滑动状态
delta_time = time.time() - self.start_time
touch_pos = utils.vec2_multi_scale(widget.getTouchEndPosition())
self._speed_inertia = 0 if delta_time == 0 else (
touch_pos.y - self.start_pos.y) / delta_time
self._speed_inertia = max(-self._max_speed, min(self._max_speed, self._speed_inertia))
self._speed_inertia = 0 if delta_time > 0.25 else self._speed_inertia
def get_attenuate_speed(self, speed, delta_time):
"""
速度衰减, 衰减为min_speed后计算吸附参数
"""
if speed > self._min_speed:
speed -= self._acc_cur * delta_time
speed = max(speed, self._min_speed)
elif speed < -self._min_speed:
speed += self._acc_cur * delta_time
speed = min(speed, -self._min_speed)
else:
speed = self._min_speed if speed > 0 else -self._min_speed
if abs(speed) == self._min_speed:
speed = 0
return speed
def update_item(self):
"""
更新Item
"""
# 检查上边界ITEM增删
if self._item_up_index < self._up_index:
# 删除不需要显示的ITEM
self._del_item(self._item_up_index)
self._item_up_index += 1
elif self._item_up_index > self._up_index:
# 加载动画时暂停加载Item
if self.is_up_loading_show and self._item_up_index <= self.up_stop_loading_index:
return
# 加载需要显示的ITEM
self._item_up_index -= 1
self._load_item(self._item_up_index)
# 检查下边界ITEM的删减
if self._item_down_index < self._down_index:
# 加载动画时暂停加载Item
if not self._has_all_load and self.is_inertia_move_state():
if self._item_down_index >= self.stop_loading_index:
return
if self.is_down_loading_show and self._item_down_index >= self.down_stop_loading_index:
return
# 加载需要显示的ITEM
self._item_down_index += 1
self._load_item(self._item_down_index)
elif self._item_down_index > self._down_index:
# 删除不需要显示的ITEM
self._del_item(self._item_down_index)
self._item_down_index -= 1
def _load_item(self, index):
"""
加载需要显示的Item
"""
if index >= self.item_count - 1:
self._has_all_load = True
# 索引超出边界
if index < 0 or index >= self.item_count:
return
# 列表添加节点内容
item_pos = self.get_item_position(index)
item_info = self._item_data_list[index]
item_clone = self._item_node.clone()
item_clone.setVisible(True)
params = [index, item_info]
self._create_item_func(item_clone, params)
self._scroll_inner.addChild(item_clone)
item_clone.setPosition(item_pos)
self._item_dict[index] = item_clone
def _del_item(self, index):
"""
删除不需要显示的Item
"""
item_widget = self._item_dict.pop(index, None)
if not item_widget:
return
self._scroll_inner.removeChild(item_widget)
##########################################################################
# 列表滚动 状态机
##########################################################################
def change_move_state(self, state):
"""
更新滑动列表的状态
"""
self._move_state = state
if self.is_inertia_move_state():
self.stop_loading_index = self._down_index
def on_stop(self, delta_time):
"""
静止停止状态
"""
pass
def on_slide_move(self, diff_x, diff_y, will_load_item=False):
"""
列表的滑动状态
"""
# 加载是否超出加载范围
self._check_loading_hard()
# 修正位移, 避免Item移出边界过多
diff_x, diff_y = self._fix_slide_move(diff_x, diff_y)
# 更新显示的上下边界
org_pos = self._scroll_inner.getPosition()
org_pos.y += diff_y
up_height = org_pos.y - self._scroll_org_pos.y
up_row = math.floor(up_height / self._item_size.height) - self.BATCH_ROW
self._up_index = int(up_row * self.BATCH_COL)
down_hight = org_pos.y - self._scroll_org_pos.y + self._scroll_size.height
down_row = math.ceil(down_hight / self._item_size.height) + self.BATCH_ROW
self._down_index = max(int(down_row * self.BATCH_COL), 2*self.BATCH_NUM) - 1
# 更新列表的位置
self._scroll_inner.setPosition(org_pos)
def on_inertia_move(self, delta_time):
"""
边界回弹状态
"""
delta_distance = 0
if abs(self._speed_inertia) > self._min_speed:
# 惯性滑动
self._speed_inertia = self.get_attenuate_speed(
self._speed_inertia, delta_time)
delta_distance = self._speed_inertia * delta_time
diff_x, diff_y = self._fix_inertia_move(0, delta_distance)
self.on_slide_move(diff_x, diff_y, False)
elif self.check_bound_limit():
# 进入边界回弹状态
self.change_move_state(ScrollViewAdapter.BOUND_MOVE)
else:
# 进入停止状态
self.change_move_state(ScrollViewAdapter.STOP)
def on_bound_move(self, delta_time):
"""
边界回弹状态
"""
if self._move_dis <> 0:
delta_distance = self._speed_rebound * delta_time
if self._move_dis > 0:
delta_distance = min(delta_distance, self._move_dis)
else:
delta_distance = max(-delta_distance, self._move_dis)
self._move_dis -= delta_distance
self.on_slide_move(0, delta_distance, False)
else:
# 进入停止状态
self.change_move_state(ScrollViewAdapter.STOP)
def on_loading_state(self, delta_time):
"""
Item 加载状态
"""
# 是否处于加载状态
if not (self.is_up_loading_show or self.is_down_loading_show):
return
# 加载是否完毕
if self._check_loading_done(delta_time):
# 加载完毕后更改状态
if self.is_up_loading_show:
self.is_up_loading_show = False
self.up_stop_loading_index = 0
if self.is_down_loading_show:
self.is_down_loading_show = False
self.down_stop_loading_index = 0
def _check_loading_done(self, delta_time):
"""
检查加载动画播放是否完毕
"""
self.load_ani_time += delta_time
if self.load_ani_time > 0.25:
self.load_ani_time = 0
self.loading_widget and self.loading_widget.setVisible(False)
if self.check_bound_limit():
self.change_move_state(ScrollViewAdapter.BOUND_MOVE)
return True
return False
def update_state_mechine(self, delta_time):
"""
更新状态机的状态
"""
# 运行当前状态机
state_handler = self.state_machine.get(self._move_state)
state_handler and state_handler(delta_time)
def is_bound_move_state(self):
"""
当前是否是边界回弹状态
"""
return self._move_state == ScrollViewAdapter.BOUND_MOVE
def is_inertia_move_state(self):
"""
当前是否是边界回弹状态
"""
return self._move_state == ScrollViewAdapter.INERTIA_MOVE