HarmonyOS鸿蒙Next中急急急急,Swiper 嵌套 Flex 再嵌套多个 List 滚动无法保持问题

HarmonyOS鸿蒙Next中急急急急,Swiper 嵌套 Flex 再嵌套多个 List 滚动无法保持问题 cke_196596.png

这是一个 Swiper 嵌套多个 Flex,然后每个 Flex 里面会嵌套 3个 List,然后每个 List 里面会嵌套 多个ListItem,每个 List 都是开启了垂直滚动。

现在的逻辑是,点击每个 ListItem ,会重新计算 pages 数据,然后重新渲染,这些逻辑都没问题,有问题的是重新渲染后 List 滚动条的位置保持不了,尝试过记录滚动条位置,然后再还原,发现还是会闪动,有什么其他方法吗?

import { AppModel } from "../../../../../../models/AppModel"
import { AppStorageV2, LengthMetrics, PersistenceV2 } from "@kit.ArkUI"
import { CacheModel } from "../../../../../../models/CacheModel"
import { ResearchService } from "../../../../../../services/ResearchService"
import { Base64Util, CryptoHelper, CryptoUtil, FileUtil, JSONUtil, MD5 } from "@pura/harmony-utils"
import { TreeNodeModel } from "../../../../../../models/TreeModel"
import fs from '@ohos.file.fs';
import { LogUtil } from "../../../../../../utils/LogUtil"
import { PageLoading } from "../../../../../../components/PageLoading"
import { TreeChangeEvent } from "../../../../../../events/TreeChangeEvent"
import { showToast } from "../../../../../../utils/HelperUtil"
import { drawing } from "@kit.ArkGraphics2D"
import { UserEvent } from "../../../../../../events/UserEvent"
import { TreeReloadEvent } from "../../../../../../events/TreeReloadEvent"

@ComponentV2
export struct Tree {
  @Local appModel: AppModel =
    AppStorageV2.connect(AppModel, () => new AppModel())!
  @Local cacheModel: CacheModel =
    PersistenceV2.connect(CacheModel, () => new CacheModel())!
  @Local loading: boolean = true
  @Local cacheFilePath: string = FileUtil.getCacheDirPath('tree', 'cache.json', false)
  @Local originDataset: TreeNodeModel[] = []
  @Local dataset: TreeNodeModel[] = []
  @Local columns: TreeNodeModel[][] = []
  @Local currentPage: number = 0
  @Local pages: TreeNodeModel[][][] = []
  @Local topScroller: ListScroller = new ListScroller()
  @Local swiperController: SwiperController = new SwiperController()
  @Local scrollerList: ListScroller[][] = []
  @Local scrollerPosition: Record<string, number>[][] = []

  aboutToAppear(): void {
    TreeChangeEvent.on(this.handleActive)
    TreeReloadEvent.on(this.handleReload)
    UserEvent.on(this.handleReload)
    this.fetch()
  }

  aboutToDisappear(): void {
    TreeChangeEvent.off(this.handleActive)
    TreeReloadEvent.off(this.handleReload)
    UserEvent.off(this.handleReload)
  }

  handleActive = (): void => {
    this.handleChangeActive(this.cacheModel.lastClickTreeNodeId)
  }
  handleReload = (): void => {
    this.fetchNetwork()
  }

  fetch() {
    try {
      this.loading = true
      const exists = fs.accessSync(this.cacheFilePath)
      if (!exists) {
        throw new Error('Cache file does not exist')
      }
      const cacheData = fs.readTextSync(this.cacheFilePath)
      if (!cacheData) {
        throw new Error('Cache file is empty')
      }
      this.originDataset = JSONUtil.jsonToArray<TreeNodeModel>(cacheData, TreeNodeModel)
      this.handleChangeActive(this.cacheModel.lastClickTreeNodeId)
      this.loading = false
    } catch {
      this.fetchNetwork()
    }
  }

  fetchNetwork() {
    this.loading = true
    // 走网络请求
    ResearchService.network().then(ret => {
      if (!ret.isSuccess() || !ret.dataset?.length) {
        return
      }
      fs.createStream(this.cacheFilePath, "w+").then(stream => {
        stream.write(JSON.stringify(ret.dataset)).catch(() => {
          LogUtil.error('Failed to write cache file')
        })
      }).catch(() => {
        LogUtil.error('Failed to create cache file')
      })
      this.originDataset = ret.dataset
      this.handleChangeActive(this.cacheModel.lastClickTreeNodeId)
    }).finally(() => {
      this.loading = false
    })
  }

