列表组件抽象(4)-滚动列表及分页说明

这是我写的关于列表组件的第4篇博客。前面的相关文章有:

1. 列表组件抽象(1)-概述

2. 列表组件抽象(2)-listViewBase说明

3. 列表组件抽象(3)-分页和排序管理说明

本文介绍列表组件中我对滚动列表及滚动分页的实现思路。

在pc端,通过滚动进行翻页的需求非常常见;移动端也是,只不过移动端由于scroll事件触发有延迟,必须等到屏幕停止滑动后才会触发,而不是在用户的手指离开屏幕就立即触发,所以移动端最好是不用scroll事件直接做滚动翻页,而是用iscroll这类插件提供更实时的scroll事件更好。

不管是pc还是移动端,滚动翻页列表的特点都是差不多的:

1)基本上由以下几个部分组成:数据列表,顶部的加载中提示,底部的加载中提示,没有更多了,没有找到记录。正是按照这个思路,所以我把滚动列表的html结构设计成:

2)跟其它列表组件不同的是,滚动列表在请求新的数据后,有2种方式来渲染新的数据。一种是跟其它列表组件一样,直接把原来的列表内容替换;另一种是将新数据追加在原有的列表内容之后。第1种通常用于直接更改列表的查询条件时使用;第2种用于翻页查询或者刷新操作。

3)在前面的几个部分中,有两个加载中的提示,都是用来提升用户体验的东西。顶部加载提示用于条件查询,底部加载提示用于翻页查询。从它们在html中的位置也能看出来。

4)加载更多的按钮,一是防止滚动事件失效而准备的,二是有些场景可能会禁用掉滚动翻页,所以就要提供直接点击按钮的手工翻页。

5)没有更多了这个部分,在翻页查询后,根据数据结果判断没有更多的数据时显示。

6)没有找到记录的这个部分用于在列表首次查询时,如果数据为空时显示。

7)当通过滚动或者滑动操作,使得滚动列表隐藏于可视区域之下的部分不断往上滚动,并在达到某一个临界点的时候,触发翻页查询,将下一页的数据追加到数据列表后面进行显示。

针对以上的这些需求逻辑,考虑pc端和移动端的场景,我写了两个组件分别用于实现滚动列表。同时与这两个列表组件一起使用的还有另外两个分页组件,它们两两之间是配套使用的。

首先是用于实现pc端,可相对window或者某个DOM元素进行滚动分页的列表组件scrollListView以及它配套的分页组件scrollPageView组件,源码分别是:

https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/scrollListView.js

https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/scrollPageView.js

然后是用于移动端,结合iscroll一起使用的iscrollListView和iscrollPageView组件,源码分别是:

https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/iscrollListView.js

https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/iscrollPageView.js

针对以上组件有以下demo可以查看相关功能演示:

pc端相对window滚动分页demo:http://liuyunzhuge.github.io/blog/form/dist/html/listView_2.html

pc端相对某个DOM元素滚动分页demo:http://liuyunzhuge.github.io/blog/form/dist/html/listView_3.html

移动端滚动分页demo:http://liuyunzhuge.github.io/blog/form/dist/html/listView_4.html

后面的部分说明以上组件的要点。不过由于iscrollListView直接继承了scrollListView,实现非常简单;iscrollPageView的实现思路也跟scrollPageView差不多。所以后面只介绍scrollListView和scrollPageView的相关内容。

先来看scrollListView.js。

首先,代码结构还是跟前几篇博客介绍的组件都差不多,所以这里不再复述。defaults是这样定义的:

