react + iscroll5 实现完美 下拉刷新,上拉加载

经过几天的反复折腾,总算做出一个体验还不错的列表页了,主要支持了下拉刷新,上拉加载两个功能。

一开始直接采用了react-iscroll插件,它是基于iscroll插件开发的组件。但是开发过程中,发现它内部封装的行为非常固化,限制了我对iscroll的控制能力,因此我转而直接基于iscroll插件实现。

网上也有一些基于浏览器原生滚动条实现的方案,找不到特别好的博客说明,而iscroll是基于Js模拟的滚动条(滚动条也是一个div哦),其兼容性更好,所以还是选择iscroll吧。

先体验效果

在讲解实现之前,可以先体验一下app整体效果。如果使用桌面浏览器访问,必须进入开发者模式,启动手机仿真,并使用鼠标左键触发滑动,否则无法达到真机效果(点我进入)!建议还是扫描二维码直接在手机浏览器中体验,二维码如下:

下载demo源码

点击这里下载源码,之后一起看一下实现中需要注意的事项和思路。

实现关键点

本篇实现了MsgListPage这个组件,支持消息列表的滚动查看,下拉刷新,上拉加载功能。

这里使用了开源的iscroll5实现滚动功能,它对iscroll4重构并修复若干bug,是目前主流版本。网上鲜有iscroll5实现下拉刷新,上拉加载功能的好例子,提供的仅是一些思路,绝大多数实现都是修改iscroll5源码,并不完美。我这次的实现不需要修改iscroll5源码,其实通过巧妙的设计是可以完美的实现这些特效的。

代码如下:

import React from "react";
import {Link} from "react-router";
import $ from "jquery";
import style from "./MsgListPage.css";
import iScroll from "iscroll/build/iscroll-probe"; // 只有这个库支持onScroll,从而支持bounce阶段的事件捕捉

export default class MsgListPage extends React.Component {
    constructor(props, context) {
        super(props, context);
        this.state = {
            items: [],
            pullDownStatus: 3,
            pullUpStatus: 0,
        };

        this.page = 1;
        this.itemsChanged = false;

        this.pullDownTips = {
            // 下拉状态
            0: ‘下拉发起刷新‘,
            1: ‘继续下拉刷新‘,
            2: ‘松手即可刷新‘,
            3: ‘正在刷新‘,
            4: ‘刷新成功‘,
        };

        this.pullUpTips = {
            // 上拉状态
            0: ‘上拉发起加载‘,
            1: ‘松手即可加载‘,
            2: ‘正在加载‘,
            3: ‘加载成功‘,
        };

        this.isTouching = false;

        this.onItemClicked = this.onItemClicked.bind(this);

        this.onScroll = this.onScroll.bind(this);
        this.onScrollEnd = this.onScrollEnd.bind(this);

        this.onTouchStart = this.onTouchStart.bind(this);
        this.onTouchEnd = this.onTouchEnd.bind(this);
    }

    componentDidMount() {
        const options = {
            // 默认iscroll会拦截元素的默认事件处理函数,我们需要响应onClick,因此要配置
            preventDefault: false,
            // 禁止缩放
            zoom: false,
            // 支持鼠标事件,因为我开发是PC鼠标模拟的
            mouseWheel: true,
            // 滚动事件的探测灵敏度,1-3,越高越灵敏,兼容性越好,性能越差
            probeType: 3,
            // 拖拽超过上下界后出现弹射动画效果,用于实现下拉/上拉刷新
            bounce: true,
            // 展示滚动条
            scrollbars: true,
        };
        this.iScrollInstance = new iScroll(`#${style.ListOutsite}`, options);
        this.iScrollInstance.on(‘scroll‘, this.onScroll);
        this.iScrollInstance.on(‘scrollEnd‘, this.onScrollEnd);

        this.fetchItems(true);
    }

    fetchItems(isRefresh) {
        if (isRefresh) {
            this.page = 1;
        }
        $.ajax({
            url: ‘/msg-list‘,
            data: {page: this.page},
            type: ‘GET‘,
            dataType: ‘json‘,
            success: (response) => {
                if (isRefresh) {    // 刷新操作
                    if (this.state.pullDownStatus == 3) {
                        this.setState({
                            pullDownStatus: 4,
                            items: response.data.items
                        });
                        this.iScrollInstance.scrollTo(0, -1 * $(this.refs.PullDown).height(), 500);
                    }
                } else {    // 加载操作
                    if (this.state.pullUpStatus == 2) {
                        this.setState({
                            pullUpStatus: 0,
                            items: this.state.items.concat(response.data.items)
                        });
                    }
                }
                ++this.page;
                console.log(`fetchItems=effected isRefresh=${isRefresh}`);
            }
        });
    }