  handleChangeActive(forceActiveId: number) {
    forceActiveId = forceActiveId || 0
    // 列数据
    let columns: TreeNodeModel[][] = []

    // 原始数据构建
    let originDataset = this.originDataset

    // 递归重组树数据
    const recursion = (children: TreeNodeModel[], layer = 0) => {
      return children.map(item => {
        if (item.children && item.children.length) {
          item.children = recursion(item.children, layer + 1)
        }

        item.active = 0

        // 指定打开到某个节点
        if (Number(forceActiveId) && Number(forceActiveId) === Number(item.id)) {
          item.active = 1
        }

        // 子节点被选中,父节点自动选中
        (item.children || []).forEach(item2 => {
          if (Number(item2.active)) {
            item.active = 1
            if (Number(item2.is_dir) === 0) {
              item2.active = 0
            }
          }
        })

        // 节点高亮 & 存在子节点 加入到分页数据中
        if (item.active && item.children && item.children.length) {
          columns.unshift(item.children)
        }

        return item
      })
    }

    let dataset = recursion([...originDataset])

    const maxColumn = 3
    if (dataset.length) {
      // 构建列数据
      const buildColumns = (
        nodes: TreeNodeModel[] = [],
        current = 0,
        maxColumn = 3
      ) => {
        let result: TreeNodeModel[][] = []

        if (current >= maxColumn) {
          return result
        }

        let activated = 0

        let index = 0
        if (nodes[index]) {
          let children = nodes[index].children

          for (let i = 0; i < nodes.length; i++) {
            if (activated) {
              break
            }
            const item = nodes[i]

            if (Number(item.default_active)) {
              activated = 1
              index = i
              children = item.children
            }
          }

          nodes[index].active = 1

          if (children && children.length) {
            result = [children]
            const nextColumns = buildColumns(children, current + 1, maxColumn)
            result = [...result, ...nextColumns]
          }
        }

        return result
      }

      const length = columns.length
      if (length < maxColumn) {
        let nodes = dataset
        if (length > 0) {
          nodes = columns[length - 1]
        }

        const otherColumns = buildColumns(nodes, 0, maxColumn)

        columns = columns.concat(otherColumns).slice(0, maxColumn)

        const lastColumnIndex = columns.length - 1
        const lastColumn = columns[lastColumnIndex]

        columns[lastColumnIndex] = lastColumn.map(item => {
          item.active = 0
          return item
        })
      }
    }

    // 视图分页数据
    const pages: TreeNodeModel[][][] = []
    const length = columns.length

    if (length) {
      pages.push(columns.slice(0, maxColumn))

      if (length === maxColumn) {
        const lastColumns = columns[maxColumn - 1]
        for (let i = 0; i < lastColumns.length; i++) {
          const item = lastColumns[i]
          if (item.active) {
            const pageData = [columns[maxColumn - 1]]
            if (item.children) {
              pageData.push(item.children)
            }
            pages.push(pageData)
            break
          }
        }
      } else {
        const buildCols = columns.slice(maxColumn - 1)
        for (let i = 0; i < buildCols.length - 1; i++) {
          const pageData = [buildCols[i]]
          if (buildCols[i + 1]) {
            pageData.push(buildCols[i + 1])
          }
          pages.push(pageData)
        }
      }
    }

    const currentPage = pages.length - 1

    const scrollerList: ListScroller[][] = []
    pages.map((page, pageIndex) => {
      let pageScrollerList: ListScroller[] = []
      page.map((_, columnIndex) => {
        pageScrollerList.push(new ListScroller())
      })
      scrollerList.push(pageScrollerList)
    })

    this.scrollerList = scrollerList

    this.columns = columns
    this.pages = pages
    this.dataset = dataset
    this.appModel.courseTreeCurrentPage = currentPage
    this.cacheModel.lastClickTreeNodeId = forceActiveId

    pages.map((page, pageIndex) => {
      page.map((column, columnIndex) => {
        const key = MD5.digestSync(column.map((item: TreeNodeModel) => item.id).join('-'))
        const position = this.scrollerPosition?.[pageIndex]?.[columnIndex]?.[key] || 0
        if (this.scrollerList?.[pageIndex]?.[columnIndex]) {
          setTimeout(() => {
            // this.scrollerList[pageIndex][columnIndex].scrollBy(0, position)
          }, 10)
        }
      })
    })
  }