var DEFAULTS = $.extend({}, ListViewBase.DEFAULTS, {
    //列表容器的选择器
    dataListSelector: ‘.data_list‘,
    //顶部加载中的html
    loadingTopHtml: ‘<div class="loading_top">加载中...</div>‘,
    //没有结果的html
    noContentHtml: ‘<div class="no_content">没有找到相关记录:(</div>‘,
    //底部加载中的html
    loadingBottomHtml: ‘<div class="loading_bottom">加载中...</div>‘,
    //没有更多的html
    noMoreHtml: ‘<div class="no_more">没有更多了</div>‘,
    //加载更多的html
    loadMoreHtml: ‘<a href="javascript:;" class="btn_load_more">加载更多</a>‘
});

主要是用来定义滚动列表的那几个组成部分。如果不想用默认值,那么在实例化组件的时候,传入想要设置的option就行了。

scrollListView继承了listViewBase,为了增加自己的初始化逻辑,所以用到了initMiddle这个模板方法,并在其中做了一些jq对象缓存,以及内部状态管理初始化的逻辑:

initMiddle: function () {
    var opts = this.options,
        $element = this.$element,
        $data_list = this.$data_list = $element.find(opts.dataListSelector),
        $load_more = this.$load_more = $(opts.loadMoreHtml).appendTo($element),
        $no_content = $(opts.noContentHtml).appendTo($element),
        $loading_top = $(opts.loadingTopHtml).prependTo($element),
        $loading_bottom = $(opts.loadingBottomHtml).appendTo($element),
        $no_more = $(opts.noMoreHtml).appendTo($element);

    $load_more.css(‘display‘, ‘block‘);

    //状态管理:初始化完毕,顶部加载中,底部加载中,没有结果,没有更多,加载完毕
    var states = this.states = {
        init: function () {
            $data_list.show();
            $load_more.hide();
            $no_content.hide();
            $loading_top.hide();
            $loading_bottom.hide();
            $no_more.hide();
        },
        loading_top: function () {
            $data_list.show();
            $load_more.hide();
            $no_content.hide();
            $loading_top.show();
            $loading_bottom.hide();
            $no_more.hide();
        },
        loading_bottom: function () {
            $data_list.show();
            $load_more.hide();
            $no_content.hide();
            $loading_top.hide();
            $loading_bottom.show();
            $no_more.hide();
        },
        no_content: function () {
            $data_list.hide();
            $load_more.hide();
            $no_content.show();
            $loading_top.hide();
            $loading_bottom.hide();
            $no_more.hide();
        },
        loaded: function () {
            $data_list.show();
            $load_more.show();
            $no_content.hide();
            $loading_top.hide();
            $loading_bottom.hide();
            $no_more.hide();
        },
        no_more: function () {
            $data_list.show();
            $load_more.hide();
            $no_content.hide();
            $loading_top.hide();
            $loading_bottom.hide();
            $no_more.show();
        },
        ‘set‘: function (action) {
            this.curState = action;
            this[action]();
        },
        isNomore: function () {
            return this.curState == ‘no_more‘;
        },
        isNoContent: function () {
            return this.curState == ‘no_content‘;
        }
    };

    states.set(‘init‘);
},

以上代码中的那个states对象用来实现内部的状态管理,可以看作一个简单的状态机。采用这个做法的原因,一是为了满足最前面介绍滚动列表组件特点时描述的那些需求逻辑,二是为了让这些UI控制逻辑看起来更清晰。有了它,我只要在何时的时机,改变下组件的状态,就能列表组件显示不同的内容了。比较简单好理解。

然后通过createPageView来实现分页组件的初始化逻辑。这里就得使用scrollPageView来实例化了:

createPageView: function () {
    var pageView,
        opts = this.options;

    if (opts.pageView) {
        //初始化分页组件
        delete opts.pageView.onChange;
        pageView = new ScrollPageView($.extend(opts.pageView, {
            $loadMore: this.$load_more,
            $element: this.$element
        }));
    }
    return pageView;
},

然后把scrollPageView需要的几个dom对象以option的形式传给了它。

考虑到滚动列表组件的特殊性,我还用到了listViewBase的其它几个模板方法来实现滚动列表的需求。

首先是beforeQuery:

beforeQuery: function (clear) {
    //如果clear为true,则显示顶部的加载状态,表示正在进行新条件的列表查询
    //否则显示底部的加载状态,表示正在进行翻页查询
    this.states.set(clear ? ‘loading_top‘ : ‘loading_bottom‘);
},

这个方法会接受一个参数clear,为true则表示进行条件查询,否则表示进行翻页查询。这个方法的作用在于查询前显示加载提示。

然后是querySuccess:

querySuccess: function (html, args) {
    var pageView = this.pageView,
        rows = this.originalRows,
        method = args.clear ? ‘html‘ : ‘append‘;//根据查询类型,来决定要如何处理渲染新的数据

    //没有查到结果
    if (rows.length == 0 && pageView.pageIndex == 1) {
        this.states.set(‘no_content‘);
        return;
    }

    //没有更多
    if (rows.length < pageView.pageSize) {
        this.states.set(‘no_more‘);
        html.length && this.$data_list[method](html);
        return;
    }

    //加载完毕
    this.states.set(‘loaded‘);
    this.$data_list[method](html);
},

它用来实现请求成功的后的逻辑,最重要的当然是渲染数据。但是考虑到列表组件的需求,还得根据多方面的参数,判断该把列表设置为什么样的状态。请求成功后的结果无非三种,没有查到数据,没有更多,加载成功。这三个状态,根据分页信息和记录数就能判断清楚,见源码里面if逻辑的写法。

然后是queryError:

queryError: function () {
    this.states.set(‘loaded‘);
},

这个主要是在请求失败的时候,还原列表的状态而已。

最后是afterQuery:

afterQuery: function () {
    if (this.states.isNoContent() || this.states.isNomore()) {
        this.pageView.disable();
    }
}

它在请求完成之后,判断如果是没有数据或者没有更多的状态的话,就禁用掉分页组件,免得用户操作不慎导致还会发出一些查不到数据的请求。

以上就是scrollListView实现的核心了,只有100多行。

再来看scrollPageView。

它的defaults定义如下:

var DEFAULTS = $.extend({}, PageViewBase.DEFAULTS, {
    //加载更多的按钮
    $loadMore: null,
    //滚动元素
    $element: null,
    //滚动区域的目标元素,如果没有传,默认就是window对象,用来注册scroll事件
    $target: null,
    //是否启用滚动翻页
    scrollPage: true,
    //滚动元素底边跟滚动可视区域底边的距离,它是滚动翻页的临界点
    offset: -100,
    //滚动事件的绑定时的延迟时间
    scrollBindDelay: 0,
    //滚动事件的节流间隔
    throttle: 100,
});

应该好理解。offset的作用后面会继续说明,scrollBindDelay是用来延迟滚动事件绑定的。为啥会搞这个,是因为chrome浏览器有个特性,如果在浏览网页的时候,刷新之后,滚动条会恢复到刷新前浏览的位置,并且它这个自动恢复也会触发滚动事件。那么当列表组件初始化完毕之后,很有可能会发出两次查询请求,一次是由初始化调用发出的,一次是由自动恢复的滚动事件发出的。所以加上这个option,有利于控制列表初始化后的首次请求。$loadMore用于注册点击事件,添加手动翻页的逻辑。$element表示滚动列表相关的dom对象。$target表示滚动相对的目标对象,如果不传,就指向window对象。

scrollPageView内部提供了简单的节流函数来做滚动事件回调的节流控制:

//简单函数节流
function throttle(func, interval) {
    var last = Date.now();
    return function () {
        var now = Date.now();
        if ((now - last) > interval) {
            func.apply(this, arguments);
            last = now;
        }
    }
}

也提供了获取css属性在浏览器重绘之后的值的函数:

//用来获取css某个属性经过浏览器重绘之后的值
function getComputedValue(element, prop) {
    var computedStyle = window.getComputedStyle(element, null);
    if (!computedStyle) return null;
    if (computedStyle.getPropertyValue) {
        return computedStyle.getPropertyValue(prop);
    } else if (computedStyle.getAttribute) {
        return computedStyle.getAttribute(prop);
    } else if (computedStyle[prop]) {
        return computedStyle[prop];
    }
}

其它代码倒是没啥好补充的,重点看下滚动事件相关的翻页控制逻辑,我就只贴了相关的匿名函数代码了:

function () {
    if (that.disabled) return;

    var targetHeight, bottom;

    //目标元素的clientHeight作为滚动区域的高度
    //bottom:滚动元素的底边到滚动区域顶边的距离

    if (!opts.$target) {
        targetHeight = document.documentElement.clientHeight;
        bottom = opts.$element[0].getBoundingClientRect().bottom;
    } else {
        targetHeight = opts.$target[0].clientHeight;

        var targetRect = opts.$target[0].getBoundingClientRect(),
            targetBorderTop = parseInt(getComputedValue(opts.$target[0], ‘border-top-width‘)),
            elemRect = opts.$element[0].getBoundingClientRect();

        //如果target是其它的html元素,由于都是采用boundingClientRect来计算的,所以要减去目标元素顶部边框的宽度,这样才不会有误差
        bottom = elemRect.bottom - targetRect.top - (isNaN(targetBorderTop) ? 0 : targetBorderTop);
    }

    //bottom+ opts.offset等于一条临界线
    //如果opts.offset小于0,那么这条临界线就位于滚动元素底边再往上opts.offsets的距离的位置
    //如果Opts.offset大于0,那么这条临界线就位于滚动元素底边再往下|opts.offsets|的绝对距离的位置
    //翻页触发的条件是:这条临界线刚好出现在滚动区域里面的时候
    if ((bottom + opts.offset) < targetHeight) {
        pageIndexChange(that.pageIndex + 1, that);
    }
}

以上的思路也比较简单,只要判断列表元素的底部跟目标对象的可视区域的底部达到临界距离即可,这个临界距离就是defaults中定义的offset值。以相对window滚动为例说明如何来做这个判断:

根据上图,可以得知翻页临界的判断条件就是上图中临界线到目标对象可视区域顶边的距离小于目标对象可视区域的高度。这个图虽然是以相对window的滚动情况来说明问题的,但是相对DOM对象进行滚动的判断方式跟这个是一模一样的,只要我们能够得到DOM对象的可视区域高度以及临界线到DOM对象可视区域顶边的距离即可。我的代码中是利用getBoundingClientRect来求的,相当于还是以浏览器的可视区域的顶边作为参考线,不过考虑到普通的DOM对象可能也有顶部边框的问题,在计算最后的临界线到DOM对象可视区域的顶边距离时,减去了DOM对象顶部变宽的宽度。只有这样得出的临界线距离才是相对于DOM对象可视区域顶边而言的。

以上就是本文的全部内容,介绍了我想要补充说明的关于滚动列表组件的部分。这一块内容,我觉得没有特别广的适用性,毕竟各个产品对滚动翻页这种需求的逻辑可能也不尽相同,我这边提供的是我现有项目中的实现思路,可能只能对您有一定的参考价值。所以要是有不妥的,欢迎随时帮我指正出来。谢谢阅读:)

