├── 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 |
--------------------------------------------------------------------------------