material-refresh
material-refresh copied to clipboard
Unexpected behavior when pulling, fixed code inside
The way the pull down worked was a bit too particular. I relaxed the requirements a bit and I also changed the movement animation to use transform: translate instead of .animate. It should feel more fluid to the user.
I modified it quickly for my app, don't have too much time to clean it up, you may find some junk variables and console.logs in there. I mainly edited, touchStart, touchMove, touchEnd, and moveCircle.
/**
* Google Material Design Swipe To Refresh.
* By Gctang(https://github.com/lightningtgc)
*
* Three types of refresh:
* 1. Above or coplanar with another surface
* 2. Below another surface in z-space.
* 3. Button action refresh
*
*/
;(function($){
var $scrollEl = $(document.body);
var $refreshMain, $spinnerWrapper, $arrowWrapper, $arrowMain;
var scrollEl = document.body;
var noShowClass = 'mui-refresh-noshow';
var mainAnimatClass = 'mui-refresh-main-animat';
var blueThemeClass = 'mui-blue-theme';
var isShowLoading = false;
var isStoping = false;
var isBtnAction = false;
var NUM_POS_START_Y = -85;
var NUM_POS_TARGET_Y = 0; // Where to stop
var NUM_POS_MAX_Y = 120; // Max position for the moving distance
var NUM_POS_MIN_Y = -25; // Min position for the moving distance
var NUM_NAV_TARGET_ADDY = 20; // For custom nav bar
var touchCurrentY;
var touchStartY = 0;
var customNavTop = 0;
var verticalThreshold = 15;
var maxRotateTime = 6000; //Max time to stop rotate
var basePosY = 60;
var onBegin = null;
var onBtnBegin= null;
var onEnd = null;
var onBtnEnd = null;
var stopAnimatTimeout = null;
var refreshNav = '';
var lastTime = new Date().getTime();
var isIOS = !!navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/);
var tmpl = '<div id="muiRefresh" class="mui-refresh-main">\
<div class="mui-refresh-wrapper ">\
<div class="mui-arrow-wrapper">\
<div class="mui-arrow-main"></div>\
</div>\
<div class="mui-spinner-wrapper" style="display:none;">\
<div class="mui-spinner-main" >\
<div class="mui-spinner-left">\
<div class="mui-half-circle"></div>\
</div>\
<div class="mui-spinner-right">\
<div class="mui-half-circle"></div>\
</div>\
</div>\
</div>\
</div>\
</div>';
// Defined the object to improve performance
var touchPos = {
top: 0,
x1: 0,
x2: 0,
y1: 0,
y2: 0
}
// Default options
/* var opts = { */
/* scrollEl: '', //String */
/* nav: '', //String */
/* top: '0px', //String */
/* theme: '', //String */
/* index: 10001, //Number*/
/* maxTime: 3000, //Number */
/* freeze: false, //Boolen */
/* onBegin: null, //Function */
/* onEnd: null //Function */
/* } */
/* Known issue:
* 1. iOS feature when scrolling ,animation will stop
* 2. Animation display issue in anfroid like miui小米
*
*
* TODO list:
* 1. Using translate and scale together to replace top
* 2. Optimize circle rotate animation
*/
// Main function to init the refresh style
function mRefresh(options) {
options = options || {};
scrollEl = options.scrollEl ? options.scrollEl :
isIOS ? scrollEl : document;
$scrollEl = $(scrollEl);
// extend options
onBegin = options.onBegin;
onEnd = options.onEnd;
maxRotateTime = options.maxTime || maxRotateTime;
refreshNav = options.nav || refreshNav;
if ($('#muirefresh').length === 0) {
renderTmpl();
}
$refreshMain = $('#muiRefresh');
$spinnerWrapper = $('.mui-spinner-wrapper', $refreshMain);
$arrowWrapper = $('.mui-arrow-wrapper', $refreshMain);
$arrowMain = $('.mui-arrow-main', $refreshMain);
// Custom nav bar
if (!isDefaultType()) {
$refreshMain.addClass('mui-refresh-nav');
basePosY = $(refreshNav).height() + 20;
if($(refreshNav).offset()){
customNavTop = $(refreshNav).offset().top;
// Handle position fix
if($(refreshNav).css('position') !== 'fixed'){
basePosY += customNavTop;
}
// Set the first Y position
$refreshMain.css('-webkit-transform', 'translate3d(0px,'+customNavTop+'px,0px)');
// $refreshMain.css('top', customNavTop + 'px');
}
//Set z-index to make sure ablow the nav bar
var navIndex = $(refreshNav).css('z-index');
$refreshMain.css('z-index', navIndex - 1);
}
//Set custom z-index
if(options.index){
$refreshMain.css('z-index', ~~options.index);
}
//Set custom top, to change the position
if(options.top){
$refreshMain.css('-webkit-transform', 'translate3d(0px,'+options.top+'px,0px)');
//$refreshMain.css('top', options.top);
}
// Extract theme
if (options.theme) {
$refreshMain.addClass(options.theme);
} else {
$refreshMain.addClass(blueThemeClass);
}
// Add Animation Class
$refreshMain.addClass(mainAnimatClass);
if(!options.freeze){
bindEvents();
}
}
// Public Methods
// Finish loading
mRefresh.resolve = function() {
if(!isStoping && stopAnimatTimeout){
clearTimeout(stopAnimatTimeout);
stopAnimatTimeout = null;
recoverRefresh();
}
}
// Destory refresh
mRefresh.destroy = function(){
unbindEvents();
$refreshMain.remove();
}
// Type3: Button action refresh
mRefresh.refresh = function(opt) {
// Do rotate
if(!isShowLoading){
var realTargetPos = basePosY + NUM_POS_TARGET_Y - 20;
isShowLoading = true;
isBtnAction = true;
opt = opt || {};
onBtnBegin = opt.onBegin;
onBtnEnd = opt.onEnd;
if (!isDefaultType()) {
realTargetPos = realTargetPos + NUM_NAV_TARGET_ADDY;
}
// Handle freeze
$refreshMain.show();
//Romove animat time
$refreshMain.removeClass(mainAnimatClass);
// move to target position
$refreshMain.css('-webkit-transform', 'translate3d(0px,'+realTargetPos+'px,0px)');
// $refreshMain.css('top', realTargetPos + 'px');
// make it small
$refreshMain.css('-webkit-transform', 'scale(' + 0.01 + ')');
setTimeout(doRotate, 60);
}
}
// Unbind touch events,for freeze type1 and type2
mRefresh.unbindEvents = function(){
unbindEvents();
}
mRefresh.bindEvents = function(){
bindEvents();
}
// Render html template
function renderTmpl(){
document.body.insertAdjacentHTML('beforeend', tmpl);
}
function touchStart(e){
if(isIOS && scrollEl == document.body){
touchPos.top = window.scrollY;
}else if(scrollEl != document){
scollPos = $(scrollEl).scrollTop();
} else {
touchPos.top = (document.documentElement || document.body.parentNode || document.body).scrollTop;
}
if (touchPos.top > 0 || isShowLoading) {
return;
}
//touchCurrentY = basePosY + NUM_POS_START_Y;
hasStarted = false;
// Fix jQuery touch event detect
e = e.originalEvent || e;
if (e.touches[0]) {
//touchPos.x1 = e.touches[0].pageX;
//touchStartY = touchPos.y1 = e.touches[0].pageY;
//touchCurrentY = e.touches[0].pageY;
//previousY = currentY;
}
}
var hasStarted = false;
var startY = 0;
var previousY = 0;
var currentY = 0;
var movePct = .25;
var scollPos = 0;
var distanceY = 0;
function touchMove(e){
var thisTouch;//, distanceY;
var now = new Date().getTime();
e = e.originalEvent || e;
scrollPos = $(scrollEl).scrollTop();
var maxHeight = $(scrollEl).height();
if ( isShowLoading || !e.touches || e.touches.length !== 1) {
// Just allow one finger
return;
}
thisTouch = e.touches[0];
touchCurrentY = thisTouch.pageY;
distanceY = (touchCurrentY - touchStartY);
console.log("Start="+touchStartY+", Current="+touchCurrentY+", Dist="+distanceY);
if( !hasStarted && scrollPos < (verticalThreshold/2) ) {
//if( distanceY > verticalThreshold ) {
if( !hasStarted ) {
hasStarted = true;
touchStartY = touchCurrentY;
}
touchStartY = touchCurrentY;
$refreshMain.show();
distanceY = 0;
console.log("HIT THRESHOLD");
//}
}
if( hasStarted && distanceY > verticalThreshold) {
e.preventDefault();
// Some android phone
// Throttle, aviod jitter
if (now - lastTime < 90) {
return;
}
console.log("MOVING CIRCLE, DISTANCE = " + distanceY);
moveCircle(distanceY);
}
/*
touchPos.x2 = thisTouch.pageX;
touchPos.y2 = thisTouch.pageY;
// Distance for pageY change
distanceY = touchPos.y2 - touchPos.y1;
if (touchPos.y2 - touchStartY + verticalThreshold > 0) {
e.preventDefault();
// Some android phone
// Throttle, aviod jitter
if (now - lastTime < 90) {
return;
}
if (touchCurrentY < basePosY - customNavTop + NUM_POS_MAX_Y) {
touchCurrentY += distanceY ;
moveCircle(touchCurrentY);
} else {
// Move over the max position will do the rotate
doRotate();
return;
}
}
*/
// y1 always is the current pageY
touchPos.y1 = thisTouch.pageY;
lastTime = now;
}
function touchEnd(e){
hasStarted = false;
if (scrollPos > 0 || isShowLoading) {
return;
}
e.preventDefault();
if (distanceY >= (maxDistance - verticalThreshold)) {
// Should move over the min position
doRotate();
} else {
backToStart();
}
}
/**
* backToStart
* Return to start position
*/
function backToStart() {
var realStartPos = basePosY + NUM_POS_START_Y;
if ( isDefaultType() ) {
$refreshMain.css('-webkit-transform', 'translate3d(0px,'+realStartPos+'px,0px)');
// $refreshMain.css('top', realStartPos + 'px');
$refreshMain.css('-webkit-transform', 'scale(' + 0 + ')');
} else {
// Distance must greater than NUM_POS_MIN_Y
$refreshMain.css('-webkit-transform', 'translate3d(0px,'+customNavTop+'px,0px)');
//$refreshMain.css('top', customNavTop + 'px');
/* $refreshMain.css('-webkit-transform', 'translateY(' + realStartPos + 'px)'); */
}
setTimeout(function(){
// Handle button action
if(!isShowLoading){
$refreshMain.css('opacity', 0);
$refreshMain.hide();
}
}, 300);
}
/**
* moveCircle
* touchmove change the circle style
*
* @param {number} y
*/
var maxDistance = 110;
function moveCircle(y){
if( y > maxDistance )
y = maxDistance;
var scaleRate = maxDistance/4;
var scalePer = y / scaleRate > 1 ? 1 : y / scaleRate < 0 ? 0 : y / scaleRate;
var currMoveY = basePosY + NUM_POS_START_Y + y;
if (isDefaultType()) {
// Small to Big
$refreshMain.css('-webkit-transform', 'scale(' + scalePer + ')');
}
/* $refreshMain.css('-webkit-transform', 'translateY('+ y + 'px)'); */
$refreshMain.css('opacity', scalePer);
// Change the position
$refreshMain.css('-webkit-transform', 'translate3d(0px,'+currMoveY+'px,0px)');//currMoveY + 'px');
$arrowMain.css('-webkit-transform', 'rotate(' + -(y * 3) + 'deg)');
/* $arrowMain.css('transform', 'rotate(' + -(y * 3) + 'deg)'); */
}
/**
* doRotate
* Rotate the circle,and you can stop it by `mRefresh.resolve()`
* or it wil stop within the time: `maxRotateTime`
*/
function doRotate(){
isShowLoading = true;
// Do button action callback
if (isBtnAction && typeof onBtnBegin === 'function') {
onBtnBegin();
} else if (typeof onBegin === 'function') {
// Do onBegin callback
onBegin();
}
// Make sure display entirely
$refreshMain.css('opacity', 1);
if (!isBtnAction) {
var realTargetPos = basePosY + NUM_POS_TARGET_Y - 20;
if (!isDefaultType()) {
realTargetPos = realTargetPos + NUM_NAV_TARGET_ADDY;
}
$refreshMain.css('-webkit-transform', 'translate3d(0px,'+realTargetPos+'px,0px)');
//$refreshMain.css('top', realTargetPos + 'px');
/* $refreshMain.css('-webkit-transform', 'translateY(' + realTargetPos + 'px)'); */
} else {
$refreshMain.addClass(mainAnimatClass);
$refreshMain.css('-webkit-transform', 'scale(' + 1 + ')');
}
$arrowWrapper.hide();
// Start animation
$spinnerWrapper.show();
// Timeout to stop animation
stopAnimatTimeout = setTimeout(recoverRefresh, maxRotateTime);
}
/**
* Recover Refresh
* Hide the circle
*/
function recoverRefresh(){
// For aviod resolve
isStoping = true;
// Stop animation
$refreshMain.addClass(noShowClass);
$spinnerWrapper.hide();
setTimeout(function(){
$refreshMain.removeClass(noShowClass);
$refreshMain.hide();
backToStart();
$arrowWrapper.show();
isShowLoading = false;
isStoping = false;
if (isBtnAction && typeof onBtnEnd === 'function') {
onBtnEnd();
} else if (typeof onEnd === 'function') {
onEnd();
}
isBtnAction = false;
}, 500);
}
/**
* isDefaultType
* Check is type1: Above surface
*
* @return {Boolen}
*/
function isDefaultType() {
return $(refreshNav).length === 0;
}
function bindEvents() {
$scrollEl.on('touchstart', touchStart);
$scrollEl.on('touchmove', touchMove);
$scrollEl.on('touchend', touchEnd);
}
function unbindEvents() {
$scrollEl.off('touchstart', touchStart);
$scrollEl.off('touchmove', touchMove);
$scrollEl.off('touchend', touchEnd);
}
window.mRefresh = mRefresh;
})(window.Zepto || window.jQuery);
I am having weird behavior too, but even when i used the code you provided, its still behaving weirdly... i am wondering if i supposed to assign the scrollEL to a div that is scrollable?? Because i have anchor tag that is supposed to be clickable, but now the touchstart/touchend event is being detected, but the anchor tag "click" isnt getting triggered.
Cool, it's quite better performance with this transform version than only with positioning, but yes, the arroy spin still extrange. My fix is a CSS fix, just add a transition to the arrow. This can be done in the selector .mui-arrow-main
that is in line 44 of material-refresh.css
by adding:
-webkit-transition: transform 0.2s;
-moz-transition: transform 0.2s;
-o-transition: transform 0.2s;
transition: transform 0.2s;
So, the full selector ends like this:
.mui-half-circle,
.mui-arrow-main {
position: absolute;
top: 0;
width: 25px;
height: 25px;
box-sizing: border-box;
border-width: 3px;
border-style: solid;
border-color: #000 #000 transparent;
border-radius: 999px;
-webkit-transition: transform 0.2s;
-moz-transition: transform 0.2s;
-o-transition: transform 0.2s;
transition: transform 0.2s;
}
Hope to help ;)
P.D. Yes, it also affects to .mui-half-circle
, but I haven't detected any extrange behaviour. If you are so paranoid about it, you can just add a new style rule between line 55 and 56, exactly this one:
.mui-arrow-main {
-webkit-transition: transform 0.2s;
-moz-transition: transform 0.2s;
-o-transition: transform 0.2s;
transition: transform 0.2s;
}
Happy coding!