时间: 2024-10-12 11:21:27

列表组件抽象(4)-滚动列表及分页说明的相关文章

列表组件概述(转)

全文转至:http://www.cnblogs.com/lyzg/ 这次要介绍的是列表组件.为了写它,我花了有将近2周的晚上,才一点一点的把它写到现在这个程度.到目前为止,一共写了有17个文件,虽然没有覆盖到一些更复杂的场景,但是把我当时计划写这个组件的基本目的已经完成了.先给大家看看我最后写出来的文件情况: 也许有人会好奇,一个列表的功能怎么会写出这么多东西出来?关于这个问题的答案,我稍后再来总结,先让我描述下我写这些东西之前产生的想法. 1. 背景介绍 我是去年5月份在上家公司开始做的前端开

仿腾讯课堂固定滚动列表ReactNative组件

前言 由于业务需要做成类似腾讯课堂课程详情滚动的效果,考虑到后面有可能有新的呈现方式,RN提供的组件没有这种滚动控件,不如自己封装,其实去年已经写了一篇但是写的比较乱,周末花了点时间重写梳理下做的东西. 项目地址 在这里,如果有好的意见欢迎提 issue或pr. 开始 我们先来看下,腾讯课堂视频播放详情页面是怎么样的? 咋一看界面感觉有点复杂,其实简化来说,这个界面可以看成tab组件+scroll组件.哲学上说,要抓好主要矛盾与次要矛盾,这个问题的主要矛盾是scroll组件实现,也就是最外层的R

