├── .gitignore ├── Gruntfile.js ├── LICENSE ├── README.md ├── css └── app.css ├── dist ├── app.css └── app.min.js ├── images └── onepixel.png ├── index.html ├── js └── app.js ├── package.json └── views └── pc.html /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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}} -------------------------------------------------------------------------------- /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); -------------------------------------------------------------------------------- /images/onepixel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/git-onepixel/guesture/517e014fdd7a5fe6329bc97941f5147900cf0f3f/images/onepixel.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /views/pc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | H5单页面切换骨架 6 | 7 | 12 | 80 | 81 |

H5单页面手势滑屏切换(请用手机或模拟器访问)

82 |
83 |
84 | 85 |
86 | 87 | --------------------------------------------------------------------------------