ScrollView 滚动区域/下拉刷新

Scan me!

用于模拟原生的滚动区域,并支持下拉刷新和加载更多

引入

  1. import { ScrollView, ScrollViewRefresh, ScrollViewMore } from 'mand-mobile'
  2. Vue.component(ScrollView.name, ScrollView)

使用指南

  • ScrollViewRefresh为组件库内置的下拉刷新组件,仅用于作为视觉展示,需在插槽refresh)中使用,下拉刷新组件也可自定义

  • ScrollViewMore为组件库内置的加载更多组件,仅用于作为视觉展示,需在插槽more)中使用,加载更多组件也可自定义

  • 组件容器需具有高度,否则会出现无法滚动或回弹问题。更多使用的常见问题请查看附录)

代码演示

基础 添加内容

请在移动设备中扫码预览

ScrollView 滚动区域/下拉刷新 - 图2

  1. <template>
  2. <div class="md-example-child md-example-child-scroll-view md-example-child-scroll-view-0">
  3. <md-scroll-view
  4. ref="scrollView"
  5. @scroll="$_onScroll"
  6. >
  7. <div
  8. v-for="i in list"
  9. class="scroll-view-item"
  10. :key="i"
  11. @click="$_onItemClick(i)"
  12. >
  13. {{i}}
  14. </div>
  15. </md-scroll-view>
  16. </div>
  17. </template>
  18. <script>
  19. import {ScrollView, Toast} from 'mand-mobile'
  20. export default {
  21. name: 'scroll-view-demo-0',
  22. components: {
  23. [ScrollView.name]: ScrollView,
  24. },
  25. data() {
  26. return {
  27. list: 5,
  28. }
  29. },
  30. mounted() {
  31. window.ScrollViewTrigger0 = () => {
  32. this.addItems()
  33. }
  34. },
  35. methods: {
  36. $_onItemClick(i) {
  37. Toast.info(`Click ${i}`)
  38. },
  39. $_onScroll({scrollLeft, scrollTop}) {
  40. console.log(`[Mand Mobile ScrollView - demo0 - onScroll] scrollLeft: ${scrollLeft}, scrollTop: ${scrollTop}`)
  41. },
  42. addItems() {
  43. this.list += 5
  44. // 如果把autoReflow设置为false, 需调用reflowScroller
  45. this.$refs.scrollView.reflowScroller()
  46. },
  47. },
  48. }
  49. </script>
  50. <style lang="stylus">
  51. .md-example-child-scroll-view-0
  52. height 400px
  53. background #FFF
  54. .scroll-view-item
  55. padding 30px 0
  56. text-align center
  57. font-size 28px
  58. font-family DINAlternate-Bold
  59. border-bottom .5px solid #efefef
  60. </style>

下拉刷新 触发下拉刷新

请在移动设备中扫码预览

ScrollView 滚动区域/下拉刷新 - 图3

        <template>
  <div class="md-example-child md-example-child-scroll-view md-example-child-scroll-view-2">
    <md-scroll-view
      ref="scrollView"
      :scrolling-x="false"
      @refreshing="$_onRefresh"
    >
      <md-scroll-view-refresh
        slot="refresh"
        slot-scope="{ scrollTop, isRefreshActive, isRefreshing }"
        :scroll-top="scrollTop"
        :is-refreshing="isRefreshing"
        :is-refresh-active="isRefreshActive"
      ></md-scroll-view-refresh>
      <div
        v-for="i in list"
        :key="i"
        class="scroll-view-list"
      >
        <p class="scroll-view-item">{{i}}</p>
      </div>
    </md-scroll-view>
  </div>
</template>

<script>
import {ScrollView, ScrollViewRefresh} from 'mand-mobile'

export default {
  name: 'scroll-view-demo-0',
  components: {
    [ScrollView.name]: ScrollView,
    [ScrollViewRefresh.name]: ScrollViewRefresh,
  },
  data() {
    return {
      list: 5,
    }
  },
  mounted() {
    window.ScrollViewTrigger1 = () => {
      this.$refs.scrollView.triggerRefresh()
    }
  },
  methods: {
    $_onRefresh() {
      // async data
      setTimeout(() => {
        this.list += 5
        this.$refs.scrollView.finishRefresh()
      }, 2000)
    },
  },
}