jQueryMobile 列表组件与面板组件

1. 列表组件 data-count-theme countTheme 指定数字泡泡的显示风格 data-divider-theme dividerTheme 指定分割线的显示风格 data-filter filter 若为true则列表组件提供过滤器 N/A filterCallback 过滤器列表项的回调函数 data-filter-placeholdr filterPlaceholder 过滤器的占位内容 data-filter-theme filterTheme  过滤器搜索栏显示风格

基于iView的列表组件封装

封装的好处多多,代码便于维护.减少代码量.减少BUG 前台封装以前没有尝试过,这回试试,哈哈 目录 1.列表组件封装 2.树组件封装 3.下拉框组件封装 4.上传组件封装 列表组件的API  属性 说明 类型 默认值  url 请求列表数据的地址 必填  String 无 pagingOption 列表底部是否显示分页信息及总数,有两个配置项 showPaging.showTotal Object 显示分页及总数信息 cols 列定义 必填 Array 无 height 列表高度 选填 Numb

用Vue来实现音乐播放器(十六):滚动列表的实现

滚动列表是一个基础组件  他是基于scroll组件实现的 在base文件夹下面创建一个list-view文件夹 里面有list-view.vue组件   <template> <!-- 当父组件传递给子组件的数据发生变化的时候 scroll可以监听到此时高度会发生变化 --> <!-- 子组件这里的:data和props里面的data相对于 --> <!-- 父传子的时候 data是对应的props里面的值 --> <scroll class=&quo

