Skip to content

TableView构建列表 Step2

zhangxin-it edited this page Dec 12, 2019 · 4 revisions

TableView(列表布局)

完整Demo:TableViewDemo.lua

TableView是MomoLua中对列表布局的封装,它的使用包括View和Adapter两个部分。其中TableView决定整个列表的显示位置,大小。Adapter说明数据(Data)和单元视图(ItemView)的绑定关系,以及每个单元视图(ItemView)的渲染方式。

一、TableView

TableView继承自View,布局方式和View相同,此处主要介绍TableView的部分常用方法。

--tableview初始化
tableView = TableView(true, true)
        :width(MeasurementType.MATCH_PARENT):height(MeasurementType.MATCH_PARENT);
--下拉刷新事件回调
tableView:setRefreshingCallback(
        function()
            print("开始刷新")
            System:setTimeOut(function()
                --2秒后结束刷新
                print("结束刷新了")
                tableView:stopRefreshing()
            end, 2)
        end)
--上拉加载事件回调
tableView:setLoadingCallback(function()
    print("开始加载")
    System:setTimeOut(function()
        --2秒后结束加载
        print("结束加载")
        tableView:stopLoading()
        --已加载全部
        tableView:noMoreData()
    end, 2)

end)

local adapter = initAdapter();--adapter初始化方法,具体实现稍后会介绍
tableView:adapter(adapter);
window:addView(tableView);

在以上代码中,我们新建一个TableView,并使其充满整个window,设置了其在下拉刷新和上滑加载更多的回调方法。
在以上新建TableView的构造方法中,两个true控制下拉刷新加载更多开关打开。打开后在交互中如果触发相关操作,会直接回调上边绑定的回调方法。

setRefreshingCallback(func callback) 设置下拉刷新回调
setLoadingCallback(func callback) 设置加载更多回调
stopRefreshing 停止刷新动画
stopLoading 停止加载动画
noMoreData 没有更多数据,之后上拉不会再加载
adapter(Adapter adapter) 绑定适配器
以上最后一步tableView调用adapter(Adapter adapter)为其绑定适配器,adapter对象的初始化我们稍后再讲。至此tableview的初始化操作基本完成。

二、Adapter(TableViewAdapter和TableViewAutoFitAdapter)

列表视图(TableView)可以看做是一组View的集合,但是TableView并不自己控制这些子View的创建以及每个子View中数据的显示,因此,它需要一个中间类来协助他完成对于子View的创建和控制,这个类就是Adapter(适配器)。
我们尝试来逆向思考,如果Adapter作为TableView管理子View的"管家",TableView将会向Adapter"索取"什么功能。首先肯定是要获知一共有多少个子View需要显示,其次,每个子View是什么样的,最后,每个子View中的业务数据如何显示。 围绕这三个要素,我们看以下代码:

-- item类型枚举
local TYPE_CELL = {
    TEXT = "TYPE_CELL_TEXT",
    IMG = "TYPE_CELL_IMG"
}

