动态加载滑动列表实现

动态加载滑动列表

        采用了类似分片加载的策略,仅加载出当前可以看到的图片以及一定的预加载部分,随着滑动逐渐的加载出下边的内容,但大量的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_load_index = -1  # 已经加载过的Item的索引

        # 列表事件监听
        self.last_tick_time = time.time()
        self._scroll_view.addTouchEventListener(self._on_scroll_view)

        # Item加载参数
        self.set_loading_speed(60)
        self.loading_widget = None
        self.loading_widget_size = cc.Size(self._x_extern / 2.0, self._x_extern / 2.0)
        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._inertia_stop_loading_mode = False  # 是否惯性滑动停止Item加载
        self.loading_done_time = 0.25  # 加载动画的显示时长
        self._is_swallow_touch = False  # Item 是否吃点击事件

        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_load_index = -1

    def _on_scroll_view(self, widget, event):
        """
            滑动列表事件监听
        """
        if event == ccui.WIDGET_TOUCHEVENTTYPE_BEGAN:
            # 开始点击, 记录参数, 进入拖动状态
            self.change_move_state(ScrollViewAdapter.STOP)
            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_speed(self, item_count):
        """
            设置加载ITEM的速度
        """
        time_interval = max(1, item_count)
        self.loading_interval = 1/float(time_interval)

    def set_stop_loading_mode(self, enable):
        """
            是否设置惯性滑动通知加载模式
        """
        self._inertia_stop_loading_mode = enable

    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 set_swallow_touch(self, enable):
        """
            设置Item是否点击穿透, 不点击穿透的要自行外部设置部分穿透
            否则无法拖动列表
        """
        self._is_swallow_touch = enable

    def request_item_run_func(self, func):
        """
            对所有的节点执行函数
        """
        if not func:
            return
        for item_node in self._item_dict.itervalues():
            func(item_node)

    def tick(self):
        """
            滚动列表的驱动TICK
        """
        cur_time = time.time()
        delta_time = cur_time - self.last_tick_time
        self.last_tick_time = cur_time

        # Item的动态加载
        self.update_item()

        # 更新加载状态
        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):
        """
            加载控件的显示
        """
        if not self.loading_widget:
            return
        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的加载与速度
        """
        cur_time = time.time()
        pass_time = cur_time - self.last_loading_time
        if self.last_loading_time == 0:
            pass_time = 2 * self.loading_interval
        
        if pass_time > self.loading_interval:
            item_count = int(round(pass_time / self.loading_interval))
            for i in xrange(item_count):
                self._update_item()
            self.last_loading_time = cur_time

    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 self.is_stop_loading_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
        """
        self._has_load_index = max(index, self._has_load_index)

        # 索引超出边界
        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.setAnchorPoint(cc.Vec2(0.5, 0.5))
        self.set_swallow_touch_deep(item_clone, self._is_swallow_touch)
        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 set_swallow_touch_deep(self, widget, enable):
        """
            设置Widget的点击喜欢
        """
        if not widget:
            return
        if hasattr(widget, 'setSwallowTouches'):
            widget.setSwallowTouches(enable)
        widget_list = widget.getChildren()
        for widget in widget_list:
            self.set_swallow_touch_deep(widget, enable)


    ##########################################################################
    # 列表滚动 状态机
    ##########################################################################

    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 > self.loading_done_time:
            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

    def is_stop_loading_state(self):
        """
            当前是否是惯性滑动停止加载Item的模式
        """
        if not self._inertia_stop_loading_mode:
            return False
        
        if not self.is_inertia_move_state():
            return False

        if self._item_down_index < self._has_load_index:
            return False
        
        return True

 

展开阅读全文

没有更多推荐了,返回首页