  buildListWidth(columnIndex: number, pageIndex: number): Length {
    if (pageIndex === 0) {
      if (columnIndex === 0) {
        return 110
      } else if (columnIndex === 2) {
        return 110
      } else {
        return 'auto'
      }
    } else {
      if (columnIndex === 0) {
        return 171
      } else {
        return 'auto'
      }
    }
  }

  buildListMargin(columnIndex: number, pageIndex: number): Margin | Length {
    if (pageIndex > 0) {
      if (columnIndex === 1) {
        return {
          right: 20,
        }
      }
    }
    return 0
  }

  buildListFlexShrink(columnIndex: number, pageIndex: number): number {
    if (pageIndex === 0) {
      if (columnIndex === 0) {
        return 0
      } else if (columnIndex === 2) {
        return 0
      } else {
        return 1
      }
    } else {
      if (columnIndex === 0) {
        return 0
      } else {
        return 1
      }
    }
  }

  @Builder
  buildListItemLeftIcon(item: TreeNodeModel, columnIndex: number, pageIndex: number) {
    if (pageIndex === 0) {
      if (columnIndex === 0) {
        if (item.active) {
          Image(item.icon)
            .width(18)
            .height(18)
            .margin({ right: 5 })
            .colorFilter(drawing.ColorFilter.createBlendModeColorFilter({
              alpha: 255,
              red: 255,
              green: 255,
              blue: 255,
            }, drawing.BlendMode.SRC_IN))
        } else {
          Image(item.icon)
            .width(18)
            .height(18)
            .margin({
              right: 5
            })
        }
      }
    }
  }

  @Builder
  buildListItemRightIcon(item: TreeNodeModel, columnIndex: number, pageIndex: number) {
    if (!item.is_dir) {
      if (item.finished) {
        Image($r('app.media.tree_play_finished'))
          .width(14)
          .height(14)
          .margin({
            left: 5,
          })
      } else {
        Image($r('app.media.tree_play'))
          .width(14)
          .height(14)
          .margin({
            left: 5,
          })
      }
    } else if (!(pageIndex === 0 && columnIndex === 2)) {
      if (item.active) {
        Image($r('app.media.icon_arrow_white'))
          .width(7)
          .height(12)
          .margin({
            left: 5,
          })
      } else if (item.finished) {
        Image($r('app.media.icon_arrow_gray'))
          .width(7)
          .height(12)
          .margin({
            left: 5,
          })
      } else {
        Image($r('app.media.icon_arrow_blue'))
          .width(7)
          .height(12)
          .margin({
            left: 5,
          })
      }
    }
  }

  @Builder
  buildLineCanvas(columnIndex: number, pageIndex: number) {
    if (pageIndex == 0) {
      if (columnIndex == 1) {
        Canvas()
          .width(10)
          .height('100%')
          .flexShrink(0)
      } else if (columnIndex === 2) {
        Canvas()
          .width(10)
          .height('100%')
          .flexShrink(0)
      }
    } else {
      if (columnIndex === 0) {
        Canvas()
          .width(10)
          .height('100%')
          .flexShrink(0)
      } else if (columnIndex === 1) {
        Canvas()
          .width(10)
          .height('100%')
          .flexShrink(0)
      }
    }
  }

  buildListItemNameFontSize(columnIndex: number, pageIndex: number): number {
    if (pageIndex === 0) {
      if (columnIndex === 0) {
        return 12
      }
    }
    return 11
  }

  buildListItemBorder(item: TreeNodeModel, columnIndex: number, pageIndex: number): BorderOptions {
    if (pageIndex === 0) {
      if (columnIndex === 0) {
        return {
          width: {
            left: 0,
            right: 1,
            top: 1,
            bottom: 1,
          },
          color: '#e7ebf6',
          radius: {
            topLeft: 0,
            bottomLeft: 0,
            topRight: 6,
            bottomRight: 6,
          }
        }
      } else if (columnIndex === 2) {
        return {
          width: {
            left: 1,
            right: 0,
            top: 1,
            bottom: 1,
          },
          color: '#e7ebf6',
          radius: {
            topLeft: 6,
            bottomLeft: 6,
            topRight: 0,
            bottomRight: 0,
          }
        }
      }
    }
    return {
      width: 1,
      color: '#e7ebf6',
      radius: 6,
    }
  }