--初始化适配器
local function initAdapter()
    -----------------------TableViewAutoFitAdapter------------------------------
    --adapter = TableViewAutoFitAdapter();--根据布局高度自适应
    -----------------------TableViewAdapter-------------------------------------
    adapter = TableViewAdapter();
    ---------TableViewAdapter需自行计算item高度,并在heightForCell方法中返回---------
    adapter:heightForCell(function(section, row)
        return 120
    end)
    -- 组数,一维list返回1
    adapter:sectionCount(function()
        return 1;
    end);

    ------------------------------------ 子View个数 ------------------------------

    -- 返回tableview中子View的个数,一维时行数取决于datas大小,和section无关
    adapter:rowCount(function(section)
        if datas == nill or #datas == 0 then
            return 0;
        else
            return #datas;
        end
    end);

    ------------------------------------ 子View类型 ------------------------------

    -- 返回当前位置子View的类型标识,一维时取决于position对应的data,和section无关
    adapter:reuseId(function(section, position)
        local theme = datas[position].theme;
        local type = nil;
        if theme then
            if theme == 101 then
                -- 布局一:显示文本
                type = TYPE_CELL.TEXT;
            elseif theme == 201 then
                -- 布局二:显示图片
                type = TYPE_CELL.IMG;
            end
        end
        return type;
    end);

    ------------------------------------ 创建子View ------------------------------

    -- 初始化指定类型:TYPE_CELL.TEXT的子View(仅描述View,不描述业务数据的绑定关系)
    adapter:initCellByReuseId(TYPE_CELL.TEXT, function(cell)
        cell.rowContainer = View():width(MeasurementType.MATCH_PARENT)
                                  :height(MeasurementType.WRAP_CONTENT)
                                  :setGravity(Gravity.CENTER);
        cell.tv = Label():fontSize(16)
                         :textAlign(TextAlign.CENTER)
                         :setGravity(Gravity.CENTER);
        cell.rowContainer:addView(cell.tv);

        cell.contentView:addView(cell.rowContainer);
    end);

    -- 初始化指定类型:TYPE_CELL.IMG的子View(仅描述View,不描述业务数据的绑定关系)
    adapter:initCellByReuseId(TYPE_CELL.IMG, function(cell)
        cell.rowContainer = View():width(MeasurementType.MATCH_PARENT)
                                  :height(MeasurementType.WRAP_CONTENT)
                                  :setGravity(Gravity.CENTER);
        cell.iv = ImageView():width(60):height(60)
                             :cornerRadius(45) 
                             :contentMode(ContentMode.SCALE_TO_FILL)
                             :setGravity(Gravity.CENTER);
        cell.rowContainer:addView(cell.iv);

        cell.contentView:addView(cell.rowContainer);
    end);
    -------------------------------- 绑定子View与业务值 --------------------------

    -- 描述指定类型:TYPE_CELL.TEXT的子View在指定位置上与业务数据的绑定关系
    adapter:fillCellDataByReuseId(TYPE_CELL.TEXT, function(cell, section, row)
        cell.tv:text(datas[row].desc);
    end);

    -- 描述指定类型:TYPE_CELL.IMG的子View在指定位置上与业务数据的绑定关系
    adapter:fillCellDataByReuseId(TYPE_CELL.IMG, function(cell, section, row)
        cell.iv:image(datas[row].img_url);
    end);

    -------------------------------- 绑定子View点击事件 --------------------------

    -- 设置指定类型:TYPE_CELL.TEXT的子View在点击时的回调
    adapter:selectedRowByReuseId(TYPE_CELL.TEXT, function(cell, section, row)
        print("点击了:" .. TYPE_CELL.TEXT .. "-" .. tostring(row));
    end);

    -- 设置指定类型:TYPE_CELL.IMG的子View在点击时的回调
    adapter:selectedRowByReuseId(TYPE_CELL.IMG, function(cell, section, row)
        print("点击了:" .. TYPE_CELL.IMG .. "-" .. tostring(row));
    end);
    return adapter;
end

【注】 上述代码中我们会经常看见一个参数section以及对section的初始化函数sectionCount(func),但在各个方法中却没有使用section参与运算,其实section在sdk中的设计初衷是描述一个二维列表,即section代表组,每个组下又有属于这个组的子列表。但在实际业务中,以一维列表居多,部分非一维列表也可以转换成一维列表开发,因此多数情况下sectionCount的回调将固定回1,且出现在其他方法中时无实意。


围绕Adapter需要提供的功能,我们看到以下核心方法:

rowCount(function(section) callback) 子View个数,在回调callback中返回
reuseId(function(number section, number row) callback) 子View的样式类型,部分业务中可能不同位置子View样式不同
initCellByReuseId(string reuseId, function(table cell) callback) 根据子View样式类型初始化子View
fillCellDataByReuseId(string reuseId, function(table cell, number section, number row)) 根据子View样式类型以及指定的在列表中的位置,描述view与业务数据的绑定关系
selectedRowByReuseId(string reuseId, function(table cell, number section, number row)) 设置子View点击回调
我们注意到,在和reuseId相关的方法中,每个方法都有多个同名的"重载"方法,而第一个参数reuseId并不是变量,而是声明在参数表中的常量,我们抽离业务代码后看上述代码:
adapter:reuseId(function(section, position)
    local theme = datas[position].theme;
    local type = nil;
    if theme then
        if theme == 101 then
            -- 布局一:显示文本
            type = TYPE_CELL.TEXT;
        elseif theme == 201 then
            -- 布局二:显示图片
            type = TYPE_CELL.IMG;
        end
    end
    return type;
end);

