lightningtgc / material-refresh

Google Material Design swipe(pull) to refresh by using JavaScript and CSS3.
http://lightningtgc.github.io/material-refresh/
648 stars 110 forks source link

Unexpected behavior when pulling, fixed code inside #12

Open joetex opened 8 years ago

joetex commented 8 years ago

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);
clarklight commented 8 years ago

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.

CodingCarlos commented 7 years ago

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!