必見の記事

HTML5 × CSS3 × jQueryを真面目に勉強 – #12 Pinterest風グリッドレイアウトを作ってみた

2013.01.15

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Pinterest.com

そんな訳で、写真共有SNSの一つであるPinterest(ぴんたれすと)。従来のグリッド式レイアウトのように高さが均一のグリッドが整然と並べられているのと違い、異なる高さのグリッドが画面いっぱいに敷き詰められているレイアウトが特徴的でオサレです。(※こういったレイアウトはピンボード風と呼べば良いのでしょうか…?

Pinterest

Pinterest とはピンボード風の写真共有のソーシャル・ネットワーキング・サービス。特に女性に人気がある。ウェブサイトとアプリはテーマに基づいて写真のコレクションを作ることが出来る。サイトのミッションステートメントは「面白いと感じるものを通じて世界全員をつなぐ」。アメリカ Palo Alto にある Cold Brew Labs によって運営されている。

Wikipediaより引用(http://ja.wikipedia.org/wiki/Pinterest)

そんな女子力の高いPinterest。メインストリートのならず者である僕ですが、こいつのグリッドレイアウトを再現してみたので、ここにその手順をまとめておくとします。けっこう難易度は高めですが、未来の自分が読んでも理解できるよう、なるべく噛み砕いて綴っていきます。

はじめに

Pinterest風グリッドレイアウトの要件を大まかに書きだしてみます。

  1. 高さの異なる各グリッドをタテ・ヨコと共に等間隔で配置したい
  2. 配置されるグリッドは親要素であるコンテナの幅いっぱいに敷き詰められ、リキッドレイアウトに対応する
  3. グリッドは左から右に向かって配置され、1行に収まり切らない場合は折り返して次の行に配置される
  4. 2行以降の配置は左端から順番に配置するとは限らず、Topからの距離が一番短い位置に配置される
  5. 以降のグリッドもTopからの距離が一番短い位置から順に配置される。(※必ずしも2行目が全て埋まってから3行目に配置されるとは限らない)
  6. ウィンドウサイズを変更することによって、列数とグリッドの配置をアニメーションで動的に変更する

なるほど わからん。

いくつか補足説明します。
従来のグリッドレイアウトのルールに習うと各グリッドは以下の図1のように1 → 2 → 3 → 4 (改行) 5 → 6 → 7 → 8 etc...といったように左から右に向かって順に配置されていきます。通常ならこれで何の問題もありませんが、今回のPinterest風グリッドでは大きな問題があります。

これではダメです 図1

図1ではグリッド3が他のグリッドと比較して縦に長いモノとなっています。この程度なら大して問題にはなりませんが、仮にブラウザをスクロールしなくてはならないほど縦に長い物となったらどうでしょうか?5 → 6と来て次のグリッド7はスクロールしないと見ることができないという状態になってしまいます。そこでPinterestでは以下の図のような順序で各グリッドを配置しています。

こういうレイアウトにしたいです。 図2

お分かりいただけただろうか?

グリッド6までは図1と変わっていませんが、グリッド7とグリッド8の左右の順序が逆になっています。これは2行目以降はTopからの距離が一番短い位置(※一番高位置に配置できるところ)から優先してグリッドが配置されていってるためです。Pinterestはピンボード風の写真共有SNSというコンセプトが優先されているためか、ユーザーに対して順序を明確に認識させることを重要視していないので、このような配置を採用しています。(※勝手な憶測です。もし間違っていたら正解を教えて下さい。)

ではこれより実装していきます。

Pinterest風グリッドレイアウトを実装

1 | HTMLをマークアップ

まずはHTMLから書いていきます。

<div id="container">
		<div class="grid">
			<div class="imgholder">
				<img src="http://media-cache-ec5.pinterest.com/upload/24769866672500089_TyoEco4P_b.jpg">
			</div>
			<h2 id="toc-1-photo-title">1 | Photo title</h2>
			<p>elisa french lace veil - enchanted atelier fall winter 2013 collection</p>
			<div class="meta">by j osborn</div>
		</div>
		<div class="grid">
			<div class="imgholder">
				<img src="http://media-cache-ec4.pinterest.com/upload/36521446949707052_2V4XPV6i_b.jpg">
			</div>
			<h2 id="toc-2-photo-title">2 | Photo title</h2>
			<p>Bead Board + Letters: What a sweet and personal way to decorate.</p>
			<div class="meta">by SigitEko</div>
		</div>
		<div class="grid">
			<div class="imgholder">
				<img src="http://media-cache0.pinterest.com/upload/155866837075421772_fQt6Oh6X_b.jpg">
			</div>
			<h2 id="toc-3-photo-title">3 | Photo title</h2>
			<p>For a foster child who's been passed around from home to home, a little piece of hope is lost at each transition. This week, your support will give them a duffel ...</p>
			<div class="meta">by Lars van de Goor</div>
		</div>
		<div class="grid">
			<div class="imgholder">
				<img src="http://media-cache-lt0.pinterest.com/upload/41376890297795636_E7VJyXn8_b.jpg">
			</div>
			<h2 id="toc-4-photo-title">4 | Photo title</h2>
			<p>pretty hues</p>
			<div class="meta">by Andrea Andrade</div>
		</div>
		<div class="grid">
			<div class="imgholder">
				<img src="http://media-cache-ec5.pinterest.com/upload/204702745534709172_LRil1AUb_b.jpg">
			</div>
			<h2 id="toc-5-photo-title">5 | Photo title</h2>
			<p>ringbearer's boutonniere</p>
			<div class="meta">by Lars van de Goor</div>
		</div>
		<div class="grid">
			<div class="imgholder">
				<img src="../common/img/myLogo.jpg">
			</div>
			<h2 id="toc-6-photo-title">6 | Photo title</h2>
			<p>wakamsha...</p>
			<div class="meta">by Zsolt Zsigmond</div>
		</div>
		etc ...
	</div>

これといって特別な記述はしていません。各グリッドに対してgridクラスを指定し、これらをcontainer要素で囲っています。gridクラス要素内の構造は後述するスタイルに合わせて適当に書いただけで、このとおりにする必要は特にありません。

ちなみにimgタグに指定している画像URLは本家サイトの画像パスを適当に拝借しています。

2 | グリッドをCSSでデザイン

各グリッドのデザインとそのコンテナ要素のスタイルを書いていきます。

pinterestgrid.css

#container {
  margin: 0 auto 25px;
  position: relative;
  padding-bottom: 10px;
}

.grid {
  background: #fff;
  box-shadow: 0 1px 3px rgba(34, 25, 25, 0.4);
  float: left;
  font-size: 12px;
  margin: 8px;
  min-height: 100px;
  padding: 15px;
  width: 188px;
  transition: top 1s ease, left 1s ease;
}
.grid h2 {
  border-bottom: 1px solid #ccc;
  color: #fa3599;
  display: block;
  font-family: 'Donegal One', serif;
  font-size: 17px;
  margin: 10px 0;
  padding: 0 0 5px;
}
.grid .meta {
  color: #777;
  font-style: italic;
  text-align: right;
}
.grid .imgholder img {
  background: #ccc;
  display: block;
  max-width: 100%;
}

当記事執筆時点(※2013年1月)ではTransitionsプロパティに対してベンダープレフィックスをつけて指定する必要があります。当記事では可読性を考慮して省略しております。

また、22行目にてフォントファミリーを指定していますが、これはGoogle Web Fontsを使用しています。

ここまでで基本的なデザインは出来ました。いよいよここからJavaScriptを駆使してレイアウトの実装を行なっていきます。

3 | JavaScriptの実装1 - レイアウトの実現

とりあえずプラグイン化といったものは後回しにして、先に動くところまで作ってみるとします。パラメータ等も現段階ではベタ書きで構いません。

まずはカラム数を5に固定して実装してみます。

pinterestgrid.js

$(function() {
	// 画像読み込み完了後に実行		// ※1
	$(window).on('load', function() {
		// カラムのwidthを設定する
		colWidth = $('.grid').outerWidth() + offsetX * 2;

		// 最初にgridArrayを初期化
		gridArray = [];
		// 空のgridArrayを作成する
		for (var i=0; i<numOfCol; i++) {
			pushGridArray(i, 0, 1, -offsetY);
		}

		$('.grid').each(function(index) {
			setPosition($(this));
		});
	});

	var gridArray = [],	// ※2
		colWidth,
		offsetX = 5,
		offsetY = 5,
		numOfCol = 5;

	// gridArrayに新しいgridを追加
	function pushGridArray(x, y, size, height) {
		for (var i=0; i<size; i++) {
			var grid = [];
			grid.x = x + i;
			grid.endY = y + height + offsetY * 2;

			gridArray.push(grid);
		}
	}

	// gridArrayから指定したx位置にあるgridを削除
	function removeGridArray(x, size) {
		for (var i=0; i<size; i++) {
			var idx = getGridIndex(x + i);
			gridArray.splice(idx, 1);
		}
	}

	// gridArray内にある高さの最小値と最大値および最小値のあるx値を取得
	function getHeightArray(x, size) {
		var heightArray = [];
		var temps = [];
		for (var i=0; i<size; i++) {
			var idx = getGridIndex(x + i);
			temps.push(gridArray[idx].endY);
		}
		heightArray.min = Math.min.apply(Math, temps);
		heightArray.max = Math.max.apply(Math, temps);
		heightArray.x = temps.indexOf(heightArray.min);

		return heightArray;
	}

	// gridのx値を基準にgridのインデックスを検索
	function getGridIndex(x) {
		for (var i=0; i<gridArray.length; i++) {
			var obj = gridArray[i];
			if (obj.x === x) {
				return i;
			}
		}
	}

	// gridを配置
	function setPosition(grid) {
		if(!grid.data('size') || grid.data('size') < 0) {
			grid.data('size', 1);
		}

		// gridの情報を定義
		var pos = [];
		var tempHeight = getHeightArray(0, gridArray.length);
		pos.x = tempHeight.x;
		pos.y = tempHeight.min;

		var gridWidth = colWidth - (grid.outerWidth() - grid.width());

		// gridのスタイルを更新		// ※3
		grid.css({
			'left': pos.x * colWidth,
			'top': pos.y,
			'position': 'absolute'
		});

		// gridArrayを新しいgridで更新
		removeGridArray(pos.x, grid.data('size'));
		pushGridArray(pos.x, pos.y, grid.data('size'), grid.outerHeight());
	}

	//IE用にArray.indexOfメソッドを追加	// ※4
	if (!Array.prototype.indexOf) {
		Array.prototype.indexOf = function(elt /*, from*/) {
			var len = this.length >>> 0;

			var from = Number(arguments[1]) || 0;
			from = (from < 0) ? Math.ceil(from) : Math.floor(from);
			if (from < 0) {
				from += len;
			}

			for (; from < len; from++) {
				if (from in this && this[from] === elt) {
					return from;
				}
			}
			return -1;
		};
	}
});
[/javascript]
<p>カラム数が固定の為にウィンドウリサイズへの対応はまだですが、とりあえずここまででPinterest風のレイアウトは実現できました。結構ややこしいことになっているので、補足説明を入れておきます。</p>

<h4>※1) loadイベント発火後に処理を実行する</h4>
<p>上記に書かれているJavaScriptコードは全体を<span class="bold">$(function(){});</span>で囲っていることから、<span class="bold">onloadイベント</span>のタイミングで処理が実行されています。しかしonloadイベントとは<span class="bold red">HTMLコードがひと通り読み込まれ、JavaScriptが実行可能な状態になったタイミング</span>で発火するイベントであり、この時点ではまだ画像ファイルは読み込みが完了していません。したがってこのタイミングで実行してしまうと各グリッドの最終的なheight値が得られないまま処理が走ってしまうため、期待通りのレイアウトにならなくなります。上記コードはすべての画像ファイルの読み込みが完了してから実行する必要があるため、<span class="cybercyan bold">windowオブジェクトのloadイベント発火時</span>に実行させる必要があります。</p>

