import nodeUtil from '../core/utils/node-util'; import dataUtil from '../core/utils/data-util'; import eventUtil from '../core/utils/event-util'; import pixelUtil from '../core/utils/pixel-util'; // 默认的 Sidebar 数据 const defaultSidebarData = [ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' ]; Component({ externalClasses: [ 'l-tip-class', 'l-tip-text-class', 'l-sidebar-class', 'l-selected-class', 'l-unselected-class', 'l-sidebar-item-class' ], relations: { '../index-anchor/index': { type: 'child' } }, options: { multipleSlots: true, pureDataPattern: /^_/ }, lifetimes: { attached() { this.init(); } }, properties: { // Anchor 是否吸附 isStick: { type: Boolean, value: false }, // 页面垂直滚动距离 // 微信官方 onScrollTop() 监听函数获取 scrollTop: { type: Number, value: 0 }, // Sidebar 显示的索引内容 sidebarData: { type: Array, value: defaultSidebarData }, // 是否显示 Sidebar showSidebar: { type: Boolean, value: true }, // Anchor 吸附时距离顶部的距离(单位 rpx) stickOffsetTop: { type: Number, value: 0 } }, data: { // Sidebar 节点信息 _sidebar: { top: 0, height: 0, sidebarItemCenterPoints: [], // 该变量用于标识是否正在滑动 Sidebar // 滑动侧栏的时候需要禁止页面滚动去改变 Sidebar 激活项 // 不然会出现 Sidebar 激活项乱跳动的问题 isMoving: false, // sidebar-item 节点 rect 信息 sidebarItemRect: {} }, // Anchor 节点信息 _anchor: { // 每个 Anchor 距离页面顶部的像素 anchorTopLocations: [], // index-anchor 所有组件实例 indexAnchorComponents: [], // 当前吸附的 Anchor 索引 currentStickAnchorIndex: -1, // 每个 Anchor 的高度 anchorItemsHeight: [] }, // stickOffsetTop px数值 _stickOffsetTopPx: 0, // 当前激活的索引项 的索引 activeSidebarItem: 0, // Tip 提示绝对定位的top值 tipTop: 0, // 是否显示 Tip showTip: false, // tip 高度 tipHeight: 0 }, observers: { 'scrollTop': function (scrollTop) { this.setIndexListStyle(scrollTop); }, 'stickOffsetTop': function (stickOffsetTop) { this.setData({ _stickOffsetTopPx: pixelUtil.rpx2px(stickOffsetTop) }); } }, methods: { // ================================== 组件初始化函数 ================================== /** * 组件初始化函数,主要计算一些必要的信息 * 该函数内的多个异步函数调用顺序非常重要,不要随意更改,否则会出错 */ async init() { // 解析 Sidebar Rect 信息 await this.parseSidebarRect(); // 解析 SidebarItem Rect 信息 await this.parseSidebarItemRect(); // 获取 index-anchor 所有组件实例 await this.parseIndexAnchors(); // 解析 Anchor Rect 信息 this.parseAnchorRect(); wx.lin = wx.lin || {}; // 传入scrollTop的值的函数 wx.lin.setScrollTop = (scrollTop) => { dataUtil.setDiffData(this, { scrollTop }); }; }, // ================================== 节点信息获取函数 ================================== /** * 把 Sidebar 在页面中的位置信息存到 data 中 */ async parseSidebarRect() { const sidebarRect = await nodeUtil.getNodeRectFromComponent(this, '.sidebar'); this.setData({ ['_sidebar.height']: sidebarRect.height, ['_sidebar.top']: sidebarRect.top }); }, /** * 把 Sidebar 每个 Item 的中点位置存到 data 中 * 用于 Tip 定位使用 */ async parseSidebarItemRect() { // Sidebar 索引个数 const sidebarLength = this.data.sidebarData.length; // 获取 sidebar-item 节点 const sidebarItemRect = await nodeUtil.getNodeRectFromComponent(this, '.sidebar-item'); // Sidebar 单个索引高度(包含了 margin 空隙) const sidebarItemHeight = this.data._sidebar.height / sidebarLength; // Sidebar 单个索引真实高度 const sidebarItemRealHeight = sidebarItemRect.height; // 获取 sidebar-item margin-top 属性 const sidebarItemFields = await nodeUtil.getNodeFieldsFromComponent(this, '.sidebar-item', { computedStyle: ['margin-top'] }); // 获取 tip height 属性 // 只能用 height 获取高度,因为 tip 旋转后,rect的宽高发生了变化 const tipFields = await nodeUtil.getNodeFieldsFromComponent(this, '.tip', { computedStyle: ['height'] }); const sidebarItemCenterPoints = []; const sidebarItemMarginTop = sidebarItemFields['margin-top'].replace('px', ''); for (let i = 1; i <= sidebarLength; i++) { sidebarItemCenterPoints.push((i * 2 - 1) * sidebarItemRealHeight / 2 + i * parseInt(sidebarItemMarginTop)); } const tipHeight = parseInt(tipFields.height.replace('px', '')); this.setData({ tipHeight, // tip 旋转后,中线位置下移了 20.5% tipHeightOverflow: tipHeight * 0.205, ['_sidebar.sidebarItemRect']: sidebarItemRect, ['_sidebar.sidebarItemHeight']: sidebarItemHeight, ['_sidebar.sidebarItemRealHeight']: sidebarItemRealHeight, ['_sidebar.sidebarItemCenterPoints']: sidebarItemCenterPoints }); }, /** * 获取所有的 index-anchor 节点实例 */ parseIndexAnchors() { // 获取该 index-list 内部所有的 index-anchor const indexAnchors = this.getRelationNodes('../index-anchor/index'); // 没获取到节点实例的异常情况 if (!indexAnchors) { console.error('获取 index-anchor 节点实例失败,请参考文档检查您的代码是否书写正确'); return; } // 存入 data this.setData({ ['_anchor.indexAnchorComponents']: indexAnchors }); for (let i = 0; i < indexAnchors.length; i++) { indexAnchors[i].setData({ anchorText: this.data.sidebarData[i] }); } }, /** * 把 Anchor 在页面中的位置信息存到 data 中 */ async parseAnchorRect() { // 每个 Anchor 距离页面顶部的像素 const anchorTopLocations = []; // 每个 Anchor 的高度 const anchorItemsHeight = []; // index-anchor 组件实例 const indexAnchorComponents = this.data._anchor.indexAnchorComponents; for (const indexAnchorComponent of indexAnchorComponents) { // todo 此处获取 .anchor 节点,不知为什么在 index-anchor 组件中获取到的为空,后期再调研修改 const anchorRect = await nodeUtil.getNodeRectFromComponent(indexAnchorComponent, '.anchor'); if (anchorRect === null) { continue; } anchorTopLocations.push(anchorRect.top); anchorItemsHeight.push(anchorRect.height); } this.setData({ // 每个 Anchor 距离页面顶部的像素 ['_anchor.anchorTopLocations']: anchorTopLocations, // 每个 Anchor 的高度 ['_anchor.anchorItemsHeight']: anchorItemsHeight }); }, // ================================== 页面元素控制函数 ================================== /** * 设置 Tip 显示隐藏 * @param isShow 是否显示 Tip */ switchTipShow(isShow) { dataUtil.setDiffData(this, { showTip: isShow }); }, /** * 切换 Sidebar 激活的选项 * @param index 被激活选项的索引 */ switchSidebarIndex(index) { dataUtil.setDiffData(this, { activeSidebarItem: index, }); }, /** * 切换是否正在滑动 Sidebar */ switchIsMovingSidebar(isMoving) { dataUtil.setDiffData(this, { ['_sidebar.isMoving']: isMoving }); }, /** * 根据 scrollTop 调整 Anchor、Sidebar 样式 * @param scrollTop onScrollTop() 函数监听得到的值 */ setIndexListStyle(scrollTop) { // 当前应该激活的索引 const currentShouldActiveIndex = this.countCurrentActiveIndex(scrollTop); if (currentShouldActiveIndex === undefined) { return; } // 设置 Anchor 的样式 this.data.isStick && this.setAnchorStyle(scrollTop); // 激活 Sidebar 选项 if (this.data.showSidebar && !this.data._sidebar.isMoving) { this.switchSidebarIndex(currentShouldActiveIndex); } }, /** * 设置 Anchor 样式 * @param scrollTop onScrollTop() 函数监听得到的值 */ setAnchorStyle(scrollTop) { const { // 每个 Anchor 距离页面顶部的 px 值 anchorTopLocations, // 所有 Anchor 的高度 anchorItemsHeight, // 所有 index-anchor 组件实例 indexAnchorComponents } = this.data._anchor; // 当前应该激活的索引 const currentShouldActiveIndex = this.countCurrentActiveIndex(scrollTop); // 当前应该激活的 index-anchor 组件实例 const currentIndexAnchorComponent = indexAnchorComponents[currentShouldActiveIndex]; // 当前应该激活的 Anchor top 值 const currentIndexAnchorTop = anchorTopLocations[currentShouldActiveIndex]; // 当前应该激活的 Anchor 高度 const currentIndexAnchorHeight = anchorItemsHeight[currentShouldActiveIndex]; // 下一个应该激活的 Anchor top 值 const nextIndexAnchorTop = anchorTopLocations[currentShouldActiveIndex + 1]; // stickOffsetTop px值 const stickOffsetTop = this.data._stickOffsetTopPx; if (scrollTop + stickOffsetTop >= currentIndexAnchorTop && scrollTop + stickOffsetTop <= nextIndexAnchorTop - currentIndexAnchorHeight && !currentIndexAnchorComponent.isFixed()) { // 该条件下,当前 Anchor 应该设置为 fixed 布局,并把其他 Anchor 样式清空 currentIndexAnchorComponent.setFixed(this.data.stickOffsetTop, currentIndexAnchorHeight); for (let i = 0; i < indexAnchorComponents.length; i++) { if (i !== currentShouldActiveIndex) { indexAnchorComponents[i].clearStyle(); } } } else if (scrollTop + stickOffsetTop > nextIndexAnchorTop - currentIndexAnchorHeight && scrollTop + stickOffsetTop < nextIndexAnchorTop && !currentIndexAnchorComponent.isRelative()) { // 该条件下,当前 Anchor 应该设置为 relative 布局,并把其他 Anchor 样式清空 currentIndexAnchorComponent.setRelative(nextIndexAnchorTop - currentIndexAnchorTop - currentIndexAnchorHeight); for (let i = 0; i < indexAnchorComponents.length; i++) { if (i !== currentShouldActiveIndex) { indexAnchorComponents[i].clearStyle(); } } } else if (scrollTop + stickOffsetTop < currentIndexAnchorTop) { // 该条件下,清空所有 Anchor 样式 for (let i = 0; i < indexAnchorComponents.length; i++) { indexAnchorComponents[i].clearStyle(); } } }, /** * 计算当前页面滚动到了第几个索引 * @param scrollTop onScrollTop() 函数监听得到的值 */ countCurrentActiveIndex(scrollTop) { let result = 0; // 每个 Anchor 距离页面顶部的 px 值 const { anchorTopLocations } = this.data._anchor; for (let i = 0; i < anchorTopLocations.length; i++) { if (scrollTop + this.data._stickOffsetTopPx < anchorTopLocations[i]) { result = i - 1; break; } } if (result < 0) { result = 0; } return result; }, // ================================== 事件监听函数 ================================== /** * 监听 手指触摸后移动 事件 * @param event 事件对象 */ onTouchMove(event) { // 显示 Tip this.switchTipShow(true); // 标识正在滑动 Sidebar this.switchIsMovingSidebar(true); // 取出 Sidebar 位置信息 const { top: sidebarTop, sidebarItemHeight } = this.data._sidebar; // Sidebar 索引个数 const sidebarLength = this.data.sidebarData.length; // 触摸点 Y 坐标 const touchY = event.touches[0].clientY; // 计算当前触摸点在第几个索引处 let index = Math.floor((touchY - sidebarTop) / sidebarItemHeight); // 滑动超过范围时限制索引边界值 if (index < 0) { index = 0; } else if (index > sidebarLength - 1) { index = sidebarLength - 1; } // 选中的索引文字 const tipText = this.data.sidebarData[index]; dataUtil.setDiffData(this, { tipText, activeSidebarItem: index, tipTop: this.data._sidebar.sidebarItemCenterPoints[index] }); // 页面应该滚动到的位置 let scrollPageToLocation = this.data._anchor.anchorTopLocations[index] - this.data._stickOffsetTopPx; // 滚动页面到对应索引处 wx.pageScrollTo({ duration: 0, scrollTop: scrollPageToLocation }); /** * fix issue1078 * 当点击 sidebar 时,onTouchend 会先于 onTouchMove 执行 * 导致滑动状态无法正常关闭,固此处添加一个延时状态修改(页面滚动需要一定时间) */ event.type === 'tap' && setTimeout(() => { this.switchIsMovingSidebar(false); }, 100); // 触发 linselected 事件 eventUtil.emit(this, 'linselected', { index, tipText }); }, /** * 监听 手指触摸动作结束 事件 */ onTouchend() { // 500 毫秒后隐藏 Tip setTimeout(() => { this.switchTipShow(false); }, 500); this.switchIsMovingSidebar(false); }, /** * 监听 点击侧边栏 */ onTapSidebar(event) { // 把事件对象传入触摸滑动监听函数即可 this.onTouchMove(event); } } });