  buildListItemStackPadding(item: TreeNodeModel): Padding {
    if (item.node_type === 2) {
      return {
        top: 12,
      }
    }
    return {}
  }

  buildListItemLinearGradient(item: TreeNodeModel, columnIndex: number, pageIndex: number): LinearGradientOptions {
    let color1: ResourceColor = Color.White
    let color2: ResourceColor = Color.White
    let color3: ResourceColor = Color.White
    let color4: ResourceColor = Color.White
    if (item.study) {
      color1 = '#e6e6e6'
      color2 = '#e6e6e6'
    }
    if (item.finished) {
      color1 = '#eaecf2'
      color2 = '#eaecf2'
      color3 = '#eaecf2'
      color4 = '#eaecf2'
    }
    if (pageIndex === 0 && columnIndex === 0) {
      color1 = Color.White
      color2 = Color.White
      color3 = Color.White
      color4 = Color.White
    }
    if (item.active) {
      color1 = '#254182'
      color2 = '#254182'
      color3 = '#254182'
      color4 = '#254182'
    }
    return {
      angle: 90,
      colors: [
        [color1, 0],
        [color2, 0.55],
        [color3, 0.65],
        [color4, 1],
      ]
    }

  }

  @Builder
  buildColumn(column: TreeNodeModel[], columnIndex: number, pageIndex: number) {
    this.buildLineCanvas(columnIndex, pageIndex)
    List({
      scroller: this.scrollerList[pageIndex]?.[columnIndex],
    }) {
      ForEach(column, (item: TreeNodeModel) => {
        ListItem() {
          Stack() {
            Flex({ alignItems: ItemAlign.Center }) {
              this.buildListItemLeftIcon(item, columnIndex, pageIndex)
              Text(item.name)
                .fontSize(this.buildListItemNameFontSize(columnIndex, pageIndex))
                .fontColor(item.active ? Color.White : item.finished ? '#777777' : '#333333')
                .layoutWeight(1)
              this.buildListItemRightIcon(item, columnIndex, pageIndex)
            }
            .zIndex(2)
            .width('100%')
            .constraintSize({
              minHeight: 50,
            })
            .padding({
              left: 5,
              right: 5,
              top: 7,
              bottom: 7,
            })
            .linearGradient(this.buildListItemLinearGradient(item, columnIndex, pageIndex))
            .align(Alignment.Center)
            .margin({
              bottom: 8,
            })
            .border(this.buildListItemBorder(item, columnIndex, pageIndex))
            .onClick(() => {
              if (item.active) {
                return
              }
              if (!item.is_dir) {
                if (!item.course_id) {
                  showToast("视频不在课程下")
                  return
                }
                return
              }
              if (!item.children || !item.children.length) {
                showToast("没有更多内容")
                return
              }
              this.handleChangeActive(Number(item.id))
            })

            if (item.node_type == 2) {
              Image(item.active ? $r('app.media.icon_course_blue') : $r('app.media.icon_course_white'))
                .width(33)
                .height(15)
                .position({
                  x: 0,
                  y: -12,
                })
                .zIndex(1)
            }
            if (item.highlight && columnIndex < 2) {
              Image($r('app.media.icon_new'))
                .width(15)
                .height(15)
                .position({
                  right: 0,
                  top: 0,
                })
                .zIndex(3)
                .border({
                  radius: {
                    topRight: 6
                  }
                })
            }
          }
          .padding(this.buildListItemStackPadding(item))
        }
      })
    }
    .width(this.buildListWidth(columnIndex, pageIndex))
    .flexShrink(this.buildListFlexShrink(columnIndex, pageIndex))
    .height('100%')
    .listDirection(Axis.Vertical)
    .scrollBar(0)
    .align(Alignment.TopStart)
    .margin(this.buildListMargin(columnIndex, pageIndex))
    .onDidScroll(() => {
      const key = MD5.digestSync(column.map((item: TreeNodeModel) => item.id).join('-'))
      if (!this.scrollerPosition[pageIndex]) {
        this.scrollerPosition[pageIndex] = []
      }
      if (!this.scrollerPosition[pageIndex][columnIndex]) {
        this.scrollerPosition[pageIndex][columnIndex] = {}
      }
      this.scrollerPosition[pageIndex][columnIndex][key] = this.scrollerList[pageIndex][columnIndex].currentOffset().yOffset
      LogUtil.debug(this.scrollerPosition)
    })
  }

