Mobile

Implementing a Fixed Position iOS Web Application

Ryan Fioravanti
December 2010


Background

Here at Google, we're constantly pushing the boundaries of what's possible in a mobile web application. Technologies like HTML5 allow us to blur the distinction between web apps and native apps on mobile devices. As part of that effort we've developed a fixed position version of our mobile Gmail application. The mechanisms being used here are similar to the ones used to implement our two pane UI for Gmail on the iPad.

In a desktop web application, this would be a simple feat because desktop browsers support position: fixed. In mobile Safari, setting an element to position: fixed does not fix the element relative to the viewport. To achieve fixed elements, we need to override native scrolling and implement our own custom scrolling for the content that we want to be scrollable.

In this article, we will show you the building blocks required to incorporate fixed position into your own mobile web applications. We will cover:

  • Creating a suitable layout
  • Animating the scrollable content on drag using transforms
  • Animating the scrollable content with momentum using transitions
  • Halting a scrolling element on touch, mid-transitions

Creating a Suitable Layout

When laying out the DOM for your fixed position application, you simply need to decide what elements you want to be fixed and what region you want to be scrollable. We will show you a simple example of an application that has a top and bottom toolbar, with the middle content being scrollable.

<html>
<head>
<style>
  .TOP_TOOLBAR,
  .BOTTOM_TOOLBAR {
    height: 50px; width: 100%; position: absolute;
  }
  .TOP_TOOLBAR {
    top: 0;
  }
  .BOTTOM_TOOLBAR {
    bottom: 0;
  }
  .SCROLLER_FRAME {
    width: 100%; position: absolute; top: 50px; bottom: 50px;
  }
</style>
</head>
<body>
  <div class=”TOP_TOOLBAR”>
    ... toolbar content ...
  </div>
  <div class=”SCROLLER_FRAME”>
    <div class=”SCROLLER”>
      ... scrollable content ...
    </div>
  </div>
  <div class=”BOTTOM_TOOLBAR”>
    ... toolbar content ...
  </div>
</body>
</html>

You will notice that in the layout we show above, there are three elements that we explicitly position (TOP_TOOLBAR, SCROLLER_FRAME, BOTTOM_TOOLBAR). These are the elements that will be “fixed” in the application, and will never move. The only elements that will ever move in this layout is the div with class SCROLLER. The SCROLLER_FRAME is particularly important because this is the element that defines the boundaries of the scrollable region. In the next section, we will show you how to attach a JavaScript behavior to the SCROLLER element to make it scrollable relative to the SCROLLER_FRAME.

Animating the scrollable content

To begin animating the scrollable content, we need to do two things. First we need to prevent all native scrolling from happening.

document.body.addEventListener('touchmove', function(e) {
  // This prevents native scrolling from happening.
  e.preventDefault();
}, false);

Once native scrolling is prevented, we can attach touch listeners to the scrollable content and use webkit-transforms to reposition the element as the user moves their finger. While performing the scrolling we will need to save some state, so we will define a class called Scroller and pass the scrollable element into this class.

Scroller = function(element) {
  this.element = this;
  this.startTouchY = 0;
  this.animateTo(0);

  element.addEventListener(‘touchstart’, this, false);
  element.addEventListener(‘touchmove’, this, false);
  element.addEventListener(‘touchend’, this, false);
}

Scroller.prototype.handleEvent = function(e) {
  switch (e.type) {
    case “touchstart”:
      this.onTouchStart(e);
      break;
    case “touchmove”:
      this.onTouchMove(e);
      break;
    case “touchend”:
      this.onTouchEnd(e);
      break;
  }
}

Scroller.prototype.onTouchStart = function(e) {
  // This will be shown in part 4.
  this.stopMomentum();

  this.startTouchY = e.touches[0].clientY;
  this.contentStartOffsetY = this.contentOffsetY;
}

Scroller.prototype.onTouchMove = function(e) {
  if (this.isDragging()) {
    var currentY = e.touches[0].clientY;
    var deltaY = currentY - this.startTouchY;
    var newY = deltaY + this.contentStartOffsetY;
    this.animateTo(newY);
  }
}

Scroller.prototype.onTouchEnd = function(e) {
  if (this.isDragging()) {
    if (this.shouldStartMomentum()) {
      // This will be shown in part 3.
      this.doMomentum();
    } else {
      this.snapToBounds();
    }
  }
}

Scroller.prototype.animateTo = function(offsetY) {
  this.contentOffsetY = offsetY;

  // We use webkit-transforms with translate3d because these animations
  // will be hardware accelerated, and therefore significantly faster
  // than changing the top value.
  this.element.style.webkitTransform = ‘translate3d(0, ‘ + offsetY + ‘px, 0)’;
}