</script>

<style lang="stylus">
.md-example-child-scroll-view-2
  height 800px
  background #FFF
  .scroll-view-item
    padding 30px 0
    text-align center
    font-size 28px
    font-family DINAlternate-Bold
    border-bottom .5px solid #efefef
</style>
      

粘性标题

请在移动设备中扫码预览

ScrollView 滚动区域/下拉刷新 - 图4

        <template>
  <div class="md-example-child md-example-child-scroll-view md-example-child-scroll-view-4">
    <md-scroll-view
      ref="scrollView"
      :scrolling-x="false"
      @scroll="$_onScroll"
    >
      <div
        v-for="i in category"
        :key="i"
        class="scroll-view-category"
      >
        <p class="scroll-view-category-title">{{ i }}</p>
        <div
          v-for="j in list"
          :key="j"
          class="scroll-view-list"
        >
          <p class="scroll-view-item">{{`${i} - ${j}`}}</p>
        </div>
      </div>
    </md-scroll-view>
    <p v-if="activeBlockIndex > 0" class="scroll-view-striky-title">{{ activeBlockIndex }}</p>
  </div>
</template>

<script>
import {ScrollView} from 'mand-mobile'

export default {
  name: 'scroll-view-demo-3',
  components: {
    [ScrollView.name]: ScrollView,
  },
  data() {
    return {
      category: 5,
      list: 5,
      dimensions: [],
      scrollY: 0,
    }
  },
  computed: {
    activeBlockIndex() {
      let activeIndex = -1
      this.dimensions.forEach((dimension, index) => {
        if (this.scrollY >= dimension[0] && this.scrollY <= dimension[1]) {
          activeIndex = index + 1
        }
      })
      return activeIndex
    },
  },
  mounted() {
    // 如果内容发生变化,需重新初始化block和scroller
    this.$_initScrollBlock()
    // this.$refs.scrollView.reflowScroller()
  },
  methods: {
    $_initScrollBlock() {
      const blocks = this.$el.querySelectorAll('.scroll-view-category')
      let offset = 0
      Array.prototype.slice.call(blocks).forEach((block, index) => {
        const innerHeight = block.clientHeight
        this.$set(this.dimensions, index, [offset, offset + innerHeight])
        offset += innerHeight
      })
    },
    $_onScroll({scrollTop}) {
      this.scrollY = scrollTop
    },
  },
}

</script>

<style lang="stylus">
.md-example-child-scroll-view-4
  position relative
  height 800px
  background #FFF
  .scroll-view-striky-title
    position absolute
    top 0
    left 0
    right 0
  .scroll-view-category-title, .scroll-view-striky-title
    padding 10px 0
    text-align center
    font-size 32px
    font-family DINAlternate-Bold
    background-color #f0f0f0
  .scroll-view-item
    padding 30px 0
    text-align center
    font-size 32px
    border-bottom .5px solid #efefef
</style>
      

手动初始化

请在移动设备中扫码预览

ScrollView 滚动区域/下拉刷新 - 图5

        <template>
  <div class="md-example-child md-example-child-scroll-view md-example-child-scroll-view-6">
    <md-tabs
      @change="$_onTabChange"
      immediate
    >
      <md-tab-pane class="content" name="scrollView0" label="Block - 1">
        <md-scroll-view
          ref="scrollView0"
          :scrolling-x="false"
          manual-init
          @refreshing="$_onRefresh(0)"
        >
          <md-scroll-view-refresh
            slot="refresh"
            slot-scope="{ scrollTop, isRefreshActive, isRefreshing }"
            :scroll-top="scrollTop"
            :is-refreshing="isRefreshing"
            :is-refresh-active="isRefreshActive"
          ></md-scroll-view-refresh>
          <div
            v-for="i in list0"
            :key="i"
            class="scroll-view-list"
          >
            <p class="scroll-view-item">{{`1 - ${i}`}}</p>
          </div>
        </md-scroll-view>
      </md-tab-pane>
      <md-tab-pane class="content" name="scrollView1" label="Block - 2">
        <md-scroll-view
          ref="scrollView1"
          :scrolling-x="false"
          manual-init
          @refreshing="$_onRefresh(1)"
        >
          <md-scroll-view-refresh
            slot="refresh"
            slot-scope="{ scrollTop, isRefreshActive, isRefreshing }"
            :scroll-top="scrollTop"
            :is-refreshing="isRefreshing"
            :is-refresh-active="isRefreshActive"
          ></md-scroll-view-refresh>
          <div
            v-for="i in list1"
            :key="i"
            class="scroll-view-list"
          >
            <p class="scroll-view-item">{{`2 - ${i}`}}</p>
          </div>
        </md-scroll-view>
      </md-tab-pane>
    </md-tabs>
  </div>
