├── .gitignore
├── images
└── onepixel.png
├── README.md
├── package.json
├── LICENSE
├── dist
├── app.css
└── app.min.js
├── Gruntfile.js
├── index.html
├── views
└── pc.html
├── css
└── app.css
└── js
└── app.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules
--------------------------------------------------------------------------------
/images/onepixel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/git-onepixel/guesture/HEAD/images/onepixel.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # H5单页面手势滑屏切换示例
2 |
3 | - 手指在屏幕上滑动时,页面跟随移动
4 | - 当手指离开屏幕时,计算手指在屏幕上停留的时间
5 |
6 | - 如果停留时间小于300ms,则认为是快速滑动切换,页面切换到下一页
7 |
8 | - 如果停留时间大于300ms,则认为是慢速滑动,慢速滑动按如下规则处理:
9 | - 如果滑动距离小于屏幕宽度的50%,则回退到上一页
10 | - 如果滑动距离大于屏幕宽度的50%,则切换到下一页
11 |
12 | ## 对于多手指触摸操作:
13 | - 第一个手指触摸时,正常滑动
14 | - 第二个手指按下时,不做任何响应操作,继续原有的滑动
15 | - 当任意一个手指离开屏幕时,结束滑动,剩余的手指操作不做任何处理
16 | - 当第二个手指再次按下时,触发新的滑动开始,但由于获取的是touches[0],因此,第一个手指移动才会引起页面的滑动
17 | - 支持多手指同时按下时进行滑动
18 |
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "H5SPA",
3 | "version": "1.0.0",
4 | "description": "This is a webapp demo",
5 | "main": "app.js",
6 | "scripts": {
7 | "test": "app"
8 | },
9 | "keywords": [
10 | "h5",
11 | "spa"
12 | ],
13 | "author": "onepixel",
14 | "license": "MIT",
15 | "devDependencies": {
16 | "grunt": "^0.4.5",
17 | "grunt-contrib-concat": "^0.5.1",
18 | "grunt-contrib-cssmin": "^0.14.0",
19 | "grunt-contrib-uglify": "^0.11.0",
20 | "grunt-contrib-watch": "^0.6.1"
21 | },
22 | "src":{
23 | "A":"a",
24 | "B":"b"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 git-onepixel
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/dist/app.css:
--------------------------------------------------------------------------------
1 | .viewport,body,html{height:100%;overflow:hidden;position:relative}body,html{width:100%;margin:0;padding:0;font-family:Arial}.viewport{width:500%;display:-webkit-box;-webkit-transform:translate3d(0,0,0);backface-visibility:hidden}.pageview{-webkit-box-flex:1;width:0}.pagenumber{display:-webkit-box;position:absolute;bottom:2em;left:35%;height:2em;width:30%}.pagenumber div{-webkit-box-flex:1;width:0;position:relative}.pagenumber .now:after{background:rgba(255,255,255,1)!important}.pagenumber div:after{content:"";width:.4em;height:.4em;background:rgba(255,255,255,.5);border-radius:2em;position:absolute;top:50%;left:50%;margin-top:-.2em;margin-left:-.2em}h3{text-align:center;font-family:Microsoft YaHei,Arial;color:#fff;font-size:1.7em;font-weight:400;padding:1em 0;margin:0}button{width:40%;margin:auto;height:3em;background:#fff6de;line-height:3rem;color:#000;padding:0;border:none;display:block}@media screen and (max-width:360px){body,html{font-size:15px}}@media screen and (min-width:360px) and (max-width:400px){body,html{font-size:16px}}@media screen and (min-width:400px) and (max-width:460px){body,html{font-size:18px}}@media screen and (min-width:460px){body,html{font-size:24px}}
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt) {
2 | var pkg = grunt.file.readJSON("package.json");
3 |
4 | grunt.initConfig({
5 | pkg:pkg,
6 | //配置js压缩混淆插件
7 | uglify:{
8 | options:{
9 | banner:'/*! <%=pkg.name%> <%=pkg.version%> | <%=grunt.template.today("yyyy-mm-dd HH:MM:ss")%> */\n'
10 | },
11 | build:{
12 | src:'js/app.js',
13 | dest:'dist/app.min.js'
14 | }
15 | },
16 | //配置css压缩插件
17 | cssmin:{
18 | options: {
19 | shorthandCompacting: false,
20 | roundingPrecision: -1
21 | },
22 | build: {
23 | src:'css/app.css',
24 | dest:'dist/app.css'
25 | }
26 | },
27 | watch:{
28 | scripts: {
29 | files: ['js/*.*','css/*.*'],
30 | tasks: ['cssmin','uglify'],
31 | options: {
32 | spawn: false
33 | }
34 | }
35 | }
36 | });
37 |
38 | grunt.loadNpmTasks('grunt-contrib-uglify');
39 | grunt.loadNpmTasks('grunt-contrib-cssmin');
40 | grunt.loadNpmTasks('grunt-contrib-watch');
41 |
42 | grunt.registerTask('default',['cssmin','uglify','watch']);
43 | }
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | H5单页面手势滑屏切换
5 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
页面-1
15 |
16 |
17 |
18 |
页面-2
19 |
20 |
21 |
页面-3
22 |
23 |
24 |
页面-4
25 |
26 |
27 |
页面-5
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/dist/app.min.js:
--------------------------------------------------------------------------------
1 | /*! H5SPA 1.0.0 | 2016-08-20 22:47:44 */
2 | !function(a,b){var c=0,d=-1,e=1,f=null,g={init:function(){/(windows)/i.test(navigator.userAgent)&&(location.href="views/pc.html"),b.addEventListener("DOMContentLoaded",function(){f=b.querySelectorAll(".pagenumber div"),g.bindTouchEvent(),g.bindBtnClick(),g.setPageNow()}.bind(g),!1)}(),bindBtnClick:function(){var a=b.querySelector("#testbtn");a.addEventListener("touchstart",function(){console.log("touch")})},transform:function(a){this.style.webkitTransform="translate3d("+a+"px,0,0)",c=a},setPageNow:function(){-1!=d&&(f[d].className=""),d=e-1,f[d].className="now"},bindTouchEvent:function(){var d,g,h=b.querySelector("#viewport"),i=a.innerWidth,j=-i*(f.length-1),k=0,l=0,m="left",n=!1,o=0;b.addEventListener("touchstart",function(a){a.preventDefault();var b=a.touches[0];d=b.pageX,g=b.pageY,k=c,h.style.webkitTransition="",o=(new Date).getTime(),n=!1}.bind(this),!1),b.addEventListener("touchmove",function(a){a.preventDefault();var b=a.touches[0],c=b.pageX-d,e=b.pageY-g;if(Math.abs(c)>Math.abs(e)){l=c;var f=k+c;0>=f&&f>=j&&(this.transform.call(h,f),n=!0),m=c>0?"right":"left"}}.bind(this),!1),b.addEventListener("touchend",function(a){a.preventDefault();var b=0,d=(new Date).getTime()-o;n&&(h.style.webkitTransition="0.3s ease -webkit-transform",300>d?(b="left"==m?c-(i+l):c+i-l,b=b>0?0:b,b=j>b?j:b):Math.abs(l)/i<.5?b=c-l:(b="left"==m?c-(i+l):c+i-l,b=b>0?0:b,b=j>b?j:b),this.transform.call(h,b),e=Math.round(Math.abs(b)/i)+1,setTimeout(function(){this.setPageNow()}.bind(this),100))}.bind(this),!1)}}}(window,document);
--------------------------------------------------------------------------------
/views/pc.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | H5单页面切换骨架
6 |
7 |
12 |
80 |
81 | H5单页面手势滑屏切换(请用手机或模拟器访问)
82 |
83 |
84 |

85 |
86 |
87 |
--------------------------------------------------------------------------------
/css/app.css:
--------------------------------------------------------------------------------
1 | html,body{
2 | width: 100%;
3 | height: 100%;
4 | margin: 0;
5 | padding: 0;
6 | position: relative;
7 | overflow: hidden;
8 | font-family: Arial;
9 | background: #000;
10 | }
11 | .container{
12 | position:absolute;
13 | height: 200px;
14 | top: 50%;
15 | margin-top:-100px;
16 | }
17 | .viewport{
18 | width: 500%;
19 | height: 100%;
20 | display: -webkit-box;
21 | overflow: hidden;
22 | /*pointer-events: none; 去掉这句话*/
23 | -webkit-transform: translate3d(0,0,0);
24 | backface-visibility: hidden;
25 | position: relative;
26 | }
27 | .pageview{
28 | -webkit-box-flex: 1;
29 | width: 0;
30 | margin: 0 5px;
31 | }
32 | .pagenumber{
33 | display: -webkit-box;
34 | position: absolute;
35 | bottom: 5%;
36 | left: 35%;
37 | height: 1em;
38 | width: 30%;
39 | }
40 | .pagenumber div{
41 | -webkit-box-flex: 1;
42 | width: 0;
43 | position: relative;
44 | }
45 | .pagenumber .now:after {
46 | background: rgba(255,255,255,1) !important;
47 | }
48 | .pagenumber div:after{
49 | content: "";
50 | width: 6px;
51 | height: 6px;
52 | background: rgba(255,255,255,0.3);
53 | border-radius: 50%;
54 | position: absolute;
55 | top: 50%;
56 | left: 50%;
57 | margin-top: -3px;
58 | margin-left: -3px;
59 | }
60 | h3{
61 | text-align: center;
62 | font-family: Microsoft YaHei,Arial;
63 | color: #fff;
64 | font-size: 1.5em;
65 | font-weight: normal;
66 | padding: 1em 0;
67 | margin: 0;
68 | }
69 | button{
70 | width: 40%;
71 | margin: auto;
72 | height: 3em;
73 | background: #fff6de;
74 | line-height: 3rem;
75 | color: #000;
76 | padding: 0;
77 | border: none;
78 | display: block;
79 | }
80 | @media screen and (max-width: 360px) {
81 | html,body{font-size: 15px}
82 | }
83 | @media screen and (min-width: 360px) and (max-width: 400px) {
84 | html,body{font-size: 16px}
85 | }
86 | @media screen and (min-width: 400px) and (max-width: 460px) {
87 | html,body{font-size: 18px}
88 | }
89 | @media screen and (min-width: 460px){
90 | html,body{font-size: 24px}
91 | }
92 |
93 |
--------------------------------------------------------------------------------
/js/app.js:
--------------------------------------------------------------------------------
1 | (function (window, document) {
2 | var currentPosition = 0; // 记录当前页面位置
3 | var currentPoint = -1; // 记录当前点的位置
4 | var pageNow = 1; // 当前页码
5 | var points = null; // 页码数
6 |
7 | var app = {
8 | init: function () {
9 | if (/(windows)/i.test(navigator.userAgent)) {
10 | location.href = 'views/pc.html';
11 | }
12 | document.addEventListener('DOMContentLoaded', function () {
13 | points = document.querySelectorAll('.pagenumber div');
14 | app.bindTouchEvent(); // 绑定触摸事件
15 | app.bindBtnClick(); // 绑定按钮点击事件
16 | app.setPageNow(); // 设置初始页码
17 | }.bind(app), false);
18 | }(),
19 |
20 |
21 | bindBtnClick: function () {
22 | var button = document.querySelector('#testbtn');
23 | button.addEventListener('touchstart', function(){
24 | console.log('touch');
25 | })
26 |
27 | },
28 |
29 |
30 | // 页面平移
31 | transform: function (translate) {
32 | this.style.webkitTransform = 'translate3d(' + translate + 'px, 0, 0)';
33 | currentPosition = translate;
34 |
35 | },
36 |
37 | /**
38 | * 设置当前页码
39 | */
40 | setPageNow: function () {
41 | if (currentPoint != -1) {
42 | points[currentPoint].className = '';
43 | }
44 | currentPoint = pageNow - 1;
45 | points[currentPoint].className = 'now';
46 | },
47 |
48 | /**
49 | * 绑定触摸事件
50 | */
51 | bindTouchEvent: function () {
52 | var viewport = document.querySelector('#viewport');
53 | var pageWidth = window.innerWidth; // 页面宽度
54 | var maxWidth = - pageWidth * (points.length-1); // 页面滑动最后一页的位置
55 | var startX, startY;
56 | var initialPos = 0; // 手指按下的屏幕位置
57 | var moveLength = 0; // 手指当前滑动的距离
58 | var direction = 'left'; // 滑动的方向
59 | var isMove = false; // 是否发生左右滑动
60 | var startT = 0; // 记录手指按下去的时间
61 | var isTouchEnd = true; // 标记当前滑动是否结束(手指已离开屏幕)
62 |
63 | // 手指放在屏幕上
64 | viewport.addEventListener('touchstart', function (e) {
65 | e.preventDefault();
66 | // 单手指触摸或者多手指同时触摸,禁止第二个手指延迟操作事件
67 | if (e.touches.length === 1 || isTouchEnd) {
68 | var touch = e.touches[0];
69 | startX = touch.pageX;
70 | startY = touch.pageY;
71 | initialPos = currentPosition; // 本次滑动前的初始位置
72 | viewport.style.webkitTransition = ''; // 取消动画效果
73 | startT = + new Date(); // 记录手指按下的开始时间
74 | isMove = false; // 是否产生滑动
75 | isTouchEnd = false; // 当前滑动开始
76 | }
77 | }.bind(this), false);
78 |
79 | // 手指在屏幕上滑动,页面跟随手指移动
80 | viewport.addEventListener('touchmove', function (e) {
81 | e.preventDefault();
82 |
83 | // 如果当前滑动已结束,不管其他手指是否在屏幕上都禁止该事件
84 | if (isTouchEnd) return ;
85 |
86 | var touch = e.touches[0];
87 | var deltaX = touch.pageX - startX;
88 | var deltaY = touch.pageY - startY;
89 |
90 | var translate = initialPos + deltaX; // 当前需要移动到的位置
91 | // 如果translate>0 或 < maxWidth,则表示页面超出边界
92 | if (translate > 0) {
93 | translate = 0;
94 | }
95 | if (translate < maxWidth) {
96 | translate = maxWidth;
97 | }
98 | deltaX = translate - initialPos;
99 | this.transform.call(viewport, translate);
100 | isMove = true;
101 | moveLength = deltaX;
102 | direction = deltaX > 0 ? 'right' : 'left'; // 判断手指滑动的方向
103 | }.bind(this),false);
104 |
105 | // 手指离开屏幕时,计算最终需要停留在哪一页
106 | viewport.addEventListener('touchend', function (e) {
107 | e.preventDefault();
108 | var translate = 0;
109 | // 计算手指在屏幕上停留的时间
110 | var deltaT = + new Date() - startT;
111 | // 发生了滑动,并且当前滑动事件未结束
112 | if (isMove && !isTouchEnd) {
113 | isTouchEnd = true; // 标记当前完整的滑动事件已经结束
114 | // 使用动画过渡让页面滑动到最终的位置
115 | viewport.style.webkitTransition = '0.3s ease -webkit-transform';
116 | if (deltaT < 300) { // 如果停留时间小于300ms,则认为是快速滑动,无论滑动距离是多少,都停留到下一页
117 | if (currentPosition === 0 && translate === 0) {
118 | return ;
119 | }
120 | translate = direction === 'left' ?
121 | currentPosition - (pageWidth + moveLength)
122 | : currentPosition + pageWidth - moveLength;
123 | // 如果最终位置超过边界位置,则停留在边界位置
124 | // 左边界
125 | translate = translate > 0 ? 0 : translate;
126 | // 右边界
127 | translate = translate < maxWidth ? maxWidth : translate;
128 | } else {
129 | // 如果滑动距离小于屏幕的50%,则退回到上一页
130 | if (Math.abs(moveLength) / pageWidth < 0.5) {
131 | translate = currentPosition - moveLength;
132 | } else {
133 | // 如果滑动距离大于屏幕的50%,则滑动到下一页
134 | translate = direction === 'left'?
135 | currentPosition - (pageWidth + moveLength)
136 | : currentPosition + pageWidth - moveLength;
137 | translate = translate > 0 ? 0 : translate;
138 | translate = translate < maxWidth ? maxWidth : translate;
139 | }
140 | }
141 |
142 | // 执行滑动,让页面完整的显示到屏幕上
143 | this.transform.call(viewport, translate);
144 | // 计算当前的页码
145 | pageNow = Math.round(Math.abs(translate) / pageWidth) + 1;
146 |
147 | setTimeout(function () {
148 | // 设置页码,DOM操作需要放到异步队列中,否则会出现卡顿
149 | this.setPageNow();
150 | }.bind(this), 100);
151 | }
152 | }.bind(this), false);
153 | }
154 | }
155 | })(window, document);
156 |
--------------------------------------------------------------------------------