├── README.md ├── app.js ├── app.json ├── app.wxss ├── images ├── .DS_Store ├── image1.png └── scroll.gif ├── pages └── index │ ├── index.js │ ├── index.wxml │ └── index.wxss ├── project.config.json └── utils └── constants.js /README.md: -------------------------------------------------------------------------------- 1 | # wechat-scroll-linkage 2 | 3 | ### 微信小程序左右联动效果 4 | 5 | 左边栏点击 button 右侧会跟随滚动到对应的区域; 右测滚动,左侧也会滚动到对应的 button ; 6 | 7 | #### 演示 8 | 9 | ![GIF演示](https://github.com/YasinChan/wechat-scroll-linkage/blob/master/images/scroll.gif) 10 | 11 | #### 预览 12 | 13 | ```bash 14 | git clone https://github.com/YasinChan/wechat-scroll-linkage.git 15 | # 用微信开发者这工具打开此案例查看效果 16 | ``` 17 | 18 | #### 实现原理 19 | 20 | 1. 数据渲染 21 | 22 | 目前我将需要渲染的数据以 json 的形式保存在`./utils/constants.js`中可参考 23 | 24 | 2. 首先需要设置常量,如下图,各个位置暂时命名为 `LEFT_ITEM` `RIGHT_BAR` `RIGHT_ITEM` 25 | 26 | ![image](https://github.com/YasinChan/wechat-scroll-linkage/blob/master/images/image1.png) 27 | 28 | 3. 在 onload 阶段,我们需要获取每个右侧分类的 RIGHT_BAR 到顶部的距离,用来做后面的计算。 29 | 30 | ``` 31 | getEachRightItemToTop: function () { // 获取每个右侧的 RIGHT_BAR 到顶部的距离,用来做后面的计算。 32 | var obj = {}; 33 | var totop = 0; 34 | obj[constants[0].id] = totop // 右侧第一类肯定是到顶部的距离为 0 35 | for (let i = 1; i < (constants.length + 1); i++) { // 循环来计算每个子类到顶部的高度 36 | totop += (RIGHT_BAR_HEIGHT + constants[i-1].category.length * RIGHT_ITEM_HEIGHT) 37 | obj[constants[i] ? constants[i].id : 'last'] = totop 38 | // 这个的目的是 例如有两类,最后需要 0-1 1-2 2-3 的数据,所以需要一个不存在的 'last' 项,此项即为第一类加上第二类的高度。 39 | } 40 | return obj 41 | }, 42 | ``` 43 | 44 | 4. 现在,我们为左右两侧添加相应的事件 45 | 46 | 1. 为左侧列表添加`bindtap`事件,使右侧滚动到相应的位置 47 | 48 | ``` 49 | jumpTo: function (e) { // 左侧 LEFT_ITEM 的点击事件,点击时,右侧会滚动到对应 RIGHT_BAR 50 | this.setData({ 51 | toView: e.target.id || e.target.dataset.id, 52 | currentLeftSelect: e.target.id || e.target.dataset.id 53 | }) 54 | } 55 | ``` 56 | 57 | 2. 为右侧添加`bindscroll`事件,用来监听右侧滚动事件,来使左侧列表响应,滚动到相应位置 58 | 59 | ``` 60 | rightScroll: function (e) { // 监听右侧的滚动事件与 eachRightItemToTop 的循环作对比 从而判断当前可视区域为第几类,从而左侧滚动到对应 LEFT_ITEM。 61 | for (let i = 0; i < this.data.constants.length; i++) { 62 | let left = this.data.eachRightItemToTop[this.data.constants[i].id] 63 | let right = this.data.eachRightItemToTop[this.data.constants[i + 1] ? this.data.constants[i+1].id : 'last'] 64 | if (e.detail.scrollTop < right && e.detail.scrollTop >= left) { 65 | this.setData({ 66 | currentLeftSelect: this.data.constants[i].id, 67 | leftToTop: LEFT_ITEM_HEIGHT * i 68 | }) 69 | } 70 | } 71 | }, 72 | ``` 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | //app.js 2 | App({ 3 | onLaunch: function () { 4 | // 展示本地存储能力 5 | var logs = wx.getStorageSync('logs') || [] 6 | logs.unshift(Date.now()) 7 | wx.setStorageSync('logs', logs) 8 | 9 | // 登录 10 | wx.login({ 11 | success: res => { 12 | // 发送 res.code 到后台换取 openId, sessionKey, unionId 13 | } 14 | }) 15 | // 获取用户信息 16 | wx.getSetting({ 17 | success: res => { 18 | if (res.authSetting['scope.userInfo']) { 19 | // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框 20 | wx.getUserInfo({ 21 | success: res => { 22 | // 可以将 res 发送给后台解码出 unionId 23 | this.globalData.userInfo = res.userInfo 24 | 25 | // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回 26 | // 所以此处加入 callback 以防止这种情况 27 | if (this.userInfoReadyCallback) { 28 | this.userInfoReadyCallback(res) 29 | } 30 | } 31 | }) 32 | } 33 | } 34 | }) 35 | }, 36 | globalData: { 37 | userInfo: null 38 | } 39 | }) -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages":[ 3 | "pages/index/index" 4 | ], 5 | "window":{ 6 | "backgroundTextStyle":"light", 7 | "navigationBarBackgroundColor": "#fff", 8 | "navigationBarTitleText": "WeChat", 9 | "navigationBarTextStyle":"black" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app.wxss: -------------------------------------------------------------------------------- 1 | /**app.wxss**/ 2 | /* .container { 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: space-between; 8 | padding: 200rpx 0; 9 | box-sizing: border-box; 10 | } */ 11 | -------------------------------------------------------------------------------- /images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YasinChan/wechat-scroll-linkage/e27ac57a433ce07b8cc55cb0b7b3592bee917760/images/.DS_Store -------------------------------------------------------------------------------- /images/image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YasinChan/wechat-scroll-linkage/e27ac57a433ce07b8cc55cb0b7b3592bee917760/images/image1.png -------------------------------------------------------------------------------- /images/scroll.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YasinChan/wechat-scroll-linkage/e27ac57a433ce07b8cc55cb0b7b3592bee917760/images/scroll.gif -------------------------------------------------------------------------------- /pages/index/index.js: -------------------------------------------------------------------------------- 1 | //index.js 2 | const constants = require('../../utils/constants.js'); 3 | 4 | const RIGHT_BAR_HEIGHT = 20; // 右侧每一类的 bar 的高度(固定) 5 | const RIGHT_ITEM_HEIGHT = 60; // 右侧每个子类的高度(固定) 6 | const LEFT_ITEM_HEIGHT = 50 // 左侧每个类的高度(固定) 7 | 8 | Page({ 9 | data: { 10 | constants: [], // 数据 11 | toView: null, // 左 => 右联动 右scroll-into-view 所需的id 12 | currentLeftSelect: null, // 当前左侧选择的 13 | eachRightItemToTop: [], // 右侧每类数据到顶部的距离(用来与 右 => 左 联动时监听右侧滚动到顶部的距离比较) 14 | leftToTop: 0 15 | }, 16 | onLoad: function (options) { 17 | this.setData({ 18 | constants: constants, 19 | currentLeftSelect: constants[0].id, 20 | eachRightItemToTop: this.getEachRightItemToTop() 21 | }) 22 | }, 23 | getEachRightItemToTop: function () { // 获取每个右侧的 bar 到顶部的距离,用来做后面的计算。 24 | var obj = {}; 25 | var totop = 0; 26 | obj[constants[0].id] = totop // 右侧第一类肯定是到顶部的距离为 0 27 | for (let i = 1; i < (constants.length + 1); i++) { // 循环来计算每个子类到顶部的高度 28 | totop += (RIGHT_BAR_HEIGHT + constants[i-1].category.length * RIGHT_ITEM_HEIGHT) 29 | obj[constants[i] ? constants[i].id : 'last'] = totop // 这个的目的是 例如有两类,最后需要 0-1 1-2 2-3 的数据,所以需要一个不存在的 'last' 项,此项即为第一类加上第二类的高度。 30 | } 31 | return obj 32 | }, 33 | rightScroll: function (e) { // 监听右侧的滚动事件与 eachRightItemToTop 的循环作对比 从而判断当前可视区域为第几类,从而渲染左侧的对应类。 34 | for (let i = 0; i < this.data.constants.length; i++) { 35 | let left = this.data.eachRightItemToTop[this.data.constants[i].id] 36 | let right = this.data.eachRightItemToTop[this.data.constants[i + 1] ? this.data.constants[i+1].id : 'last'] 37 | if (e.detail.scrollTop < right && e.detail.scrollTop >= left) { 38 | this.setData({ 39 | currentLeftSelect: this.data.constants[i].id, 40 | leftToTop: LEFT_ITEM_HEIGHT * i 41 | }) 42 | } 43 | } 44 | }, 45 | jumpTo: function (e) { // 左侧类的点击事件,点击时,右侧会滚动到对应分类 46 | this.setData({ 47 | toView: e.target.id || e.target.dataset.id, 48 | currentLeftSelect: e.target.id || e.target.dataset.id 49 | }) 50 | } 51 | }) 52 | -------------------------------------------------------------------------------- /pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{item.name}} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{item.name}} 17 | 18 | {{item.category_name}} 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | box-sizing: border-box; 4 | } 5 | 6 | ::-webkit-scrollbar { 7 | width: 0; 8 | height: 0; 9 | color: transparent; 10 | } 11 | 12 | .index { 13 | display: flex; 14 | } 15 | 16 | .index-top { 17 | height: 200px; 18 | } 19 | 20 | .index-left { 21 | flex: 1; 22 | background: #f5f7f9; 23 | } 24 | 25 | .index-right { 26 | flex: 3; 27 | } 28 | 29 | .index-left-text { 30 | padding: 0 10rpx; 31 | height: 50px; 32 | line-height: 50px; 33 | } 34 | 35 | .index-right-text { 36 | box-sizing: border-box; 37 | padding: 40rpx 10rpx; 38 | height: 60px; 39 | } 40 | 41 | .index-right-text-top { 42 | height: 40rpx; 43 | background: #f5f7f9; 44 | } 45 | 46 | .mark { 47 | position: absolute; 48 | top: 200px; 49 | height: 100vh; 50 | width: 100%; 51 | z-index: 10; 52 | } 53 | 54 | .index-right-text-top { 55 | height: 20px; 56 | } -------------------------------------------------------------------------------- /project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "项目配置文件。", 3 | "packOptions": { 4 | "ignore": [] 5 | }, 6 | "setting": { 7 | "urlCheck": true, 8 | "es6": true, 9 | "postcss": true, 10 | "minified": true, 11 | "newFeature": true 12 | }, 13 | "compileType": "miniprogram", 14 | "libVersion": "2.0.8", 15 | "appid": "touristappid", 16 | "projectname": "%E5%AF%B9%E5%AF%B9%E5%AF%B9", 17 | "condition": { 18 | "search": { 19 | "current": -1, 20 | "list": [] 21 | }, 22 | "conversation": { 23 | "current": -1, 24 | "list": [] 25 | }, 26 | "game": { 27 | "currentL": -1, 28 | "list": [] 29 | }, 30 | "miniprogram": { 31 | "current": -1, 32 | "list": [] 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /utils/constants.js: -------------------------------------------------------------------------------- 1 | const constants = [ 2 | { 3 | "id": "id1", 4 | "name": "name1", 5 | "category": [ 6 | { 7 | "category_id": 1, 8 | "category_name": "category1", 9 | }, 10 | { 11 | "category_id": 2, 12 | "category_name": "category2", 13 | }, 14 | { 15 | "category_id": 3, 16 | "category_name": "category3", 17 | } 18 | ], 19 | }, 20 | { 21 | "id": "id2", 22 | "name": "name2", 23 | "category": [ 24 | { 25 | "category_id": 4, 26 | "category_name": "category4", 27 | }, 28 | { 29 | "category_id": 5, 30 | "category_name": "category5", 31 | }, 32 | { 33 | "category_id": 6, 34 | "category_name": "category6", 35 | }, 36 | { 37 | "category_id": 7, 38 | "category_name": "category7", 39 | }, 40 | { 41 | "category_id": 8, 42 | "category_name": "category8", 43 | }, { 44 | "category_id": 9, 45 | "category_name": "category9", 46 | }, 47 | { 48 | "category_id": 10, 49 | "category_name": "category10", 50 | }, 51 | { 52 | "category_id": 11, 53 | "category_name": "category11", 54 | }, 55 | { 56 | "category_id": 12, 57 | "category_name": "category12", 58 | }, 59 | { 60 | "category_id": 13, 61 | "category_name": "category13", 62 | }, 63 | { 64 | "category_id": 14, 65 | "category_name": "category14", 66 | }, 67 | { 68 | "category_id": 15, 69 | "category_name": "category15", 70 | }, 71 | { 72 | "category_id": 16, 73 | "category_name": "category16", 74 | }, 75 | ] 76 | }, 77 | { 78 | "id": "id3", 79 | "name": "name3", 80 | "category": [ 81 | { 82 | "category_id": 17, 83 | "category_name": "category1", 84 | }, 85 | { 86 | "category_id": 18, 87 | "category_name": "category2", 88 | }, 89 | { 90 | "category_id": 19, 91 | "category_name": "category3", 92 | } 93 | ], 94 | }, 95 | { 96 | "id": "id4", 97 | "name": "name4", 98 | "category": [ 99 | { 100 | "category_id": 20, 101 | "category_name": "category1", 102 | }, 103 | { 104 | "category_id": 21, 105 | "category_name": "category2", 106 | }, 107 | { 108 | "category_id": 22, 109 | "category_name": "category3", 110 | }, 111 | { 112 | "category_id": 23, 113 | "category_name": "category4", 114 | }, 115 | { 116 | "category_id": 24, 117 | "category_name": "category5", 118 | }, 119 | { 120 | "category_id": 25, 121 | "category_name": "category6", 122 | }, 123 | { 124 | "category_id": 26, 125 | "category_name": "category7", 126 | }, 127 | { 128 | "category_id": 27, 129 | "category_name": "category8", 130 | }, 131 | { 132 | "category_id": 28, 133 | "category_name": "category9", 134 | }, 135 | { 136 | "category_id": 29, 137 | "category_name": "category10", 138 | }, 139 | { 140 | "category_id": 30, 141 | "category_name": "category11", 142 | }, 143 | { 144 | "category_id": 31, 145 | "category_name": "category12", 146 | }, 147 | { 148 | "category_id": 32, 149 | "category_name": "category13", 150 | } 151 | ], 152 | }, 153 | { 154 | "id": "id5", 155 | "name": "name5", 156 | "category": [ 157 | { 158 | "category_id": 33, 159 | "category_name": "category1", 160 | }, 161 | { 162 | "category_id": 34, 163 | "category_name": "category2", 164 | }, 165 | { 166 | "category_id": 35, 167 | "category_name": "category3", 168 | } 169 | ], 170 | }, 171 | { 172 | "id": "id6", 173 | "name": "name6", 174 | "category": [ 175 | { 176 | "category_id": 36, 177 | "category_name": "category1", 178 | }, 179 | { 180 | "category_id": 37, 181 | "category_name": "category2", 182 | }, 183 | { 184 | "category_id": 38, 185 | "category_name": "category3", 186 | } 187 | ], 188 | }, 189 | { 190 | "id": "id7", 191 | "name": "name7", 192 | "category": [ 193 | { 194 | "category_id": 39, 195 | "category_name": "category1", 196 | }, 197 | { 198 | "category_id": 40, 199 | "category_name": "category2", 200 | }, 201 | { 202 | "category_id": 41, 203 | "category_name": "category3", 204 | } 205 | ], 206 | }, 207 | { 208 | "id": "id8", 209 | "name": "name8", 210 | "category": [ 211 | { 212 | "category_id": 42, 213 | "category_name": "category1", 214 | }, 215 | { 216 | "category_id": 43, 217 | "category_name": "category2", 218 | }, 219 | { 220 | "category_id": 44, 221 | "category_name": "category3", 222 | } 223 | ], 224 | }, 225 | { 226 | "id": "id9", 227 | "name": "name9", 228 | "category": [ 229 | { 230 | "category_id": 45, 231 | "category_name": "category1", 232 | }, 233 | { 234 | "category_id": 46, 235 | "category_name": "category2", 236 | }, 237 | { 238 | "category_id": 47, 239 | "category_name": "category3", 240 | } 241 | ], 242 | }, 243 | { 244 | "id": "id10", 245 | "name": "name10", 246 | "category": [ 247 | { 248 | "category_id": 48, 249 | "category_name": "category1", 250 | }, 251 | { 252 | "category_id": 49, 253 | "category_name": "category2", 254 | }, 255 | { 256 | "category_id": 50, 257 | "category_name": "category3", 258 | } 259 | ], 260 | }, 261 | { 262 | "id": "id11", 263 | "name": "name11", 264 | "category": [ 265 | { 266 | "category_id": 51, 267 | "category_name": "category1", 268 | }, 269 | { 270 | "category_id": 52, 271 | "category_name": "category2", 272 | }, 273 | { 274 | "category_id": 53, 275 | "category_name": "category3", 276 | } 277 | ], 278 | }, 279 | { 280 | "id": "id12", 281 | "name": "name12", 282 | "category": [ 283 | { 284 | "category_id": 54, 285 | "category_name": "category1", 286 | }, 287 | { 288 | "category_id": 55, 289 | "category_name": "category2", 290 | }, 291 | { 292 | "category_id": 56, 293 | "category_name": "category3", 294 | } 295 | ], 296 | }, 297 | { 298 | "id": "id13", 299 | "name": "name13", 300 | "category": [ 301 | { 302 | "category_id": 57, 303 | "category_name": "category1", 304 | }, 305 | { 306 | "category_id": 58, 307 | "category_name": "category2", 308 | }, 309 | { 310 | "category_id": 59, 311 | "category_name": "category3", 312 | } 313 | ], 314 | }, 315 | { 316 | "id": "id14", 317 | "name": "name14", 318 | "category": [ 319 | { 320 | "category_id": 60, 321 | "category_name": "category1", 322 | }, 323 | { 324 | "category_id": 61, 325 | "category_name": "category2", 326 | }, 327 | { 328 | "category_id": 62, 329 | "category_name": "category3", 330 | } 331 | ], 332 | }, 333 | { 334 | "id": "id15", 335 | "name": "name15", 336 | "category": [ 337 | { 338 | "category_id": 63, 339 | "category_name": "category1", 340 | }, 341 | { 342 | "category_id": 64, 343 | "category_name": "category2", 344 | }, 345 | { 346 | "category_id": 65, 347 | "category_name": "category3", 348 | } 349 | ], 350 | }, 351 | { 352 | "id": "id16", 353 | "name": "name16", 354 | "category": [ 355 | { 356 | "category_id": 66, 357 | "category_name": "category1", 358 | }, 359 | { 360 | "category_id": 67, 361 | "category_name": "category2", 362 | }, 363 | { 364 | "category_id": 68, 365 | "category_name": "category3", 366 | }, 367 | { 368 | "category_id": 69, 369 | "category_name": "category4", 370 | }, 371 | { 372 | "category_id": 70, 373 | "category_name": "category5", 374 | }, 375 | { 376 | "category_id": 71, 377 | "category_name": "category6", 378 | }, 379 | { 380 | "category_id": 72, 381 | "category_name": "category7", 382 | }, 383 | { 384 | "category_id": 73, 385 | "category_name": "category8", 386 | }, 387 | { 388 | "category_id": 74, 389 | "category_name": "category9", 390 | }, 391 | { 392 | "category_id": 75, 393 | "category_name": "category10", 394 | }, 395 | { 396 | "category_id": 76, 397 | "category_name": "category11", 398 | }, 399 | { 400 | "category_id": 77, 401 | "category_name": "category12", 402 | }, 403 | { 404 | "category_id": 78, 405 | "category_name": "category13", 406 | } 407 | ], 408 | }, 409 | ] 410 | 411 | module.exports = constants 412 | 413 | --------------------------------------------------------------------------------