<h4>※2) 配置済みのグリッドの情報を配列で管理する</h4>
<p>このレイアウトを実現する上で一番のキーポイントです。まず<span class="bold">gridArray</span>という配列を定義します。</p>
<dl>
	<dt>gridArray:Array</dt>
	<dd>1. カラム数ぶんの長さを持つ。</dd>
	<dd>2. 配列には配置済みのグリッドのうち、各カラムの一番下に位置しているグリッドの情報が格納される</dd>
	<dd>3. 配列の各要素には以下の情報が格納される
		<ul>
			<li><span class="bold">x</span>: 自分<span class="ash">(※グリッド)</span>が格納されている配列のインデックス</li>
			<li><span class="bold">endY</span>: 自分<span class="ash">(※グリッド)</span>が配置されているカラムの上から下までの長さ<span class="ash">(※距離)</span></li>
		</ul>
	</dd>
</dl>
<p><img src="https://devio2023-media.developers.io/wp-content/uploads/2013/01/img-pin_logic1-640x288.png" alt="図3" width="640" height="288" class="custom-img size-medium wp-image-42242" /> <span class="bold ash">図3</span></p><!-- logic1 -->
<p>配列は図3のような構造をしています。5つのグリッド情報が配列に格納されており、これに6個目のグリッドを格納したいとします。<span class="ash">(※図4)</span></p>
<p><img src="https://devio2023-media.developers.io/wp-content/uploads/2013/01/img-pin_logic2-640x288.png" alt="図4" width="640" height="288" class="custom-img alignnone size-medium wp-image-42243" /> <span class="bold ash">図4</span></p><!-- logic2 -->
<p>すでに格納されている5つのグリッド情報のうち、<span class="bold">endY</span>の値が最も小さく且つ順番が若いのはgridArray[0]であり、グリッド6はここに格納します。</p>
<p><img src="https://devio2023-media.developers.io/wp-content/uploads/2013/01/img-pin_logic3-640x288.png" alt="img-pin_logic3" width="640" height="288" class="custom-img alignnone size-medium wp-image-42244" /> <span class="bold ash">図5</span></p><!-- logic3 -->
<p>よって図5のようにgridArray[0]には新たにグリッド6が格納され、endYの値も更新されます。このルーチンをひたすら繰り返すことで、次に来るグリッドがどのカラムのどの位置に配置されるかを算出していきます。</p>