  @Builder
  buildTop() {
    List({ scroller: this.topScroller }) {
      ForEach(this.dataset, (item: TreeNodeModel, i) => {
        ListItem() {
          Stack() {
            Text(item.name)
              .font({
                size: this.dataset.length > 4 ? 14 : 16,
                weight: item.active ? FontWeight.Bold : FontWeight.Normal,
              })
              .fontColor(item.active ? "#254182" : "#606060")
              .zIndex(2)
            if (item.active) {
              Flex()
                .width(41)
                .height(8)
                .borderRadius({
                  topRight: 4,
                  bottomRight: 4
                })
                .linearGradient({
                  angle: 90,
                  colors: [
                    ['#00fa6400', 0],
                    ['#FA6400', 1],
                  ]
                })
                .margin({
                  top: 15
                })
                .zIndex(1)
            }
          }
          .width('100%')
          .height('100%')
          .align(Alignment.Center)
        }
        .width(this.dataset.length > 4 ? '20%' : '25%')
        .height('100%')
        .align(Alignment.Center)
        .onClick(() => {
          if (item.active) {
            return
          }
          this.topScroller.scrollToIndex(i, true, ScrollAlign.CENTER)
          this.handleChangeActive(Number(item.id))
        })
      })
    }
    .width('100%')
    .height(45)
    .listDirection(Axis.Horizontal)
    .backgroundColor(Color.White)
    .align(Alignment.TopStart)
    .scrollBarWidth(0)
    .fadingEdge(true, { fadingEdgeLength: LengthMetrics.vp(40) })
    .borderWidth({
      bottom: 1,
    })
    .borderColor({
      bottom: "#f0f4fc"
    })
    .flexShrink(0)
    .margin({
      bottom: 7
    })
  }

  showNextPageArrow() {
    let showNextPage = !!this.pages[this.appModel.courseTreeCurrentPage + 1]

    let hasActive = false
    let firstHasChildrenNode: TreeNodeModel = {}

    if (!showNextPage && this.pages.length) {
      if (this.appModel.courseTreeCurrentPage === this.pages.length - 1) {
        const columns = this.pages[this.appModel.courseTreeCurrentPage]
        const lastColumns: TreeNodeModel[] = columns[columns.length - 1]
        for (let i = 0; i < lastColumns.length; i++) {
          const item = lastColumns[i]
          if (item.active) {
            hasActive = true
          }
          if (!firstHasChildrenNode.id && item.children && item.children.length) {
            firstHasChildrenNode = item
          }
        }
        if (!hasActive && firstHasChildrenNode.id) {
          showNextPage = true
        }
      }
    }

    return showNextPage;
  }

  handleNextPageArrowClick() {
    let showNextPage = !!this.pages[this.appModel.courseTreeCurrentPage + 1]

    let hasActive = false
    let firstHasChildrenNode: TreeNodeModel = {}

    if (!showNextPage && this.pages.length) {
      if (this.appModel.courseTreeCurrentPage === this.pages.length - 1) {
        const columns = this.pages[this.appModel.courseTreeCurrentPage]
        const lastColumns: TreeNodeModel[] = columns[columns.length - 1]
        for (let i = 0; i < lastColumns.length; i++) {
          const item = lastColumns[i]
          if (item.active) {
            hasActive = true
          }
          if (!firstHasChildrenNode.id && item.children && item.children.length) {
            firstHasChildrenNode = item
          }
        }
        if (!hasActive && firstHasChildrenNode && firstHasChildrenNode.id) {
          showNextPage = true
        }
      }
    }

    if (firstHasChildrenNode.id) {
      this.handleChangeActive(Number(firstHasChildrenNode.id))
    } else {
      this.appModel.courseTreeCurrentPage = this.appModel.courseTreeCurrentPage + 1
    }
  }