当滚动列表的时候,让input框失去焦点(移动端会收起键盘)

1.拓展scroll.vue事件 1 beforeScroll:{ 2 type:Boolean, 3 default:false 4 } 5 6 7 if(this.beforeScroll){//滚动列表的时候收起键盘(移动端) 8 this.scroll.on('beforeScrollStart',()=>{ 9 this.$emit('beforeScroll') 10 }) 11 } 2.在suggest.vue里声明beforeScrll:true,并$emit(beforeScr

Android精通:View与ViewGroup,LinearLayout线性布局,RelativeLayout相对布局,ListView列表组件

UI的描述 对于Android应用程序中,所有用户界面元素都是由View和ViewGroup对象构建的.View是绘制在屏幕上能与用户进行交互的一个对象.而对于ViewGroup来说,则是一个用于存放其他View和ViewGroup对象的布局容器! Android为我们提供了View和ViewGroup的两个子类的集合,提供常用的一些输入控件(比如按钮,图片和文本域等)和各种各样的布局模式(比如线程布局,相对布局,绝对布局,帧布局,表格布局等). 用户界面布局 在你APP软件上的,用户界面上显示

ReactNative: 使用列表组件ListView组件

一.简介 在前面介绍过了FlatList列表组件用来展示大量的数据,ListView组件也是同样地功能.虽然ListView组件已经过时,但是作为新手还是有必要了解一下.它们的API差不太多,但是ListView组件使用起来确实要比FlatList列表组件复杂一下.ListView组件是一个垂直滚动列表组件,继承自ScrollView组件,拥有ScrollView组件的全部属性.该组件用简单的数据blob数组填充它,并使用该数据源和`renderRow`回调实例化一个`ListView`组件.

微信小程序实现无限滚动列表(滚动新闻动态列表)

本文实例为大家分享了微信小程序实现无限滚动列表的具体代码,供大家参考,具体内容如下 实现方式是利用小程序原声组件swiper,方向设置为纵向 :vertical=‘true'设置同时显示的滑块数量:display-multiple-items=‘4'设置自动轮播:autoplay:‘true'. 话不所说,直接上代码: <!-- 底部排名 --> <view class='contentBottom'> <view class='BottomFirst'> <te