// Implementation of this method is left as an exercise for the reader.
// You need to measure the current position of the scrollable content
// relative to the frame. If the content is outside of the boundaries
// then simply reposition it to be just within the appropriate boundary.
Scroller.prototype.snapToBounds = function() {
  ...
}

// Implementation of this method is left as an exercise for the reader.
// You need to consider whether their touch has moved past a certain
// threshold that should be considered ‘dragging’.
Scroller.prototype.isDragging = function() {
  ...
}

// Implementation of this method is left as an exercise for the reader.
// You need to consider the end velocity of the drag was past the
// threshold required to initiate momentum.
Scroller.prototype.shouldStartMomentum = function() {
  ...
}

So far we have an element that can scroll within its frame, but without any momentum. A few methods are left for you to figure out in order to simplify this article, but with a bit of tinkering you should be able to implement them relatively easily. Next we will talk about initiating momentum.

Animating the scrollable content with momentum

When the user performs a touch drag with enough velocity, the application should continue scrolling the content with some momentum even after they lift their finger. We can use the webkit-transition property to animate the content with realistic deceleration. There are three variables to consider when applying a webkit-transition:

  • The distance to move
  • The time of the animation
  • The timing function to apply to the transition

Distance and time can be calculated given the end velocity of the drag and an acceleration constant (we use 0.0005 px/ms^2). Now we’ll show you a possible implementation for doMomentum() that sets up the proper transition.

Scroller.prototype.doMomentum = function() {
  // Calculate the movement properties. Implement getEndVelocity using the
  // start and end position / time.
  var velocity = this.getEndVelocity();
  var acceleration = velocity < 0 ? 0.0005 : -0.0005;
  var displacement = - (velocity * velocity) / (2 * acceleration);
  var time = - velocity / acceleration;

  // Set up the transition and execute the transform. Once you implement this
  // you will need to figure out an appropriate time to clear the transition
  // so that it doesn’t apply to subsequent scrolling.
  this.element.style.webkitTransition = ‘-webkit-transform ‘ + time +
      ‘ms cubic-bezier(0.33, 0.66, 0.66, 1)’;

  var newY = this.contentOffsetY + displacement;
  this.contentOffsetY = newY;
  this.element.style.webkitTransform = ‘translate3d(0, ‘ + newY + ‘px, 0)’;
}

This function gives us momentum! Notice the timing function we use (cubic-bezier). This particular timing function will simulate natural deceleration, and will look very realistic if you calculate your movement properties correctly and if you select a reasonable acceleration constant. There is one case that we don’t talk about here: what to do if the momentum takes the scrollable area past the boundaries of the scrollable frame. Handling this case is a little bit tricky and is outside the scope of this article. In our implementation we detect this case and handle it by queueing up several transitions:

  • The first transition takes us to the boundary, here the final velocity is not zero so we need a different cubic-bezier timing function.
  • The second transition bounces past the boundary a little bit, the final velocity is zero so we can use the cubic-bezier timing function shown above.
  • The third transition decelerates the content back to the boundary using the build in ease-out timing function.

Note that for transitions two and three, you might consider using different acceleration constants in order to make the bounce feel more elastic.

Halting a scrolling element on touch

If a transition is in progress when the user touches the screen, the scroll view should stop exactly where it is. This is a little bit tricky because when we initiate the transition, we only know the start and end points, but we don’t know where the element is at any given point in time during the transition. Here we will show you how to compute its current location during a transition, and stop its movement.

Scroller.prototype.stopMomentum = function() {
  if (this.isDecelerating()) {
    // Get the computed style object.
    var style = document.defaultView.getComputedStyle(this.element, null);
    // Computed the transform in a matrix object given the style.
    var transform = new WebKitCSSMatrix(style.webkitTransform);
    // Clear the active transition so it doesn’t apply to our next transform.
    this.element.style.webkitTransition = ‘’;
    // Set the element transform to where it is right now.
    this.animateTo(transform.m42);
  }
}

WebKitCSSMatrix is a built-in WebKit class used to perform matrix operations. There are equivalent classes for other HTML5 enabled web browsers. You will need to implement isDecelerating() yourself by saving some state about whether momentum is currently in progress or not.

Conclusion

At this point you should be able to set up a fixed position DOM, scroll content in response to touch events, and initiate/stop momentum. There are a few things you will have to figure out yourself, but we will follow up more information as we learn more and as we get feedback about what developers want to hear about.

Authentication required

You need to be signed in with Google+ to do that.

Signing you in...

Google Developers needs your permission to do that.