├── README.md ├── app.js ├── app.json ├── app.wxss ├── demo.GIF ├── icons └── play.png ├── pages ├── index │ ├── index.js │ ├── index.json │ ├── index.wxml │ └── index.wxss └── logs │ ├── logs.js │ ├── logs.json │ ├── logs.wxml │ └── logs.wxss ├── project.config.json └── utils ├── event.js └── util.js /README.md: -------------------------------------------------------------------------------- 1 | # vision 2 | 小程序 类似抖音、微视的滑动切换视频播放 3 | -------------------------------------------------------------------------------- /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 | "pages/logs/logs" 5 | ], 6 | "window":{ 7 | "backgroundTextStyle":"light", 8 | "navigationBarBackgroundColor": "#fff", 9 | "navigationBarTitleText": "WeChat", 10 | "navigationBarTextStyle":"black", 11 | "navigationStyle": "custom" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app.wxss: -------------------------------------------------------------------------------- 1 | /**app.wxss**/ -------------------------------------------------------------------------------- /demo.GIF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xunuo0x/vision/820d490a64c31c68a64b3facab3df31e4bb88d3f/demo.GIF -------------------------------------------------------------------------------- /icons/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xunuo0x/vision/820d490a64c31c68a64b3facab3df31e4bb88d3f/icons/play.png -------------------------------------------------------------------------------- /pages/index/index.js: -------------------------------------------------------------------------------- 1 | //index.js 2 | //获取应用实例 3 | import * as event from '../../utils/event.js' 4 | const app = getApp() 5 | const windowHeight = wx.getSystemInfoSync().windowHeight 6 | 7 | Page({ 8 | data: { 9 | percent: 1, 10 | autoplay: true, 11 | controls: false, 12 | showFullscreenBtn: false, 13 | showPlayBtn: false, 14 | showFullscreenBtn: false, 15 | showCenterPlayBtn: false, 16 | enableProgressGesture: false, 17 | showProgress: false, 18 | playState: true, 19 | animationShow: false, 20 | currentTranslateY: 0, 21 | touchStartingY: 0, 22 | videos: [ 23 | { 24 | videoUrl: "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200faf0000bg5joco1ahq89k7ik9j0&line=0", 25 | durations: 10, 26 | poster: "https://p3.pstatp.com/large/131040001488de047292a.jpg" 27 | }, 28 | { 29 | videoUrl: "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f2f0000bg2dbhb6j2qj3mr8pa9g&line=0", 30 | durations: 10, 31 | poster: "https://p1.pstatp.com/large/12bea0008f8a226fc53c3.jpg" 32 | }, 33 | { 34 | videoUrl: "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200fce0000bg36q72j2boojh1t030g&line=0", 35 | durations: 10, 36 | poster: "https://p99.pstatp.com/large/12c5c0009891b32e947b7.jpg" 37 | }, 38 | { 39 | videoUrl: "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300fd10000bfrb9mlpimm72a92fsj0&line=0", 40 | durations: 10, 41 | poster: "https://p99.pstatp.com/large/12246000525d4c87900e7.jpg" 42 | } 43 | ], 44 | videoIndex: 0, 45 | objectFit: "contain" 46 | }, 47 | onLoad: function () { 48 | // 滑动 49 | this.videoChange = throttle(this.touchEndHandler, 200) 50 | // 绑定updateVideoIndex事件,更新当前播放视频index 51 | event.on('updateVideoIndex', this, function (index) { 52 | console.log('event updateVideoIndex:', index) 53 | setTimeout(() => { 54 | this.setData({ 55 | animationShow: false, 56 | playState: true 57 | }, ()=> { 58 | // 切换src后,video不能立即播放,settimeout一下 59 | setTimeout(()=> { 60 | this.vvideo.play() 61 | },100) 62 | }) 63 | }, 500) 64 | }) 65 | }, 66 | bindplay() { 67 | console.log('--- video play ---') 68 | }, 69 | binderror(err) { 70 | console.log(err) 71 | }, 72 | bindtimeupdate(e) { 73 | let percent = (e.detail.currentTime / e.detail.duration)*100 74 | this.setData({ 75 | percent: percent.toFixed(2) 76 | }) 77 | }, 78 | onReady: function () { 79 | this.vvideo = wx.createVideoContext("kdvideo", this) 80 | this.animation = wx.createAnimation({ 81 | duration: 500, 82 | transformOrigin: '0 0 0' 83 | }) 84 | }, 85 | changePlayStatus() { 86 | console.log('changePlayStatus') 87 | let playState = !this.data.playState 88 | if (playState) { 89 | this.vvideo.play() 90 | } else { 91 | this.vvideo.pause() 92 | } 93 | this.setData({ 94 | playState: playState 95 | }) 96 | }, 97 | touchStart(e) { 98 | let touchStartingY = this.data.touchStartingY 99 | console.log('------touchStart------') 100 | touchStartingY = e.touches[0].clientY 101 | this.setData({ 102 | touchStartingY: touchStartingY 103 | }) 104 | }, 105 | touchMove(e) { 106 | // this.videoChange(e) 107 | }, 108 | touchEndHandler(e) { 109 | let touchStartingY = this.data.touchStartingY 110 | let deltaY = e.changedTouches[0].clientY - touchStartingY 111 | console.log('deltaY ',deltaY) 112 | 113 | let index = this.data.videoIndex 114 | if (deltaY > 100 && index !== 0) { 115 | // 更早地设置 animationShow 116 | this.setData({ 117 | animationShow: true 118 | }, () => { 119 | console.log('-1 切换') 120 | this.createAnimation(-1, index).then((res) => { 121 | console.log(res) 122 | this.setData({ 123 | animation: this.animation.export(), 124 | videoIndex: res.index, 125 | currentTranslateY: res.currentTranslateY, 126 | percent: 1 127 | }, () => { 128 | event.emit('updateVideoIndex', res.index) 129 | }) 130 | }) 131 | }) 132 | } else if (deltaY < -100 && index !== (this.data.videos.length - 1)) { 133 | this.setData({ 134 | animationShow: true 135 | }, () => { 136 | console.log('+1 切换') 137 | this.createAnimation(1, index).then((res) => { 138 | console.log(res) 139 | this.setData({ 140 | animation: this.animation.export(), 141 | videoIndex: res.index, 142 | currentTranslateY: res.currentTranslateY, 143 | percent: 1 144 | }, () => { 145 | event.emit('updateVideoIndex', res.index) 146 | }) 147 | }) 148 | }) 149 | } 150 | }, 151 | touchEnd(e) { 152 | console.log('------touchEnd------') 153 | this.videoChange(e) 154 | }, 155 | touchCancel(e) { 156 | console.log('------touchCancel------') 157 | console.log(e) 158 | }, 159 | createAnimation(direction, index) { 160 | // direction为-1,向上滑动,animationImage1为(index)的poster,animationImage2为(index+1)的poster 161 | // direction为1,向下滑动,animationImage1为(index-1)的poster,animationImage2为(index)的poster 162 | let videos = this.data.videos 163 | let currentTranslateY = this.data.currentTranslateY 164 | console.log('direction ', direction) 165 | console.log('index ', index) 166 | 167 | // 更新 videoIndex 168 | index += direction 169 | currentTranslateY += -direction*windowHeight 170 | console.log('currentTranslateY: ', currentTranslateY) 171 | this.animation.translateY(currentTranslateY).step() 172 | 173 | return Promise.resolve({ 174 | index: index, 175 | currentTranslateY: currentTranslateY 176 | }) 177 | } 178 | }) 179 | function throttle (fn, delay) { 180 | var timer = null; 181 | return function () { 182 | var context = this, args = arguments; 183 | clearTimeout(timer); 184 | timer = setTimeout(function () { 185 | fn.apply(context, args); 186 | }, delay); 187 | } 188 | } -------------------------------------------------------------------------------- /pages/index/index.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | /**index.wxss**/ 2 | page { 3 | height: 100vh; 4 | width: 100vw; 5 | position: fixed; 6 | } 7 | .container { 8 | height: 100vh; 9 | width: 100vw; 10 | } 11 | .vvideo { 12 | height: 100vh; 13 | width: 100vw; 14 | position: absolute; 15 | top: 0; 16 | left: 0; 17 | } 18 | .play-btn { 19 | position: absolute; 20 | top: 50%; 21 | left: 50%; 22 | transform: translate(-50%,-50%); 23 | height: 128rpx; 24 | width: 128rpx; 25 | } 26 | .progress { 27 | position: absolute; 28 | left: 0; 29 | bottom: 60px; 30 | height: 4px; 31 | width: 100vw; 32 | } 33 | .animation-wrapper { 34 | position: relative; 35 | height: 100vh; 36 | width: 100vw; 37 | background-color: #000; 38 | } 39 | .animation-image { 40 | height: 100vh; 41 | width: 100vw; 42 | display: block; 43 | } 44 | .animationPre { 45 | transform: translateY(-100%); 46 | } -------------------------------------------------------------------------------- /pages/logs/logs.js: -------------------------------------------------------------------------------- 1 | //logs.js 2 | const util = require('../../utils/util.js') 3 | 4 | Page({ 5 | data: { 6 | logs: [] 7 | }, 8 | onLoad: function () { 9 | this.setData({ 10 | logs: (wx.getStorageSync('logs') || []).map(log => { 11 | return util.formatTime(new Date(log)) 12 | }) 13 | }) 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /pages/logs/logs.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "查看启动日志" 3 | } -------------------------------------------------------------------------------- /pages/logs/logs.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{index + 1}}. {{log}} 5 | 6 | 7 | -------------------------------------------------------------------------------- /pages/logs/logs.wxss: -------------------------------------------------------------------------------- 1 | .log-list { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 40rpx; 5 | } 6 | .log-item { 7 | margin: 10rpx; 8 | } 9 | -------------------------------------------------------------------------------- /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 | "autoAudits": false 13 | }, 14 | "compileType": "miniprogram", 15 | "libVersion": "2.0.3", 16 | "appid": "wx6fa4cddaa65450f2", 17 | "projectname": "vision", 18 | "debugOptions": { 19 | "hidedInDevtools": [] 20 | }, 21 | "isGameTourist": false, 22 | "condition": { 23 | "search": { 24 | "current": -1, 25 | "list": [] 26 | }, 27 | "conversation": { 28 | "current": -1, 29 | "list": [] 30 | }, 31 | "game": { 32 | "currentL": -1, 33 | "list": [] 34 | }, 35 | "miniprogram": { 36 | "current": -1, 37 | "list": [] 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /utils/event.js: -------------------------------------------------------------------------------- 1 | var events = {}; 2 | 3 | function on(name, self, callback) { 4 | var tuple = [self, callback]; 5 | var callbacks = events[name]; 6 | if (Array.isArray(callbacks)) { 7 | callbacks.push(tuple); 8 | } else { 9 | events[name] = [tuple]; 10 | } 11 | } 12 | 13 | function remove(name, self) { 14 | var callbacks = events[name]; 15 | if (Array.isArray(callbacks)) { 16 | events[name] = callbacks.filter((tuple) => { 17 | return tuple[0] != self; 18 | }) 19 | } 20 | } 21 | 22 | function emit(name, data) { 23 | var callbacks = events[name]; 24 | if (Array.isArray(callbacks)) { 25 | callbacks.map((tuple) => { 26 | var self = tuple[0]; 27 | var callback = tuple[1]; 28 | callback.call(self, data); 29 | }) 30 | } 31 | } 32 | 33 | export { 34 | on, 35 | remove, 36 | emit 37 | } -------------------------------------------------------------------------------- /utils/util.js: -------------------------------------------------------------------------------- 1 | const formatTime = date => { 2 | const year = date.getFullYear() 3 | const month = date.getMonth() + 1 4 | const day = date.getDate() 5 | const hour = date.getHours() 6 | const minute = date.getMinutes() 7 | const second = date.getSeconds() 8 | 9 | return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':') 10 | } 11 | 12 | const formatNumber = n => { 13 | n = n.toString() 14 | return n[1] ? n : '0' + n 15 | } 16 | 17 | module.exports = { 18 | formatTime: formatTime 19 | } 20 | --------------------------------------------------------------------------------