A04 ·
Two Level Shrinking Header and Footer with Ionic Framework
<div ng-app="ionicApp">
<fake-statusbar></fake-statusbar>
<div class="bar bar-subheader bar-assertive">
<h2 class="title">Sub Header</h2>
</div>
<ion-nav-bar class="nav-title-slide-ios7 bar-positive">
<ion-nav-back-button class="button-icon ion-arrow-left-c">
</ion-nav-back-button>
</ion-nav-bar>
<ion-nav-view animation="slide-left-right"></ion-nav-view>
<script id="tabs.html" type="text/ng-template">
<ion-tabs class="tabs-icon-top tabs-positive">
<ion-tab title="Home" icon="ion-home" href="#/tab/home">
<ion-nav-view name="home-tab"></ion-nav-view>
</ion-tab>
<ion-tab title="About" icon="ion-ios7-information" href="#/tab/about">
<ion-nav-view name="about-tab"></ion-nav-view>
</ion-tab>
<ion-tab title="Contact" icon="ion-ios7-world" ui-sref="tabs.contact">
<ion-nav-view name="contact-tab"></ion-nav-view>
</ion-tab>
</ion-tabs>
</script>
<script id="home.html" type="text/ng-template">
<ion-view title="Home">
<ion-content header-shrink scroll-event-interval="5" has-bouncing="false"><div style="height: 64px;"></div>
<div class="list card">
<div class="item item-avatar">
<img src="https://ionicframework.com/img/docs/mcfly.jpg">
<h2>Marty McFly</h2>
<p>November 05, 1955</p>
</div>
<div class="item item-body">
<img class="full-image" src="https://ionicframework.com/img/docs/delorean.jpg">
<p>
This is a "Facebook" styled Card. The header is created from a Thumbnail List item,
the content is from a card-body consisting of an image and paragraph text. The footer
consists of tabs, icons aligned left, within the card-footer.
</p>
<p>
<a href="#" class="subdued">1 Like</a>
<a href="#" class="subdued">5 Comments</a>
</p>
</div>
<div class="item tabs tabs-secondary tabs-icon-left">
<a class="tab-item" href="#">
<i class="icon ion-thumbsup"></i>
Like
</a>
<a class="tab-item" href="#">
<i class="icon ion-chatbox"></i>
Comment
</a>
<a class="tab-item" href="#">
<i class="icon ion-share"></i>
Share
</a>
</div>
</div>
<div class="list card">
<div class="item item-avatar">
<img src="https://ionicframework.com/img/docs/mcfly.jpg">
<h2>Marty McFly</h2>
<p>November 05, 1955</p>
</div>
<div class="item item-body">
<img class="full-image" src="https://ionicframework.com/img/docs/delorean.jpg">
<p>
This is a "Facebook" styled Card. The header is created from a Thumbnail List item,
the content is from a card-body consisting of an image and paragraph text. The footer
consists of tabs, icons aligned left, within the card-footer.
</p>
<p>
<a href="#" class="subdued">1 Like</a>
<a href="#" class="subdued">5 Comments</a>
</p>
</div>
<div class="item tabs tabs-secondary tabs-icon-left">
<a class="tab-item" href="#">
<i class="icon ion-thumbsup"></i>
Like
</a>
<a class="tab-item" href="#">
<i class="icon ion-chatbox"></i>
Comment
</a>
<a class="tab-item" href="#">
<i class="icon ion-share"></i>
Share
</a>
</div>
</div>
<div class="list card">
<div class="item item-avatar">
<img src="https://ionicframework.com/img/docs/mcfly.jpg">
<h2>Marty McFly</h2>
<p>November 05, 1955</p>
</div>
<div class="item item-body">
<img class="full-image" src="https://ionicframework.com/img/docs/delorean.jpg">
<p>
This is a "Facebook" styled Card. The header is created from a Thumbnail List item,
the content is from a card-body consisting of an image and paragraph text. The footer
consists of tabs, icons aligned left, within the card-footer.
</p>
<p>
<a href="#" class="subdued">1 Like</a>
<a href="#" class="subdued">5 Comments</a>
</p>
</div>
<div class="item tabs tabs-secondary tabs-icon-left">
<a class="tab-item" href="#">
<i class="icon ion-thumbsup"></i>
Like
</a>
<a class="tab-item" href="#">
<i class="icon ion-chatbox"></i>
Comment
</a>
<a class="tab-item" href="#">
<i class="icon ion-share"></i>
Share
</a>
</div>
</div>
<div style="height: 49px;"></div>
</ion-content>
</ion-view>
</script>
<script id="facts.html" type="text/ng-template">
<ion-view title="Facts" class="padding">
<ion-content>
<p>Banging your head against a wall uses 150 calories an hour.</p>
<p>Dogs have four toes on their hind feet, and five on their front feet.</p>
<p>The ant can lift 50 times its own weight, can pull 30 times its own weight and always falls over on its right side when intoxicated.</p>
<p>A cockroach will live nine days without it's head, before it starves to death.</p>
<p>Polar bears are left handed.</p>
<p>
<a class="button icon ion-home" href="#/tab/home"> Home</a>
<a class="button icon icon-right ion-chevron-right" href="#/tab/facts2">More Facts</a>
</p>
</ion-content>
</ion-view>
</script>
<script id="facts2.html" type="text/ng-template">
<ion-view title="Also Factual">
<ion-content class="padding">
<p>111,111,111 x 111,111,111 = 12,345,678,987,654,321</p>
<p>1 in every 4 Americans has appeared on T.V.</p>
<p>11% of the world is left-handed.</p>
<p>1 in 8 Americans has worked at a McDonalds restaurant.</p>
<p>$283,200 is the absolute highest amount of money you can win on Jeopardy.</p>
<p>101 Dalmatians, Peter Pan, Lady and the Tramp, and Mulan are the only Disney cartoons where both parents are present and don't die throughout the movie.</p>
<p>
<a class="button icon ion-home" href="#/tab/home"> Home</a>
<a class="button icon ion-chevron-left" href="#/tab/facts"> Scientific Facts</a>
</p>
</ion-content>
</ion-view>
</script>
<script id="about.html" type="text/ng-template">
<ion-view title="About">
<ion-content class="padding">
<h3>Create hybrid mobile apps with the web technologies you love.</h3>
<p>Free and open source, Ionic offers a library of mobile-optimized HTML, CSS and JS components for building highly interactive apps.</p>
<p>Built with Sass and optimized for AngularJS.</p>
<p>
<a class="button icon icon-right ion-chevron-right" href="#/tab/navstack">Tabs Nav Stack</a>
</p>
</ion-content>
</ion-view>
</script>
<script id="nav-stack.html" type="text/ng-template">
<ion-view title="Tab Nav Stack">
<ion-content class="padding">
<p><img src="https://ionicframework.com/img/diagrams/tabs-nav-stack.png" style="width:100%"></p>
</ion-content>
</ion-view>
</script>
<script id="contact.html" type="text/ng-template">
<ion-view title="Contact">
<ion-content>
<p>@IonicFramework</p>
<p>@DriftyCo</p>
</ion-content>
</ion-view>
</script>
</div> .fake-statusbar {
height: 20px;
max-height: 20px;
font-size: 12px;
box-sizing: border-box;
width: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 11;
color: #fff;
padding: 2px 3px 3px 3px;
}
.fake-statusbar .time {
position: absolute;
width: 100%;
height: 100%;
text-align: center;
}
.fake-statusbar .pull-left { float: left; }
.fake-statusbar .pull-right { float: right; }
.bar-header {
height: 64px !important;
}
.bar-header > *{
margin-top: 20px !important;
}
.bar-subheader {
top: 64px !important;
}
.scroll-content{
top: 0 !important;
bottom: 0 !important;
padding-top: 44px !important;
} angular.module('ionicApp', ['ionic'])
.config(function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('tabs', {
url: "/tab",
abstract: true,
templateUrl: "tabs.html"
})
.state('tabs.home', {
url: "/home",
views: {
'home-tab': {
templateUrl: "home.html",
controller: 'HomeTabCtrl'
}
}
})
.state('tabs.facts', {
url: "/facts",
views: {
'home-tab': {
templateUrl: "facts.html"
}
}
})
.state('tabs.facts2', {
url: "/facts2",
views: {
'home-tab': {
templateUrl: "facts2.html"
}
}
})
.state('tabs.about', {
url: "/about",
views: {
'about-tab': {
templateUrl: "about.html"
}
}
})
.state('tabs.navstack', {
url: "/navstack",
views: {
'about-tab': {
templateUrl: "nav-stack.html"
}
}
})
.state('tabs.contact', {
url: "/contact",
views: {
'contact-tab': {
templateUrl: "contact.html"
}
}
});
$urlRouterProvider.otherwise("/tab/home");
})
.controller('HomeTabCtrl', function($scope) {
console.log('HomeTabCtrl');
})
.directive('fakeStatusbar', function() {
return {
restrict: 'E',
replace: true,
template: '<div class="fake-statusbar"><div class="pull-left">Carrier</div><div class="time">3:30 PM</div><div class="pull-right">50%</div></div>'
}
})
.directive('headerShrink', function($document) {
var fadeAmt;
var shrink = function(tabs, tabs_amt, subHeader, header, amt, dir) {
ionic.requestAnimationFrame(function() {
// Threshold is equal to bar-height
var threshold = 44;
// Scrolling down
if(dir === 1) {
var _amt = Math.min(threshold, amt - threshold);
}
// Scrolling up
else if(dir === -1) {
var _amt = Math.max(0, amt - threshold);
}
// The translation amounts should never be negative
_amt = _amt < 0 ? 0 : _amt;
amt = amt < 0 ? 0 : amt;
tabs_amt = tabs_amt < 0 ? 0 : tabs_amt;
// Re-position the header
header.style[ionic.CSS.TRANSFORM] = 'translate3d(0,-' + _amt + 'px, 0)';
fadeAmt = 1 - _amt / threshold;
for(var i = 0, j = header.children.length; i < j; i++) {
header.children[i].style.opacity = fadeAmt;
}
// Re-position the sub-header
subHeader.style[ionic.CSS.TRANSFORM] = 'translate3d(0,-' + amt + 'px, 0)';
// Re-position the tabs
tabs.style[ionic.CSS.TRANSFORM] = 'translate3d(0,' + tabs_amt + 'px, 0)';
});
};
return {
restrict: 'A',
link: function($scope, $element, $attr) {
var starty = 0;
var shrinkAmt;
var tabs_amt;
// Threshold is equal to bar-height + create-post height;
var threshold = 88;
// header
var header = $document[0].body.querySelector('.bar-header');
// sub-header
var subHeader = $document[0].body.querySelector('.bar-subheader');
var headerHeight = header.offsetHeight;
var subHeaderHeight = subHeader.offsetHeight;
// tabs
var tabs = $document[0].body.querySelector('.tabs');
var tabsHeight = tabs.offsetHeight;
var prev = 0
var delta = 0
var dir = 1
var prevDir = 1
var prevShrinkAmt = 0;
var prevTabsShrinkAmt = 0;
$element.bind('scroll', function(e) {
// if negative scrolling (eg: pull to refresh don't do anything)
if(e.detail.scrollTop < 0)
return false;
// Scroll delta
delta = e.detail.scrollTop - prev;
// Claculate direction of scrolling
dir = delta >= 0 ? 1 : -1;
// Capture change of direction
if(dir !== prevDir)
starty = e.detail.scrollTop;
// If scrolling up
if(dir === 1) {
// Calculate shrinking amount
shrinkAmt = headerHeight + subHeaderHeight - Math.max(0, (starty + headerHeight + subHeaderHeight) - e.detail.scrollTop);
// Calculate shrinking for tabs
tabs_amt = tabsHeight - Math.max(0, (starty + tabsHeight) - e.detail.scrollTop);
// Start shrink
shrink(tabs, tabs_amt, subHeader, header, Math.min(threshold, shrinkAmt), dir);
// Save prev shrink amount
prevShrinkAmt = Math.min(threshold, shrinkAmt);
prevTabsShrinkAmt = Math.min(tabsHeight, tabs_amt);
}
// If scrolling down
else {
// Calculate expansion amount
shrinkAmt = prevShrinkAmt - Math.min(threshold, (starty - e.detail.scrollTop));
// Calculate shrinking for tabs
tabs_amt = prevTabsShrinkAmt - Math.min(tabsHeight, (starty - e.detail.scrollTop));
// Start shrink
shrink(tabs, tabs_amt, subHeader, header, shrinkAmt, dir);
}
// Save prev states for comparison
prevDir = dir;
prev = e.detail.scrollTop;
});
}
}
}); The Ionic Framework is an open source front-end framework for developing hybrid mobile apps with Cordova and AngularJS. I’m a big fan and use it a lot in my work at Rangle.io! We are a full-stack web and mobile app development consultancy.
One of the apps we are currently developing has an infinite stream similar to Facebook. Therefore, we decided to mimic the iOS shrinking header UI, to give the user more screen space for actual content. Ionic has had a demo for this for quite some time. However, the shrinking behaviour is limited to the top of the scroll view.
We wanted to take this one step further and allow the shrinking and expansion to happen at any point during the scroll view. Also, we have a two level header: main header and a secondary toolbar.
Since, the animation can occur at any point we have to keep a track of the scroll direction. This is done by calculating the delta:
delta = e.detail.scrollTop - prev;
If delta is greater than zero then the user is scrolling up and vice versa:
dir = delta >= 0 ? 1 : -1;
The shrink amount is calculated based on the scroll position:
// If Shrinking
shrinkAmt = headerHeight + subHeaderHeight - ( (starty + headerHeight + subHeaderHeight) - e.detail.scrollTop) );
// If Expanding
shrinkAmt = prevShrinkAmt - (starty - e.detail.scrollTop);
This works great until the user scrolls to the top/bottom edge and the view has bounce. The bounce causes all kinds of weird glitches. To prevent these from happening we need to add max and min limits:
// If Shrinking
shrinkAmt =
headerHeight +
subHeaderHeight -
Math.max(0, starty + headerHeight + subHeaderHeight - e.detail.scrollTop);
// If Expanding
shrinkAmt = prevShrinkAmt - Math.min(threshold, starty - e.detail.scrollTop);
The final step is to apply the actual CSS transforms from within the ionic.requestAnimationFrame function. This is also where we check to see if the shrink amount is greater than the height of the sub-header and only apply the transforms to the main-header if it is.
The ionic.requestAnimationFrame calls the window.requestAnimationFrame or a polyfill if it’s not available. This helps optimize the animation updates making it much more smoother:
The window.requestAnimationFrame() method tells the browser that you wish to perform an animation and requests that the browser call a specified function to update an animation before the next repaint. The method takes as an argument a callback to be invoked before the repaint.
This will request that your animation function be called before the browser performs the next repaint. The number of callbacks is usually 60 times per second.
— Mozilla Developer Network