</template>

<script>
import {Tabs, TabPane, ScrollView, ScrollViewRefresh} from 'mand-mobile'

export default {
  name: 'scroll-view-demo-6',
  components: {
    [Tabs.name]: Tabs,
    [TabPane.name]: TabPane,
    [ScrollView.name]: ScrollView,
    [ScrollViewRefresh.name]: ScrollViewRefresh,
  },
  data() {
    return {
      list0: 5,
      list1: 5,
      isFinished: false,
    }
  },
  methods: {
    $_onRefresh(index) {
      // async data
      setTimeout(() => {
        this[`list${index}`] += 5
        this.$refs[`scrollView${index}`].finishRefresh()
      }, 2000)
    },
    $_onTabChange(tab) {
      console.log(tab.name)
      this.$refs[tab.name].init()
    },
  },
}

</script>

<style lang="stylus">
.md-example-child-scroll-view-6
  background #FFF
  .content
    height 800px
  .md-tab-bar
    box-shadow 0 2px 8px #f0f0f0
  .scroll-view-item
    padding 30px 0
    text-align center
    font-size 32px
    font-family DINAlternate-Bold
    border-bottom .5px solid #efefef
</style>
      

横向滚动

isPrevent false, touchAngle 80

ScrollView 滚动区域/下拉刷新 - 图6

        <template>
  <div class="md-example-child md-example-child-scroll-view md-example-child-scroll-view-1">
    <md-scroll-view
      ref="scrollView"
      :scrolling-y="false"
      :touch-angle="80"
      :is-prevent="false"
    >
      <div class="scroll-view-list">
        <p
          v-for="i in list"
          :key="i"
          class="scroll-view-item"
        >{{i}}</p>
      </div>
    </md-scroll-view>
  </div>
</template>

<script>
import {ScrollView} from 'mand-mobile'

export default {
  name: 'scroll-view-demo-0',
  components: {
    [ScrollView.name]: ScrollView,
  },
  data() {
    return {
      list: 5,
    }
  },
}

</script>

<style lang="stylus">
.md-example-child-scroll-view-1
  height 100px
  background #FFF
  .md-scroll-view
    display flex
    align-items center
    .scroll-view-list
      display flex
      width 1000px
      .scroll-view-item
        flex 1
        text-align center
        font-size 28px
        font-family DINAlternate-Bold
        border-right .5px solid #efefef
</style>
      

加载更多

请在移动设备中扫码预览

ScrollView 滚动区域/下拉刷新 - 图7

        <template>
  <div class="md-example-child md-example-child-scroll-view md-example-child-scroll-view-3">
    <md-scroll-view
      ref="scrollView"
      :scrolling-x="false"
      @end-reached="$_onEndReached"
    >
      <div
        v-for="i in list"
        :key="i"
        class="scroll-view-list"
      >
        <p class="scroll-view-item">{{i}}</p>
      </div>
      <md-scroll-view-more
        slot="more"
        :is-finished="isFinished"
      >
      </md-scroll-view-more>
    </md-scroll-view>
  </div>
</template>

<script>
import {ScrollView, ScrollViewMore} from 'mand-mobile'

export default {
  name: 'scroll-view-demo-2',
  components: {
    [ScrollView.name]: ScrollView,
    [ScrollViewMore.name]: ScrollViewMore,
  },
  data() {
    return {
      list: 10,
      isFinished: false,
    }
  },
  methods: {
    $_onEndReached() {
      if (this.isFinished) {
        return
      }
      // async data
      setTimeout(() => {
        this.list += 5
        if (this.list >= 20) {
          this.isFinished = true
        }
        this.$refs.scrollView.finishLoadMore()
      }, 1000)
    },
  },
}