<h4>※3) グリッドを絶対配置する</h4>
<p>※2の処理から取得したx値とy値でグリッドを絶対配置します。</p>

<h4>※4) IEにArray.indexOfメソッドを追加する</h4>
<p>なんとビックリ、InternetExplorerには<span class="bold red">Array.indexOfメソッドが実装されていません。</span>そこで上記のコードを記述することでIEにindexOfメソッドを追加させます。</p>
<ul>
	<li><a href="https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/indexOf" target="_blank" rel="noopener noreferrer">Array indexOf method - JavaScript | MDN</a></li>
</ul>

<h3>4 | JavaScriptの実装2 - ウィンドウリサイズに対応</h3>
<p>ウィンドウ幅をリサイズした際にカラム数がウィンドウ幅に合わせて可変し、都度レイアウトが最適化されるような処理を追加していきます。</p>
<p class="code-summary bold">コードを以下のように編集、追記します。</p>

<p>ウィンドウのリサイズイベントにて<span class="bold">Container要素</span>と<span class="bold">Windowオブジェクト</span>の幅を取得します。そしてそこから表示可能なカラム数を算出して、レイアウトを適用します。</p>

<h3>4 | jQueryプラグイン化する</h3>
<p>最後にJavaScriptコードをjQueryプラグインとして外出しします。オプションとして渡せるパラメータは以下にしました。</p>
<ol>
	<li>offsetX</li>
	<li>offsetY</li>
	<li>gridElement</li>
