Fistright Fistright - 5 months ago 30
CSS Question

imitate the android page swipe with JS

So I was wondering how to rebuild the android animation that occurs when you are swiping though the app pages. This video is an quick example of what I mean.

I have not that much experience with JavaScript or even doing animations with it. But there is one particular thing that made me wonder of how to translate it to JavaScript.

So how could you select the pages that have to be animated as soon as the previous gets moved and how to manage that the animation is more fluent than one that relies on mouse move ?

I did not wrote any code it is just a thought of myself of how to implement this animation.

Answer

Ok Fistright, this is my solution. Yeah I though we might solve this together but you wrote that your problem is urgent so I did it myself.

HERE you can see the working example.

The page was tested with Google Chrome 51, Mozilla Firefox 47 and Internet Explorer 11.

The whole code:

<!DOCTYPE html>
<html>
    <head>
        <title>Android Swipe for Fistright</title>
        <meta charset="UTF-8">
        <script type="text/javascript" src="./swipe.js"></script>
        <link rel="stylesheet" type="text/css" href="swipe.css">
    </head>
    <body>
        <div id="swipe-container">
            <div class="page-container" style="background-color:#7A48C1;">page one</div>
            <div class="page-container" style="background-color:#BA99E7;">page two</div>
            <div class="page-container" style="background-color:#966BD4;">page thre</div>
            <div class="page-container" style="background-color:#622BB0;">page four</div>
            <div class="page-container" style="background-color:#481295;">page five</div>
            <div class="page-container" style="background-color:#481295;">page seven</div>
            <div class="page-container" style="background-color:#481295;">page eight</div>
            <div class="page-container" style="background-color:#481295;">page nine</div>
            <div class="page-container" style="background-color:#481295;">page ten</div>
            <div class="page-container" style="background-color:#481295;">page eleven</div>
            <div class="page-container" style="background-color:#481295;">page twelve</div>
        </div>
        <script type="text/javascript">
            var s = new swipe(document.getElementById('swipe-container'));
        </script>
    </body>
</html>

This is just an example, the only thing you need to keep is:

<!DOCTYPE html>
<html>
    <head>
        <title>Android Swipe for Fistright</title>
        <meta charset="UTF-8">
        <script type="text/javascript" src="./swipe.js"></script>
        <link rel="stylesheet" type="text/css" href="swipe.css">
    </head>
    <body>
        <div id="blabla">
            <div class="page-container"></div>
            <div class="page-container"></div>
            <div class="page-container"></div>
            <div class="page-container"></div>
            <div class="page-container"></div>
            <div class="page-container"></div>
            <div class="page-container"></div>
            <div class="page-container"></div>
            <div class="page-container"></div>
            <div class="page-container"></div>
            <div class="page-container"></div>
            <div class="page-container"></div>
        </div>
        <script type="text/javascript">
            var s = new swipe(document.getElementById('blabla'));
        </script>
    </body>
</html>

The css is rather simple

body,html{
    margin: 0px;
    padding: 0px;
    border: 0px;
}
#swipe-container, .page-container{
    margin: 0px;
    padding: 0px;
    border: 0px;
}

#swipe-container, .page-container{
    width: 450px;
    height: 640px;
}
.page-container{
    position: absolute;
    left:0px;
    opacity: 0;
    display:none;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none; 
}
#swipe-container{
    position:relative;
    cursor: pointer;
    margin:0px auto;
    margin-top: 60px;
    overflow: hidden;
    border:1px grey solid;
}

And the shortened version:

#swipe-container, .page-container{
    width: 450px;
    height: 640px;
}
#swipe-container{
    position:relative;
    overflow: hidden;
}

And now the JavaScript:

// swipe.js
// contains a class that gives the interactivity for the swiping
//
// written by Florian Völker (feirell.de) 11.06.2016


function swipe(container){
    if(typeof container == 'undefined'){
        this.conta = document.getElementById('swipe-container')
    }else{
        this.conta = container
    }

    this.pages = this.conta.getElementsByClassName('page-container')

    /*prepare all pages*/
    for (var i = this.pages.length - 1; i >= 0; i--) {
        this.resetElement(this.pages[i])
        this.pages[i].style.zIndex    = this.pages.length - 1 - i
        this.pages[i].style.position  = "absolute";
    }

    this.width = parseInt(window.getComputedStyle(this.conta,null).getPropertyValue("width").replace('px',''));

    this.conta.addEventListener('mouseenter',this.M_enter.bind(this))
    this.conta.addEventListener('mouseleave',this.M_out.bind(this))
    this.conta.addEventListener('mousemove', this.M_move.bind(this))

    this.conta.addEventListener('mousedown', this.M_down.bind(this))
    this.conta.addEventListener('mouseup', this.M_up.bind(this))

    this.start_position = false
    this.start_swipe = false
    this.mousedown = false
    this.inside = false
    this.virtualPosition = this.width/2
    this.lastRenderPosition = 0

    this.magnet_in_progress = false;
    this.magnet_destination = 0
    this.magnet_last_call = 0
    this.magnet_start = 0
    this.magnet_time = 100

    requestAnimationFrame(this.animationLoop.bind(this))
}