</script>

<style lang="stylus">
.md-example-child-scroll-view-3
  height 800px
  background #FFF
  .scroll-view-item
    padding 30px 0
    text-align center
    font-size 32px
    font-family DINAlternate-Bold
    border-bottom .5px solid #efefef
</style>
      

配合TabBar

请在移动设备中扫码预览

ScrollView 滚动区域/下拉刷新 - 图8

        <template>
  <div class="md-example-child md-example-child-scroll-view md-example-child-scroll-view-5">
    <md-tab-bar
      v-model="activeBlockIndex"
      :items="tabBarItems"
      :max-length="5"
      ref="tabBar"
      @change="$_onTabChange"
    ></md-tab-bar>
    <md-scroll-view
      class="scroll-view-with-tab-bar"
      ref="scrollView"
      :scrolling-x="false"
      @scroll="$_onScroll"
      @mousedown.native="$_onScrollStart"
      @touchstart.native="$_onScrollStart"
    >
      <div
        v-for="i in category"
        :key="i"
        class="scroll-view-category"
      >
        <div
          v-for="j in list"
          :key="j"
          class="scroll-view-list"
        >
          <p class="scroll-view-item">{{`${i} - ${j}`}}</p>
        </div>
      </div>
    </md-scroll-view>
  </div>
</template>

<script>
import {ScrollView, TabBar} from 'mand-mobile'

const debounce = function(fn, delay) {
  let timer
  return function() {
    const context = this
    timer && clearTimeout(timer)

    timer = setTimeout(function() {
      fn.apply(context, arguments)
    }, delay)
  }
}
export default {
  name: 'scroll-view-demo-3',
  components: {
    [ScrollView.name]: ScrollView,
    [TabBar.name]: TabBar,
  },
  data() {
    return {
      category: 5,
      list: 5,
      dimensions: [],
      scrollY: 0,
      isManual: false,
      activeBlockIndex: 0,
    }
  },
  computed: {
    tabBarItems() {
      return this.dimensions.map((item, index) => {
        return {name: index, label: `Block - ${index + 1}`}
      })
    },
  },
  mounted() {
    // 如果内容发生变化,需重新初始化block和scroller
    this.$_initScrollBlock()
    // this.$refs.scrollView.reflowScroller()
  },
  methods: {
    $_initScrollBlock() {
      const blocks = this.$el.querySelectorAll('.scroll-view-category')

      let offset = 0
      Array.prototype.slice.call(blocks).forEach((block, index) => {
        const innerHeight = block.clientHeight
        this.$set(this.dimensions, index, [offset, offset + innerHeight])
        offset += innerHeight
      })

      // setTimeout(() => {
      //   this.$refs.tabBar.reflow()
      // }, 1000)
    },
    $_onScrollStart() {
      this.isManual = false
    },
    $_onScroll({scrollTop}) {
      if (!this.isManual) {
        this.dimensions.some((dimension, index) => {
          if (scrollTop >= dimension[0] && scrollTop <= dimension[1]) {
            this.activeBlockIndex = index
            return true
          }
        })
      }
    },
    $_onTabChange(item, index) {
      this.isManual = true
      debounce(() => {
        const offsetTop = this.dimensions[index][0]
        this.$refs.scrollView.scrollTo(0, offsetTop, true)
        this.scrollY = offsetTop
      }, 100)()
    },
  },
}

</script>

<style lang="stylus">
.md-example-child-scroll-view-5
  position relative
  height 800px
  background #FFF
  .md-tab-bar
    position absolute
    left 0
    top 0
    right 0
    z-index 2
    box-shadow 0 2px 8px #f0f0f0
  .scroll-view-with-tab-bar
    & > .scroll-view-container
      padding-top 100px
    .scroll-view-item
      padding 30px 0
      text-align center
      font-size 32px
      border-bottom .5px solid #efefef
</style>
      

API

ScrollView Props