    /**
     * 点击跳转详情页
     */
    onItemClicked(ev) {
        // 获取对应的DOM节点, 转换成jquery对象
        let item = $(ev.target);
        // 操作router实现页面切换
        this.context.router.push(item.attr(‘to‘));
        this.context.router.goForward();
    }

    onTouchStart(ev) {
        this.isTouching = true;
    }

    onTouchEnd(ev) {
        this.isTouching = false;
    }

    onPullDown() {
        // 手势
        if (this.isTouching) {
            if (this.iScrollInstance.y > 5) {
                this.state.pullDownStatus != 2 && this.setState({pullDownStatus: 2});
            } else {
                this.state.pullDownStatus != 1 && this.setState({pullDownStatus: 1});
            }
        }
    }

    onPullUp() {
        // 手势
        if (this.isTouching) {
            if (this.iScrollInstance.y <= this.iScrollInstance.maxScrollY - 5) {
                this.state.pullUpStatus != 1 && this.setState({pullUpStatus: 1});
            } else {
                this.state.pullUpStatus != 0 && this.setState({pullUpStatus: 0});
            }
        }
    }

    onScroll() {
        let pullDown = $(this.refs.PullDown);

        // 上拉区域
        if (this.iScrollInstance.y > -1 * pullDown.height()) {
            this.onPullDown();
        } else {
            this.state.pullDownStatus != 0 && this.setState({pullDownStatus: 0});
        }

        // 下拉区域
        if (this.iScrollInstance.y <= this.iScrollInstance.maxScrollY + 5) {
            this.onPullUp();
        }
    }

    onScrollEnd() {
        console.log("onScrollEnd" + this.state.pullDownStatus);

        let pullDown = $(this.refs.PullDown);

        // 滑动结束后,停在刷新区域
        if (this.iScrollInstance.y > -1 * pullDown.height()) {
            if (this.state.pullDownStatus <= 1) {   // 没有发起刷新,那么弹回去
                this.iScrollInstance.scrollTo(0, -1 * $(this.refs.PullDown).height(), 200);
            } else if (this.state.pullDownStatus == 2) { // 发起了刷新,那么更新状态
                this.setState({pullDownStatus: 3});
                this.fetchItems(true);
            }
        }

        // 滑动结束后,停在加载区域
        if (this.iScrollInstance.y <= this.iScrollInstance.maxScrollY) {
            if (this.state.pullUpStatus == 1) { // 发起了加载,那么更新状态
                this.setState({pullUpStatus: 2});
                this.fetchItems(false);
            }
        }
    }

    shouldComponentUpdate(nextProps, nextState) {
        // 列表发生了变化, 那么应该在componentDidUpdate时调用iscroll进行refresh
        this.itemsChanged = nextState.items !== this.state.items;
        return true;
    }

    componentDidUpdate() {
        // 仅当列表发生了变更,才调用iscroll的refresh重新计算滚动条信息
        if (this.itemsChanged) {
            this.iScrollInstance.refresh();
        }
        return true;
    }

    render() {
        let lis = [];
        this.state.items.forEach((item, index) => {
            lis.push(
                <li key={index} to={`/msg-detail-page/${index}`} onClick={this.onItemClicked}>
                    {item.title}{index}
                </li>
            );
        })

        // 外层容器要固定高度,才能使用滚动条
        return (
            <div id={style.ScrollContainer}>
                <div id={style.ListOutsite} style={{height: window.innerHeight}}
                     onTouchStart={this.onTouchStart} onTouchEnd={this.onTouchEnd}>
                    <ul id={style.ListInside}>
                        <p ref="PullDown" id={style.PullDown}>{this.pullDownTips[this.state.pullDownStatus]}</p>
                        {lis}
                        <p ref="PullUp" id={style.PullUp}>{this.pullUpTips[this.state.pullUpStatus]}</p>
                    </ul>
                </div>

            </div>
        );
    }
}

MsgListPage.contextTypes = {
    router: () => { React.PropTypes.object.isRequired }
};

  