------------------------------------ 创建子View ------------------------------

adapter:initCellByReuseId(TYPE_CELL.TEXT, function(cell)

end);
adapter:initCellByReuseId(TYPE_CELL.IMG, function(cell)

end);

当我们在reuseId(func)方法中返回多少种reuseId,与reuseId相关的方法就各自要多少种"重载",例如上述代码中返回两种reuseId:TYPE_CELL.TEXT和TYPE_CELL.IMG,那么用来创建子view的方法initCellByReuseId就有两个"重载"(其实不是重载,sdk的底层用委托的方式实现调用)。在每个方法中,我们已经只需要初始化参数表第一个常量reuseId所代表类型的view就行了。可以看做我们从方法层实现了业务的拆分,类似的fillCellDataByReuseId等方法也是如此。

同时,在initCellByReuseId中,sdk在参数表中传入了cell变量,这个cell是table类型,其中已经提供了View类型的属性contentView,我们在这个方法中自己创建的view最后要添加到这个原始的cell.contentView中去,sdk最终显示的时候会直接显示cell的contentView,我们的view作为contentView的子View显示。需要注意的是,在绑定view与业务数据的时候,fillCellDataByReuseId的回调中再次返回了cell用于让我们为view设置值,此时我们能获取到contentView,但是无法获取到我们要操作的自定义view了,所以在initCellByReuseId的时候,我们不仅要把自己创建的view添加到cell.contentView,也要把自己稍后要操作的view同样委妥到cell,例如上述initCellByReuseId中创建的Label委托给cell.tv,然后再fillCellDataByReuseId中用cell.tv取出后再进行text()赋值操作。


三、简化

如果我们的业务中,列表中的所有view都是同一种样式,也就是reuseId(func)只有一种返回值时,我们有相关的一系列方法用来简化上述的代码:

-- 初始化适配器
local function initAdapter()
    adapter = TableViewAdapter();

    -- 组数,一维list返回1
    adapter:sectionCount(function ()
        return 1;
    end);

    ------------------------------------ 子View个数 ------------------------------

    -- 返回tableview中子View的个数,一维时行数取决于datas大小,和section无关
    adapter:rowCount(function(section)
        if datas == nill or #datas == 0 then
            return 0;
        else
            return #datas;
        end
    end);

    ------------------------------------ 创建子View ------------------------------

    adapter:initCell(function(cell)
        cell.rowContainer = View():width(MeasurementType.MATCH_PARENT):height(120);
        cell.tv = Label():fontSize(16)
                         :textAlign(TextAlign.CENTER)
                         :setGravity(Gravity.CENTER);
        cell.rowContainer:addView(cell.tv);

        cell.contentView:addView(cell.rowContainer);
    end);

    -------------------------------- 绑定子View与业务值 --------------------------

    adapter:fillCellData(function(cell, section, row)
        cell.tv:text(datas[row].desc);
    end);

    -------------------------------- 绑定子View点击事件 --------------------------

    adapter:selectedRow(function(cell, section, row)
        print("点击了:" .. tostring(row));
    end);

    return adapter;
end

也就是说,我们的adapter可以根据业务不同,使用一下两组方法进行设置
Item多种类型 Item单一类型
reuseId(function(number section, number row) callback)
initCellByReuseId(string reuseId, function(table cell) callback) initCell(function(table cell) callback)
fillCellDataByReuseId(string reuseId, function(table cell, number section, number row)) fillCellData(function(table cell, number section, number row))
selectedRowByReuseId(string reuseId, function(table cell, number section, number row)) selectedRow(function(table cell, number section, number row))

附录

上述例子datas的数据格式:

datas = {
{
theme = 101,
desc = "Apple"
},
{
theme = 101,
desc = "Pear"
},
{
theme = 201,
img_url = "http://img0.imgtn.bdimg.com/it/u=383546810,2079334210&fm=26&gp=0.jpg"
},
{
theme = 201,
img_url = "http://img0.imgtn.bdimg.com/it/u=383546810,2079334210&fm=26&gp=0.jpg"
},
{
theme = 101,
desc = "Orange"
},
{
theme = 201,
img_url = "http://img0.imgtn.bdimg.com/it/u=383546810,2079334210&fm=26&gp=0.jpg"
}
}
Clone this wiki locally