小球拖动吸附
前段时间,登录起点的移动站,发现上面的'最近阅读',小球很有意思
自己辛辛苦苦研究了两个小时,终于做出来了类似的效果,却发现人家的效果远没有那么简单,当拖住小球用力一扔,小球会有惯性效果,并且碰到边缘进行回弹,最后才会吸附到边缘,心头一颤,好牛逼啊,真是不看不知道,一看吓一跳(其实,这个功能估计没几个人知道,苦逼的程序员,做出来的效果都没几个人发现,要不是老子心细来研究一下,就错过了),我就在想,惯性这个情况怎么来判断,又怎么实现,情不自禁的将手伸到网上,一捞,果然有一篇帖子<开源移动端元素拖拽惯性弹动以及下拉加载两个JS>,打开一看,我了个天,你大爷的,竟然已经有大神模仿出来了起点上的效果,还封装好贴起来了,当时心里一万头草泥马跑过,真是
早知道有这东西,我还写个屁啊....泪流满面,写也就写了,还没人家写的好,真扯淡,心一横,研究一下.
先贴一下最初的写好的版本...
<div class="i-pendant" id="pendant"></div>
.i-pendant { width: 60px; height: 60px; border-radius: 50%; background: #f00; position: fixed; top: 300px; right: 0; z-index: 90; }
// tween函数 var Tween = { bounceIn: function(t, b, c, d){ //弹球减振(弹球渐出) return c - Tween['bounceOut'](d-t, 0, c, d) + b; }, bounceOut: function(t, b, c, d){ if ((t/=d) < (1/2.75)) { return c*(7.5625*t*t) + b; } else if (t < (2/2.75)) { return c*(7.5625*(t-=(1.5/2.75))*t + 0.75) + b; } else if (t < (2.5/2.75)) { return c*(7.5625*(t-=(2.25/2.75))*t + 0.9375) + b; } return c*(7.5625*(t-=(2.625/2.75))*t + 0.984375) + b; }, bounceBoth: function(t, b, c, d){ if (t < d/2) { return Tween['bounceIn'](t*2, 0, c, d) * 0.5 + b; } return Tween['bounceOut'](t*2-d, 0, c, d) * 0.5 + c*0.5 + b; } } var pendant = document.getElementById(obj) || document.getElementsByClassName(obj)[0], posX = parseInt(pendant.offsetLeft), posY = parseInt(pendant.offsetTop), pW = pendant.clientWidth, pH = pendant.clientHeight, screenWidth = document.documentElement.clientWidth, screenHeight = document.documentElement.clientHeight, isBounce = false, innerW, innerH, moveX, moveY, mX, mY; // 按下 pendant.addEventListener('touchstart',function (event) { if (isBounce) { return false; } posX = parseInt(pendant.offsetLeft); posY = parseInt(pendant.offsetTop); var _touchs = event.touches[0], ofX = _touchs.clientX, ofY = _touchs.clientY; innerW = ofX - posX; innerH = ofY - posY; }); // 移动 pendant.addEventListener('touchmove',function (event) { if (isBounce) { return false; } event.preventDefault(); var _touchs = event.touches[0]; moveX = _touchs.clientX; moveY = _touchs.clientY; // mX = moveX - innerW; mX = (moveX - innerW) > (screenWidth - pW) ? (screenWidth - pW) : ((moveX - innerW) > 0 ? (moveX - innerW) : 0); mY = (moveY - innerH) > (screenHeight - pH) ? (screenHeight - pH) : ((moveY - innerH) > 0 ? (moveY - innerH) : 0); pendant.style.left = mX + 'px'; pendant.style.top = mY + 'px'; }); // 松开 document.addEventListener('touchend',function (event) { if (isBounce) { return false; } isBounce = true; posX = parseInt(pendant.offsetLeft); if (posX + (pW/2) >= screenWidth/2) { // 靠右 isL = false; setTimeout(function () { bounce(); },200); }else{ // 靠左 isL = true; setTimeout(function () { bounce(); },200); } }); var t,b,c,d,isL = false; // 弹性动画 function bounce() { t = 0; b = posX; c = (isL ? 0 : screenWidth - pW) - posX; d = 50; function sport () { t+=1; if (t == d) { clearInterval(timer); isBounce = false; } pendant.style.left = Tween.bounceOut(t,b,c,d) + 'px'; } var timer = setInterval(sport,10); } }
这样是有了小球吸附反弹的效果,但是快要开始上马项目的时候,产品给我说希望可以点击小球,侧边划出一个菜单栏,想法很好,我也只能继续往下写着.
不过很快,发现,这其中有一个很尴尬的问题,我原先的效果是通过touch事件来实现的,但是,如果再给小球添加click事件,两者会有一定的冲突,它们实现的过程会是touchstart->touchmove->touchend->click,这样一个过程,拖拽和小球的吸附反弹效果,我都是用的left属性,通过设置定时器,来实现的,而侧滑出菜单栏,我则决定使用css3的过渡动画效果,一是因为css的动画更加流畅,二也是写出来更简单(这在之后让我因为当时天真的想法痛不欲生).
决定好之后,首先就要先解决touch和click有些冲突的问题,我的解决办法,就是通过判断touchmove的移动距离,在touchmove的函数中做一个判断,如果移动超过一定的距离,则禁止click事件的函数,如果没有,则禁止touchend函数中的小球吸附反弹函数,如此一来,就可以解决问题,不过,随即又发现了第二个bug,因为我给菜单栏设置的是侧边滑出效果是用过渡动画所做,会对小球移动所设置的left属性有影响,然后就重新设置了动画属性transition-property: right,但问题还是接连不断,期间尝试了很多办法(其中差点放弃,决定使用left通过tween.js来做动画效果),最后发现,因为先前给两个元素都设置了left,而之后的滑出菜单栏又设置了right了,就会有问题,就在滑出菜单栏的动画中,添加上了right:auto,最后,又给将每个动画设置了动画时长,当需要拖动小球和执行吸附反弹时,设置pendant.style.webkitTransitionDuration = '0s',当需要过渡动画的时候,再设置回pendant.style.webkitTransitionDuration = '0.6s';问题完美解决.
麻蛋,不说了,说的我自己都要迷了,上代码...
<div class="i-pendant" id="pendant"></div> <ul class="menu"> <li><a href="###">一</a></li> <li><a href="###">二</a></li> <li><a href="###">三</a></li> </ul>
body{ height: 2000px; } .i-pendant { width: 3rem; height: 3rem; border-radius: 50%; background: #f00; position: fixed; top: 300px; right: 0; z-index: 90; transition-property: right; transition-timing-function: ease-in-out; } .menu li{ float: left; font-size: 1.2rem; width: 3rem; height: 3rem; text-align: center; line-height: 3rem; color: #fff; background-color: rgba(0,0,0,0.6); } .menu{ width: 9rem; position: fixed; right: -9rem; top: 300px; transition-property: right; transition-timing-function: ease-in-out; }
// tween函数 var Tween = { bounceIn: function(t, b, c, d){ //弹球减振(弹球渐出) return c - Tween['bounceOut'](d-t, 0, c, d) + b; }, bounceOut: function(t, b, c, d){ if ((t/=d) < (1/2.75)) { return c*(7.5625*t*t) + b; } else if (t < (2/2.75)) { return c*(7.5625*(t-=(1.5/2.75))*t + 0.75) + b; } else if (t < (2.5/2.75)) { return c*(7.5625*(t-=(2.25/2.75))*t + 0.9375) + b; } return c*(7.5625*(t-=(2.625/2.75))*t + 0.984375) + b; }, bounceBoth: function(t, b, c, d){ if (t < d/2) { return Tween['bounceIn'](t*2, 0, c, d) * 0.5 + b; } return Tween['bounceOut'](t*2-d, 0, c, d) * 0.5 + c*0.5 + b; } } var pendant = document.getElementById(obj0) || document.getElementsByClassName(obj0)[0], menu = document.getElementById(obj1) || document.getElementsByClassName(obj1)[0], posX = parseInt(pendant.offsetLeft), //初试位置 posY = parseInt(pendant.offsetTop), pW = pendant.clientWidth, //小球自身宽度 pH = pendant.clientHeight, //小球自身高度 screenWidth = document.documentElement.clientWidth, //设备宽度 screenHeight = document.documentElement.clientHeight, //设备高度 mW = menu.clientWidth, //菜单栏自身宽度 isBounce = false, //记录是否正在执行小球吸附弹性动画 ofX, //touchstart的x位置 ofY, //touchstart的y位置 innerW, //点击位置距离元素边缘位置的距离 innerH, moveX, //touchmove的x位置 moveY, //touchmove的y位置 mX, //小球移动阶段位置 mY, isTrans = false, //记录是否已经打开了菜单 isTransAni = false, //记录是否正在执行呼出菜单动画 isWeiYi = false, //用来区分touch事件和click事件,通过位移量来判断是否执行touchend之中的bounce函数 isL = false; //记录小球是否在屏幕的左边 // 按下 pendant.addEventListener('touchstart',function (event) { isWeiYi = false; if (isBounce) { return false; } posX = parseInt(pendant.offsetLeft); posY = parseInt(pendant.offsetTop); var _touchs = event.touches[0]; ofX = _touchs.clientX; ofY = _touchs.clientY; innerW = ofX - posX; innerH = ofY - posY; }); // 移动 pendant.addEventListener('touchmove',function (event) { if (isBounce || isTrans || isTransAni) { return false; } event.preventDefault(); var _touchs = event.touches[0]; moveX = _touchs.clientX; moveY = _touchs.clientY; mX = (moveX - innerW) > (screenWidth - pW) ? (screenWidth - pW) : ((moveX - innerW) > 0 ? (moveX - innerW) : 0); mY = (moveY - innerH) > (screenHeight - pH) ? (screenHeight - pH) : ((moveY - innerH) > 0 ? (moveY - innerH) : 0); // 判断是用touch事件触发还是click事件触发 if (Math.abs(mX - ofX) >= 5 || Math.abs(mY - ofY) >= 5) { isWeiYi = true; } pendant.style.left = mX + 'px'; pendant.style.top = mY + 'px'; }); // 松开 document.addEventListener('touchend',function (event) { if (isBounce || isTrans || isTransAni || !isWeiYi) { return false; } isBounce = true; posX = parseInt(pendant.offsetLeft); if (posX + (pW/2) >= screenWidth/2) { // 靠右 isL = false; setTimeout(function () { bounce(); },200); }else{ // 靠左 isL = true; setTimeout(function () { bounce(); },200); } }); var t,b,c,d; // 弹性动画 function bounce() { t = 0; b = posX; c = (isL ? 0 : screenWidth - pW) - posX; d = 50; function sport () { t+=1; if (t == d) { clearInterval(timer); isBounce = false; // 变换菜单栏位置 pendant.style.webkitTransitionDuration = '0s'; menu.style.webkitTransitionDuration = '0s'; pendant.style.right = isL ? (screenWidth - pW + 'px') : '0px'; menu.style.right = isL ? (screenWidth + 'px') : (-mW + 'px'); // 菜单栏顶部位置 menu.style.top = pendant.offsetTop + 'px'; } pendant.style.left = Tween.bounceOut(t,b,c,d) + 'px'; } var timer = setInterval(sport,10); } // 呼出菜单 pendant.addEventListener('click',function () { if (isTransAni || isWeiYi || isBounce) { return false; }else{ isTransAni = true; } pendant.style.webkitTransitionDuration = '0.6s'; menu.style.webkitTransitionDuration = '0.6s'; if (isTrans) { //关闭菜单栏 pendant.style.right = isL ? (screenWidth - pW + 'px') : '0px'; //根据小球是在左边还是右边来判断从那个方向关闭 pendant.style.left = 'auto'; //靠,left和right会互相影响的,必加!!! menu.style.left = 'auto'; menu.style.right = isL ? (screenWidth + 'px') : (-mW + 'px'); } else{ //打开菜单栏 pendant.style.left = 'auto'; menu.style.left = 'auto'; pendant.style.right = isL ? (screenWidth - pW - mW + 'px') : (mW + 'px'); menu.style.right = isL ? (screenWidth - mW + 'px') : '0px'; } isTrans = !isTrans; setTimeout(function () { isTransAni = false; },600); }); } ball_animate('pendant','menu');
看效果:
完成,唯一遗憾的就是没有添加上小球拖动惯性效果,之后会继续更新小球拖动之后的惯性效果,待续...
(续)
阿西吧,本来说好要加上小球拖动的惯性效果,但是做出来的效果都不理想,加上产品催的紧,前段时间把它完善了一下,就给直接上线了...尴尬
这次完善主要是加上了吸附到边缘之后紧贴边缘和记录住位置的效果.
第一个效果很简单,稍微一研究就能明白,无非是再封装一个方法,第二个就更简单了,仅仅是把小球吸附的位置坐标在localstorage中存储下来,再给它设置一个初试位置,每次进入页面都先获取一下localstroage中是否存储有小球先前的位置,如果有的话就调用之前的位置数据,没有就是用户第一次打开,以前从未打开过页面,就调用默认位置,简单又贴心,暖男程序员的贴心照顾
没错,这就是笔者本人,被你们看到了...
闲言碎语不要讲,上代码
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /> <link rel="stylesheet" type="text/css" href="css/global.css"/> <style type="text/css"> body{ height: 2000px; } .i-pendant { width: 3rem; height: 3rem; /*border-radius: 50%;*/ background: #f00; position: fixed; /*top: 300px;*/ /*right: 0;*/ z-index: 90; transition-property: right; transition-timing-function: ease-in-out; text-align: center; line-height: 3rem; background-color: #959595; overflow: hidden; } .i-pendant img{ /*width: 2.8rem; height: 2.8rem; margin: 0.1rem 0.1rem;*/ width: 120%; margin-left: -0.3rem; margin-top: -0.3rem; } .menu li{ float: left; font-size: 1.2rem; width: 3rem; height: 3rem; text-align: center; line-height: 3rem; color: #fff; background-color: #959595; border-radius: 5px; margin-left: 0.4rem; } .menu li img{ width: 1.6rem; height: 1.6rem; margin: 0.7rem 0.7rem; } .menu{ width: 10.6rem; padding-right: 0.4rem; box-sizing: border-box; position: fixed; /*right: -9rem;*/ /*top: 300px;*/ transition-property: right; transition-timing-function: ease-in-out; } </style> </head> <body> <div class="i-pendant" id="pendant"><img src="img/feiji.gif"/></div> <ul class="menu"> <li><a href="###"><img src="img/gozhuye.png"/></a></li> <li><a href="###"><img src="img/weixin.png"/></a></li> <li><a href="###"><img src="img/gouwuche.png"/></a></li> </ul> </body> <script type="text/javascript"> function ball_animate (obj0,obj1) { // tween函数 var Tween = { bounceIn: function(t, b, c, d){ //弹球减振(弹球渐出) return c - Tween['bounceOut'](d-t, 0, c, d) + b; }, bounceOut: function(t, b, c, d){ if ((t/=d) < (1/2.75)) { return c*(7.5625*t*t) + b; } else if (t < (2/2.75)) { return c*(7.5625*(t-=(1.5/2.75))*t + 0.75) + b; } else if (t < (2.5/2.75)) { return c*(7.5625*(t-=(2.25/2.75))*t + 0.9375) + b; } return c*(7.5625*(t-=(2.625/2.75))*t + 0.984375) + b; } } var pendant = document.getElementById(obj0) || document.getElementsByClassName(obj0)[0], menu = document.getElementById(obj1) || document.getElementsByClassName(obj1)[0], posX = parseInt(pendant.offsetLeft), //初试位置 posY = parseInt(pendant.offsetTop), pW = pendant.clientWidth, //小球自身宽度 pH = pendant.clientHeight, //小球自身高度 screenWidth = document.documentElement.clientWidth, //设备宽度 screenHeight = document.documentElement.clientHeight, //设备高度 mW = menu.clientWidth, //菜单栏自身宽度 isBounce = false, //记录是否正在执行小球吸附弹性动画 ofX, //touchstart的x位置 ofY, //touchstart的y位置 innerW, //点击位置距离元素边缘位置的距离 innerH, moveX, //touchmove的x位置 moveY, //touchmove的y位置 mX, //小球移动阶段位置 mY, isTrans = false, //记录是否已经打开了菜单 isTransAni = false, //记录是否正在执行呼出菜单动画 isWeiYi = false, //用来区分touch事件和click事件,通过位移量来判断是否执行touchend之中的bounce函数 isL = localStorage.getItem('startX') ? (localStorage.getItem('startX') == 0 ? false : true) : false, //记录小球是否在屏幕的左边 startX, //记录小球初始位置 startY; menu.style.webkitTransitionDuration = '0s'; get_storage(); //查看是否有记录 console.log(isL,startX,startY); //根据是否有记录来对小球和菜单栏设置初始位置 pendant.style.right = startX + 'px'; pendant.style.top = startY + 'px'; menu.style.top = startY + 'px'; menu.style.right = isL ? screenWidth + 'px' : -mW + 'px'; borderRad(); // 根据是否是在左边来判断border-raduis的方向 function borderRad () { if (isWeiYi || isBounce) { pendant.style.borderRadius = '50%'; return false; } console.log('判断'); pendant.style.borderRadius = '0'; pendant.style.borderTopLeftRadius = 'none'; pendant.style.borderBottomLeftRadius = 'none'; pendant.style.borderTopRightRadius = 'none'; pendant.style.borderBottomRightRadius = 'none'; if (isL) { //在左边 pendant.style.borderTopRightRadius = '50%'; pendant.style.borderBottomRightRadius = '50%'; }else{ //在右边 pendant.style.borderTopLeftRadius = '50%'; pendant.style.borderBottomLeftRadius = '50%'; } } // 按下 pendant.addEventListener('touchstart',function (event) { isWeiYi = false; if (isBounce) { return false; } posX = parseInt(pendant.offsetLeft); posY = parseInt(pendant.offsetTop); var _touchs = event.touches[0]; ofX = _touchs.clientX; ofY = _touchs.clientY; innerW = ofX - posX; innerH = ofY - posY; }); // 移动 pendant.addEventListener('touchmove',function (event) { if (isBounce || isTrans || isTransAni) { return false; } event.preventDefault(); var _touchs = event.touches[0]; moveX = _touchs.clientX; moveY = _touchs.clientY; mX = (moveX - innerW) > (screenWidth - pW) ? (screenWidth - pW) : ((moveX - innerW) > 0 ? (moveX - innerW) : 0); mY = (moveY - innerH) > (screenHeight - pH) ? (screenHeight - pH) : ((moveY - innerH) > 0 ? (moveY - innerH) : 0); // 判断是用touch事件触发还是click事件触发 if (Math.abs(mX - ofX) >= 5 || Math.abs(mY - ofY) >= 5) { isWeiYi = true; } borderRad(); //判断小球是否需要变化 pendant.style.left = mX + 'px'; pendant.style.top = mY + 'px'; }); // 松开 document.addEventListener('touchend',function (event) { if (isBounce || isTrans || isTransAni || !isWeiYi) { return false; } isWeiYi = false; isBounce = true; posX = parseInt(pendant.offsetLeft); if (posX + (pW/2) >= screenWidth/2) { // 靠右 isL = false; set_storage(); //记录位置 setTimeout(function () { bounce(); },200); }else{ // 靠左 isL = true; set_storage(); //记录位置 setTimeout(function () { bounce(); },200); } }); var t,b,c,d; // 弹性动画 function bounce() { t = 0; b = posX; c = (isL ? 0 : screenWidth - pW) - posX; d = 50; function sport () { t+=1; if (t == d) { clearInterval(timer); isBounce = false; // 变换菜单栏位置 pendant.style.webkitTransitionDuration = '0s'; menu.style.webkitTransitionDuration = '0s'; pendant.style.right = isL ? (screenWidth - pW + 'px') : '0px'; menu.style.right = isL ? (screenWidth + 'px') : (-mW + 'px'); // 菜单栏顶部位置 menu.style.top = pendant.offsetTop + 'px'; } pendant.style.left = Tween.bounceOut(t,b,c,d) + 'px'; } setTimeout(function () { borderRad(); },900); var timer = setInterval(sport,10); } // 呼出菜单 pendant.addEventListener('click',function () { if (isTransAni || isWeiYi || isBounce) { return false; }else{ isTransAni = true; } //判断方向 borderRad(); pendant.style.webkitTransitionDuration = '0.6s'; menu.style.webkitTransitionDuration = '0.6s'; if (isTrans) { //关闭菜单栏 pendant.style.right = isL ? (screenWidth - pW + 'px') : '0px'; //根据小球是在左边还是右边来判断从那个方向关闭 pendant.style.left = 'auto'; //靠,left和right会互相影响的,必加!!! menu.style.left = 'auto'; menu.style.right = isL ? (screenWidth + 'px') : (-mW + 'px'); } else{ //打开菜单栏 pendant.style.left = 'auto'; menu.style.left = 'auto'; pendant.style.right = isL ? (screenWidth - pW - mW + 'px') : (mW + 'px'); menu.style.right = isL ? (screenWidth - mW + 'px') : '0px'; } isTrans = !isTrans; setTimeout(function () { isTransAni = false; },600); }); // 记录位置 function set_storage () { if(window.localStorage){ localStorage.setItem('startX',isL ? screenWidth - pW : 0); localStorage.setItem('startY',mY); }else{ return false; } } // 获取位置 function get_storage () { if(window.localStorage){ startX = localStorage.getItem("startX") ? localStorage.getItem("startX") : 0; startY = localStorage.getItem('startY') ? localStorage.getItem('startY') : 350; }else{ return false; } } } ball_animate('pendant','menu'); </script> </html>
这次直接把所有的demo代码一起贴出来,希望有所帮助.
看眼效果
还不错,心里美滋滋.
最后扯一句,由于此功能是放在一个植入到app内的页面中,跟app原有的右滑退出功能有些冲突,会造成用户的误操作,在产品的威压下,最后只得让小球全部吸附在右边,欲哭无泪...