思路

  • 在react的componentDidMount回调中,DOM已经渲染完成。此时进行iscroll插件的初始化,监听其scroll和scrollEnd两个插件回调用于滚动监听,同时,调用fetchItems发起首次数据加载。
  • 在react的shouldComponentUpdate回调中,我判断并记录本次render是否对ul的元素进行了增删,从而在componentDidUpdate回调中决策是否需要为iscroll进行refresh刷新,因为如果iscroll容器内的元素数量发生变动,iscroll是需要重新计算整个高度等信息的。
  • 为了获知用户是否在触屏,我给div注册了onTouchStart和onTouchEnd两个事件函数,这主要是为了区分滚动条是因为触屏拖拽移动,还是因为惯性移动。
  • 在iscroll的onScroll回调中,专门处理用户的触屏行为。我判断y坐标确认当前滚动条所处的范围是顶部的上拉区域,还是底部的下拉区域。当处于上拉区域中的时候,根据拖拽的偏移量展现不同的文案,下拉区域也是一样。
  • 在iscroll的onScrollEnd回调中,专门处理滚动结束后的状态判断,主要是判断用户是否此前的触屏行为是否触发了下载需求,如果产生了下载需求那么发起网络调用fetchItems。
  • 需要注意,下拉刷新条也位于iscroll容器内,在它能被用户可见但又没有抵达刷新触发偏移量之前,如果用户没有触屏那么应该立即向上滚动把下拉提示条滚到视野范围外。上拉加载条也位于iscroll容器内,但是它总是可以被用户看见,所以对应的处理逻辑相对简单。
  • 不要在onScroll内调用scrollTo等移动滚动条的函数,因为onScroll内调用ScrollTo会导致继续回调onScroll,如此往复像在打乒乓球,是不合理的。我的实现中,onScroll仅仅检测用户的触屏行为(不处理惯性滑动),而onScrollEnd中才进行对应的逻辑处理或者发起scrollTo,而scrollTo触发的是惯性滑动(isTouching=false),因而又不会造成onScroll的困扰。
  • 点击某一行会跳转到MsgDetailPage组件,这是通过注册onClick事件回调,并通过this.context.router操作react-router的路由实现的切换。
  • 如果iscroll内元素太少没有产生滚动条,那么会影响上述的效果实现逻辑。因此,我给<ul>元素设置了min-height:150%的高度,也就是最小溢出iscroll容器50%,保证滚动条总是存在,并且刷新提示条 有足够的滚动范围逃离用户视线。
  • 如果你在手机浏览器里上下拖拽,有时候会发现页面整体在移动,而不是滚动条滚动。为了解决这个问题,我在react的根容器里,捕获了body的touchmove事件,调用了preventDefault()阻止了浏览器默认行为。

必须注意,所有的网络请求都是模拟的,并没有动态的后端计算。

本文实现了非常有意思的动画效果,也非常实用。

另外,第3个组件『留言提交页』因为精力原因,不打算继续写完了。

当前访问路径如果是:列表页 -> 详情页 -> 返回列表页,会发现列表页内容重新刷新了,滚动条也没有停留在原先的位置上。这是因为每次路由切换,都是重新分配一个component对象进行重新渲染,所以状态没有保存,我当然可以在跳转详情页之前把列表页的状态保存到一个全局变量里或者localStorage里,但是这毕竟比较麻烦。

为了实现状态保存,redux就是在做类似的框架级支持,所以我可能接下来真的要学学redux了,学无止境,太可怕!

时间: 2024-11-05 13:40:20

react + iscroll5 实现完美 下拉刷新,上拉加载的相关文章

最新Android ListView 下拉刷新 上滑加载

开发项目过程中基本都会用到listView的下拉刷新和上滑加载更多,之前大家最常用的应该是pull to refresh或它的变种版吧,google官方在最新的android.support.v4包中增加了一个新类SwipeRefreshLayout,地址 这个类的作用就是提供官方的下拉刷新,并且效果相当不错,而上拉加载更多则用我们自定义的listview,也是相当简单. 下拉刷新 简单的介绍下: 首先它是一个viewgroup,但是它只允许有一个子控件,子控件能是任何view,使用的时候,所在

十分钟实现ListView下拉刷新上滑加载更多

说到ListView下拉刷新几乎每个APP都会用到,所以ListView下拉刷新是很重要的,就像ListView优化一样是你必会的东西. ListView实现下拉刷新如果我们开发人员自己编写相对来说比较费事的,当我们使用第三方库之后我们再来开发这个功能就会省事很多.相比与自己实现可以少编写不少代码,Android-PullToRefresh库可以轻松实现ListView的下拉刷新功能. 要使用Android—PullToRefesh库对ListView实现下拉刷新要经过以下几个步骤: 1.下载A