属性说明类型默认值备注
scrolling-x水平滚动Booleantrue-
scrolling-y垂直滚动Booleantrue-
bouncing可回弹Booleantrue-
auto-reflow内容发生变化时自动重置滚动区域尺寸Booleanfalse当设置为false时,内容发生变化需手动调用reflowScroller
manual-init手动初始化Booleanfalse一般用于异步初始化的场景,需手动调用init方法完成初始化
end-reached-threshold触发到达底部的提前量Number0单位px
immediate-check-end-reaching 2.1.0+初始化时立即触发是否到达底部检查Booleanfalse-
touch-angle 2.1.0+触发滚动的角度范围Number45单位deg
is-prevent 2.3.0+阻止浏览器默认滚动Booleantrue如果设置为false,当在非可滚动角度范围触发滚动时会触发浏览器默认滚动

ScrollView TouchAngle

ScrollView 滚动区域/下拉刷新 - 图9

ScrollViewRefresh Props

属性说明类型默认值备注
scroll-top距离顶部距离Number0单位px
is-refresh-active释放可刷新状态Booleanfalse-
is-refreshing刷新中状态Booleanfalse-
refresh-text待刷新文案String下拉刷新-
refresh-active-text释放可刷新文案String释放刷新-
refreshing-text刷新中文案String刷新中…-
roller-color 2.2.0+进度条颜色String#2F86F6-

ScrollViewMore Props

属性说明类型默认值备注
is-finished全部已加载Booleanfalse-
loading-text加载中文案String更多加载中…-
finished-text全部已加载文案String全部已加载-

ScrollView Slots

default

滚动区域内容插槽,当内容发生变化是,需要调用reflowScroller重置滚动区域,参考reflowScroller)

refresh

下拉刷新组件插槽,可如下使用slot-scoped获取相关滚动状态(不兼容slot-scoped时滚动状态也可通过事件中动态设置)

<md-scroll-view-refresh
  slot="refresh"
  slot-scope="{ scrollTop, isRefreshActive, isRefreshing }"
  :scroll-top="scrollTop"
  :is-refreshing="isRefreshing"
  :is-refresh-active="isRefreshActive"
></md-scroll-view-refresh>
more

加载更多组件插槽

header

吸顶区域插槽

吸底区域插槽

ScrollView Methods

init()

初始化滚动区域,当manual-init设置为true时使用

reflowScroller()

重置滚动区域,一般滚动区域中的内容发生变化之后需调用

scrollTo(left, top, animate = false)

滚动至指定位置

参数说明类型
left距左侧距离Number
top距顶部距离Number
animate使用动画Boolean
triggerRefresh()

触发下拉刷新

finishRefresh()

停止下拉刷新

finishLoadMore()

停止加载更多

ScrollView Events

@scroll({scrollLeft, scrollTop})

滚动事件

属性说明类型
scrollLeft距左侧距离Number
scrollTop距顶部距离Number
@refreshActive()

释放可刷新事件

@refreshing()

刷新中事件

@end-reached()

滚动触底事件

附录

无法正常滚动且异常回弹

首先,大多数滚动异常的情况是由于容器尺寸(垂直滚动:高度,水平滚动:宽度)的问题导致,容器的高度可以通过固定尺寸流式布局flex布局等多种方式控制,当容器尺寸不足时会导致内部Scroller初始化异常。当出现此类情况时,可通过浏览器元素查看器检查容器元素的.md-scroll-view高度是否正确。

其次,确认是否存在动态变更滚动区域内容,导致滚动区域尺寸变化,此时需调用reflowScroller或者直接将auto-reflow设置为true

下拉刷新后滚动无法触发endReached

在组件内部下拉刷新上拉加载应该视为两个无关联的动作,因为动作内容有用户决定(业务逻辑),故无法确定下拉刷新一定是"刷新列表回第一页的状态",所以无法直接在下拉刷新的时候控制isEndReaching。该问题可以抽象为下拉刷新时需将上拉加载的状态重置,可以在refreshing事件去手动重置:

$_onRefresh() {
  // 重置列表数据
  this.list = 10
  this.$refs.scrollView.finishRefresh()
  // 重置“上拉加载”的状态
  this.isFinished = false
  this.$refs.scrollView.finishLoadMore()
}