  build() {
    Stack() {
      Flex({ direction: FlexDirection.Column }) {
        this.buildTop()

        Stack() {
          Swiper(this.swiperController) {
            ForEach(this.pages, (page: TreeNodeModel[][], pageIndex) => {
              Flex() {
                ForEach(page, (column: TreeNodeModel[], columnIndex) => {
                  this.buildColumn(column, columnIndex, pageIndex)
                })
              }
              .height('100%')
            })
          }
          .index(this.appModel.courseTreeCurrentPage)
          .indicator(
            new DotIndicator()
              .itemWidth(20)
              .itemHeight(4)
              .selectedItemWidth(20)
              .selectedItemHeight(4)
              .space(LengthMetrics.vp(2))
              .color('#c0c4cc')
              .selectedColor('#254182'))
          .loop(false)
          .height('100%')
          .onChange(index => {
            this.appModel.courseTreeCurrentPage = index
          })

          Flex({ alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) {
            Image($r('app.media.icon_tree_left'))
              .width(42)
              .height(42)
              .transform({ scaleX: -1 })
              .opacity(0.9)
              .visibility(this.pages[this.appModel.courseTreeCurrentPage - 1] ? Visibility.Visible : Visibility.Hidden)
              .onClick(() => this.appModel.courseTreeCurrentPage = this.appModel.courseTreeCurrentPage - 1)
            Image($r('app.media.icon_tree_right'))
              .width(42)
              .height(42)
              .opacity(0.9)
              .visibility(this.showNextPageArrow() ? Visibility.Visible : Visibility.Hidden)
              .onClick(() => this.handleNextPageArrowClick())
          }
          .height('100%')
          .hitTestBehavior(HitTestMode.None)
        }
        .align(Alignment.Center)
        .flexShrink(1)

        Flex({ alignItems: ItemAlign.Center }) {
          Image($r('app.media.icon_tips'))
            .width(9)
            .height(9)
            .margin({
              right: 3,
              top: 2
            })
          Text("节点提示:白色-未学习,灰白过渡色-未学完,灰色-已学完")
            .fontSize(10)
            .fontColor('#777777')
        }
        .backgroundColor("#eaecf2")
        .padding(8)
        .flexShrink(0)
      }
      .width('100%')
      .height('100%')

      PageLoading({ loading: this.loading })
    }
    .width('100%')
    .height('100%')
    .backgroundColor("#f0f4fc")

  }
}

更多关于HarmonyOS鸿蒙Next中急急急急,Swiper 嵌套 Flex 再嵌套多个 List 滚动无法保持问题的实战教程也可以访问 https://www.itying.com/category-93-b0.html

6 回复

开发者您好,您可以使用LazyForEach代替Foreach减轻渲染压力,同时参考键值生成规则,为每个item生成一个唯一且持久的键值,避免出现渲染结果异常、渲染效率降低等问题。如果以上方案仍然无法解决问题,请及时反馈。

更多关于HarmonyOS鸿蒙Next中急急急急,Swiper 嵌套 Flex 再嵌套多个 List 滚动无法保持问题的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


不是同一个问题

开发者您好,您描述中提供的demo无法运行,请提供一个可复现问题的完整demo,方便问题分析解决。

蹲一个后续,等大佬出现

在HarmonyOS Next中,Swiper嵌套Flex再嵌套多个List时,需确保Swiper的滑动方向设为Axis.Horizontal,每个List的滑动方向设为Axis.Vertical。同时,禁用Swiper的嵌套滚动拦截属性(如nestedScrollEnabled(false)),避免事件冲突。若仍无法保持,可对List设置scrollEnabled(true)并检查Flex布局参数。,

问题出在每次点击更新数据时,handleChangeActive 里完全重建了 scrollerList 数组(new ListScroller()),导致之前绑定的滚动状态丢失。即便记录了偏移量,恢复时仍会触发一次跳变产生闪动。解决思路是避免重建 ListScroller:在组件初始化时预分配足够数量的 scroller,或仅在页数、列数增加时动态补充,保持已有 scroller 实例不变。同时确保 ForEach 的 key 使用稳定的 item.id,让 ArkUI 能复用 List 组件,这样滚动位置就会自然保持,不会闪动。

回到顶部