Android 下拉刷新上啦加载SmartRefreshLayout + RecyclerView

在弄android刷新的时候,可算是耗费了一番功夫,最后发觉有现成的控件,并且非常好用,这里记录一下. 原文是 https://blog.csdn.net/huangxin112/article/details/78781682 ,这里是看了之后,结合自己实际遇到的问题写的. 首先引入包. //下拉框 implementation 'com.android.support:recyclerview-v7:28.0.0-beta01' implementation 'com.scwang.smar

Android 下拉刷新上拉加载 多种应用场景 超级大放送(上)

转载请标明原文地址:http://blog.csdn.net/yalinfendou/article/details/47707017 关于Android下拉刷新上拉加载,网上的Demo太多太多了,这里不是介绍怎么去实现下拉刷新上拉加载,而是针对下拉刷新上拉加载常用的一些应用场景就行了一些总结,包含了下拉刷新上拉加载过程中遇到的一些手势冲突问题的解决方法(只能算是抛砖引玉). 去年9月的时候,那时自己正在独立做Android项目.记得刚刚写完那个ListView列表页面(木有下拉刷新,上拉加载)

vue10行代码实现上拉翻页加载更多数据,纯手写js实现下拉刷新上拉翻页不引用任何第三方插件

vue10行代码实现上拉翻页加载更多数据,纯手写js实现下拉刷新上拉翻页不引用任何第三方插件/库 一提到移动端的下拉刷新上拉翻页,你可能就会想到iScroll插件,没错iScroll是一个高性能,资源占用少,无依赖,多平台的javascript滚动插件.iScroll不仅仅是 滚动.它可以处理任何需要与用户进行移动交互的元素.在你的项目中包含仅仅4kb大小的iScroll,你的项目便拥有了滚动,缩放,平移,无限滚动,视差滚动,旋转功能.iScroll的强大毋庸置疑,本人也非常欢迎大家使用iScr

SwipeRefreshLayout+RecyclerView实现下拉刷新上拉自动加载

在实际开发中,为了节省开发周期,下拉刷新上拉加载通常都会采取使用一些第三方库,典型的就是用PullToRefresh,XListView等等,还有就是谷歌推荐的SwipeRefreshLayout,可惜没有上拉加载功能,需要自己去实现一个上拉加载的脚View,再加上现在代替ListView的RecyclerView+CardView使用的频率也是也来也高,不得不说,CardView效果确实很好看,一个一个的小卡片,用户体验好,I like it!!!废话不说了,奔主题!今天也玩了一下SwipeR

Android 下拉刷新上拉载入 多种应用场景 超级大放送(上)

转载请标明原文地址:http://blog.csdn.net/yalinfendou/article/details/47707017 关于Android下拉刷新上拉载入,网上的Demo太多太多了,这里不是介绍怎么去实现下拉刷新上拉载入,而是针对下拉刷新上拉载入经常使用的一些应用场景即可了一些总结,包括了下拉刷新上拉载入过程中遇到的一些手势冲突问题的解决方法(仅仅能算是抛砖引玉). 去年9月的时候.那时自己正在独立做Android项目. 记得刚刚写完那个ListView列表页面(木有下拉刷新,上

android 安卓 listview 支持下拉刷新 上拉加载更多

[1]重写listView import java.text.SimpleDateFormat; import java.util.Date; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewGrou

带你实现开发者头条APP(五)--RecyclerView下拉刷新上拉加载

转载请注明出处:http://blog.csdn.net/lowprofile_coding/article/details/51321896 一 .前言 最近实在太忙,一个多礼拜没有更新文章了,于是今晚加班加点把demo写出来,现在都12点了才开始写文章. 1.我们的目标 把RecyclerView下拉刷新上拉加载更多加入到我们的开发者头条APP中. 2.效果图 3.实现步骤 找一个带上拉刷新下载加载更多的RecyclerView开源库,我们要站在巨人的肩膀上 下载下来自己先运行下demo,然

android 下拉刷新上拉加载更多,高仿ios左滑动删除item,解决了众多手势问题

一.前言 老规矩,别的不说,这demo是找了很相关知识集合而成的,可以说对我这种小白来说是绞尽脑汁!程序员讲的是无图无真相!现在大家一睹为快! 二.比较关键的还是scroller这个类的 package com.icq.slideview.view; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; i