swipe.prototype.M_enter = function(){
    this.inside = true
};
swipe.prototype.M_out = function(ev){
    this.inside = false
    this.M_up()
};
swipe.prototype.M_up = function(ev){
    this.mousedown = false
    this.start_swipe = false
    this.start_position = false
    this.magnet()
};
swipe.prototype.M_down = function(ev){
    this.clear_magnet()
    this.start_position = ev.clientX;
    this.start_swipe = this.virtualPosition
    this.mousedown = true
};

swipe.prototype.M_move = function(ev){
    if(this.mousedown == true){
        shift = ev.clientX - this.start_position
        new_position = this.start_swipe - shift
        if (new_position <= this.width*0.5){
            this.virtualPosition = this.width*0.5
            return;
        }
        if(new_position >= (this.pages.length-0.5)*this.width ){
            this.virtualPosition = (this.pages.length-0.5)*this.width
            return;
        }
        this.virtualPosition = new_position;
    }
};

swipe.prototype.magnet = function(){
    this.clear_magnet()
    this.magnet_in_progress = true
    this.magnet_start = this.virtualPosition;
    this.magnet_destination = Math.round(((this.virtualPosition/(this.width*0.5))+1)/2)*this.width - this.width*0.5
    this.magnet_last_call = Date.now()
    //console.log('start: ',this.magnet_start,'\ndestination: ',this.magnet_destination)
};

swipe.prototype.clear_magnet = function(){
    this.magnet_in_progress = false;
    this.magnet_start = 0
    this.magnet_destination = 0
    this.magnet_last_call = 0
};
swipe.prototype.magnet_position = function(){
    var now = Date.now(),
        diff =  now - this.magnet_last_call,
        pixel = this.magnet_destination - this.magnet_start;

    this.magnet_last_call = now

    var set_to = this.virtualPosition + ((diff/this.magnet_time) * pixel)

    if(pixel > 0 && set_to > this.magnet_destination){
        var dest = this.magnet_destination
        this.clear_magnet()
        return dest
    }
    if(pixel < 0 && set_to < this.magnet_destination){
        var dest = this.magnet_destination
        this.clear_magnet()
        return dest
    }
    return set_to   
}
swipe.prototype.displayAs = function(position){
    if (position >= this.width*0.5 && position <= (this.pages.length-0.5)*this.width ) {
        var el_index = ((position/(this.width*0.5))+1)/2 -1
        if(el_index % 1 == 0){
            if(el_index-1 >= 0){
                this.resetElement(this.pages[el_index-1])
            }

            this.pages[el_index].swipe_default  = false
            this.pages[el_index].style.opacity  = "1"
            this.pages[el_index].style.left     = "0px"
            this.pages[el_index].style.display  = "block"
            this.pages[el_index].style.transform = "scale(1)"

            if(el_index+1 <= this.pages.length-1){
                this.resetElement(this.pages[el_index+1])
            }
        }else{
            var distance = (position+(this.width/2)) % this.width,
                a = Math.floor(el_index),
                b = Math.ceil(el_index),
                per = distance/this.width

            this.pages[a].swipe_default = false
            this.pages[a].style.left    = "-"+distance+"px"
            this.pages[a].style.display = 'block';
            this.pages[a].style.opacity = '1';
            this.pages[a].style.transform  = "scale(1)";

            this.pages[b].swipe_default = false
            this.pages[b].style.display = 'block';
            this.pages[b].style.left    = '0px'
            this.pages[b].style.opacity = per
            this.pages[b].style.transform = "scale("+ (1 - ((1-per) * 0.2)) +")"

            /*resetting the a-1 and b+1 elements so they are default*/
            if(a-1 >= 0 && this.pages[a-1].swipe_default == false){
                this.resetElement(this.pages[a-1])
            }
            if(b+1 <= this.pages.length-1 && this.pages[b+1].swipe_default == false){
                this.resetElement(this.pages[b+1])
            }
        }
    }
};
swipe.prototype.resetElement = function(el){
    el.swipe_default    = true
    el.style.display    = "none";
    el.style.left       = "0px";
    el.style.opacity    = "0";
    el.style.transform  = "scale(0)";
}
swipe.prototype.animationLoop = function(){
    if(this.magnet_in_progress == true){
        this.virtualPosition = this.magnet_position()
    }
    if(this.lastRenderPosition != this.virtualPosition){
        this.displayAs(this.virtualPosition)
        this.lastRenderPosition = this.virtualPosition
    }
    requestAnimationFrame(this.animationLoop.bind(this))
};

I though about to comment the whole javascript but this would just stretch the script and I think that it is long enough, if you have any questions, or think that comments would be a good idea feel free to ask and I will try my best to help you.

EDIT:

My English is not that well if something is unclear please ask.

If you have no idea what this and prototype mean than just think of those as scopes. this means that it is variable in this object which is accessible from all swipe.prototype functions.