</ol>
<p class="code-summary bold">jquery.pinterestGrid.js</p>

<p>これでプラグイン化できました。jQueryプラグインについては以下の記事にて詳しく解説しています。</p>
<ul>
	<li><a href="https://dev.classmethod.jp/client-side/html5-x-css3-x-jquery-8-jqplugin/" target="_blank" rel="noopener noreferrer">jQueryプラグインについて詳しく</a></li>
</ul>
<p>最後に呼び出し部分です。以下のようにオプション指定して呼び出します。</p>
<p class="code-summary bold">pinterestgrid.html <span class="ash">(※JS部分のみ)</span></p>

$(function() {
	$(window).on('load', function() {
		$('#container').pinterestGrid({
			offsetX: 8,
			offsetY: 8,
			gridElement: '.grid'
		});
	});
});

sampleimg-pinterestgrid1

これでようやく完成です。こちらから実際の動きを確認できます。

おわりに

従来とは一味違ったこのグリッドレイアウトは、Pinterestを始めファッション性の高いWebサイトにてよく見かけます。以前このシリーズで紹介したパララックスエフェクトと並ぶトレンドだったと言っても過言ではないでしょう。トレンドだっただけにこの手のレイアウトを実現するプラグインは探せばいくつか出てきますが、僕としては自前で作成することによって良いアルゴリズムのトレーニングになったと思います。

今回作成したのは他に出回っているプラグインに比べていくらか機能の少ないシンプルなモノとなっていますが、もしご興味ある方がおりましたら当ソースコードをフォークして、より高機能なシロモノにカスタマイズしていただければ幸いです。

参考サイト

本家サイト

その他のPinterest風レイアウトを実現するプラグイン

女子力の高そうなサイト