Ok so first of all @HovercraftFullOfEels is right, like he wrote in his comment under your question. You really should try to move your explanation from the comments to your answer and trying to be more specific about what you need to know.

The reason I am helping you with this whole solution, which is pretty rare because you did not provide your own ideas, is that you seem to be pretty new to this whole topic and I though that you have an interesting idea :)

So you should rewrite your question with something like "So I saw that interesting animation on my Android phone. Does someone have an idea how to manage this transitions ? This would be my first animation with JavaScript and I have not that much of experience." ... or something like this.

So back to the answer itself.

The basic Idea is that the mouse has to control the movement of the boxes. But to control the boxes it needs some management, which is provided by virtualPosition and lastRenderPosition. virtualPosition contains the pixel offset from the left side where the boxes have to move.lastRenderPosition is just a memory to know if this position was already rendered. The position of the boxes is just the width of the swipe-container. So you could imagine the position like this:

explanation for virtualPosition

For example I use this position in the line this.virtualPosition = this.width/2 which just says that we start with the first page, you could modify this to start with another page.

displayAs is the magic point of this idea. It transforms the divs depending on the position given.

swipe.prototype.displayAs = function(position){
    if (position >= this.width*0.5 && position <= (this.pages.length-0.5)*this.width ) {    //this checks if the position is in range of the page divs
        var el_index = ((position/(this.width*0.5))+1)/2 -1                                 //the one is just to adjust for the array 0 start, el_index gives array id of the most centered div
        if(el_index % 1 == 0){                                              //if the id is even, go for the first block (this is the case when it is stable in the center and no other div is showing)
            if(el_index-1 >= 0){                                            //hide the privous page if it exists
                this.resetElement(this.pages[el_index-1])
            }

            this.pages[el_index].swipe_default  = false                     //completely show the current page
            this.pages[el_index].style.opacity  = "1"
            this.pages[el_index].style.left     = "0px"
            this.pages[el_index].style.display  = "block"
            this.pages[el_index].style.transform = "scale(1)"

            if(el_index+1 <= this.pages.length-1){                          //hide the next page if it exists
                this.resetElement(this.pages[el_index+1])
            }
        }else{
            var distance = (position+(this.width/2)) % this.width,          //this contains the distance to the next full show page (0 -> this-width)
                a = Math.floor(el_index),                                   //two elements are showing, the first on is the left (floor el_index)
                b = Math.ceil(el_index),                                    //the second one is the right one (ceil el_index) example: el_index = 3.2; a = 3 b = 4
                per = distance/this.width                                   //distance/this.width is the PERCENT of the left page showing I used this to determinate to how much to shift the element to the left and how much to show the right one

            this.pages[a].swipe_default = false
            this.pages[a].style.left    = "-"+distance+"px"                 //just using - distance to shift the left page out of the view
            this.pages[a].style.display = 'block';
            this.pages[a].style.opacity = '1';
            this.pages[a].style.transform  = "scale(1)";

            this.pages[b].swipe_default = false
            this.pages[b].style.display = 'block';
            this.pages[b].style.left    = '0px'
            this.pages[b].style.opacity = per                               // scaling the opacity from the procent of showing 
            this.pages[b].style.transform = "scale("+ (1 - ((1-per) * 0.2)) +")" // and scale it in a range from 0.8 to 1.0

            /*resetting the a-1 and b+1 elements so they are default*/
            if(a-1 >= 0 && this.pages[a-1].swipe_default == false){     //and hiding the previous and 
                this.resetElement(this.pages[a-1])
            }
            if(b+1 <= this.pages.length-1 && this.pages[b+1].swipe_default == false){   //next one (swipe_default is just to prevent of setting it each time the animation occurs)
                this.resetElement(this.pages[b+1])
            }
        }
    }
};

This is the whole trick, if you need more explanation, just add console.log(per) for example under the definition of the variable to get a better insight.

The position comes from two sources:

animation tree

Mouse is just capturing the shift of mouse down till mouse up (with a little trick to prevent the moving of the mouse out of the display area).

Magnet is just the magic :) It shifts the whole position to the next stable page. So basicly it prevents that the moving stops in the middle of two pages. The reason why it seems so big is that I wrote it in a animation style of flow.

this.magnet (swipe.prototype.magnet): This functions initialize the magnet drift, it is just the starter for the whole process. It gets called from M_up.

this.magnet_position (swipe.prototype.magnet_position): This function gives the position to reach for the animation step, it gets called by animationLoop

this.clear_magnet (swipe.prototype.clear_magnet): just clears the animation (if the user presses the left button with the mouse before magnet finished, or magnet itself finished his progress)

Ok the mouse movement is above it is just to record the current distance the user dragged the mouse. I hope that it clarifies itself with the help of the this.conta.addEventListener lines. If not than just ask and I will try to explain it. Btw. use MDN for a great documentation of the function I used.

And look at the code on top again I modified it a bit in the displayAs part.

Comments