何となくどうぶつフォトコンテストアプリを作りたくなったので、PinterestライクなUIをMonaca IDEでサクッと作ります。
こんなの
ライブラリはあるけど・・
Masonry
https://masonry.desandro.com/
スマホだから2カラムで良いし、何か大袈裟な気がする。
CSSで作る
まずはCSSで作ってみます。
段組みレイアウト(CSS Columns)を使えばよさそうです。
ons.bootstrap()
.controller('PhotoController', function() {
this.photos = [
{
url: './img/214hariIMGL9360_TP_V.jpg',
text: '1.かわいいやろ?かわいいやろ?かわいいやろ?',
imageHeight: '200px'
},
{
url: './img/214hariIMGL9876_TP_V.jpg',
text: '2.かわいいやろ?かわいいやろ?かわいいやろ?かわいいやろ?かわいいやろ?',
imageHeight: '600px'
},
{
url: './img/C784_edawokuwaerugorira_TP_V.jpg',
text: '3.かわいいやろ?かわいいやろ?かわいいやろ?',
imageHeight: '180px'
},
{
url: './img/inosisiFTHG2811_TP_V.jpg',
text: '4.かわいいやろ?かわいいやろ?かわいいやろ?かわいいやろ?',
imageHeight: '150px'
},
{
url: './img/miyaDSC_4461_TP_V.jpg',
text: '5.かわいいやろ?かわいいやろ?かわいいやろ?かわいいやろ?',
imageHeight: '240px'
},
{
url: './img/nuko-9_TP_V.jpg',
text: '6.かわいいやろ?',
imageHeight: '300px'
},
{
url: 'https://pbs.twimg.com/profile_images/581025665727655936/9CnwZZ6j.jpg',
text: '7.かわいいやろ?かわいいやろ?かわいいやろ?',
imageHeight: '300px'
}
];
});
<ons-page ng-controller="PhotoController as vm">
<div class="header">動物フォトコンテスト開催中</div>
<div class="photos">
<div class="photo" ng-repeat="photo in vm.photos">
<div class="photo-image" ng-style="{ backgroundImage: 'url('+ photo.url +')', height: photo.imageHeight }"></div>
<div class="photo-text">{{ photo.text }}</div>
</div>
</div>
</ons-page>
.header {
padding: 16px;
background-color: white;
text-align: center;
font-size: 24px;
font-weight: bold;
}
.photos {
-webkit-column-count: 2;
column-count: 2;
-webkit-column-gap: 30px;
column-gap: 8px;
padding: 8px;
}
.photo {
-webkit-column-break-inside: avoid;
page-break-inside: avoid;
break-inside: avoid;
display: inline-block;
width: 100%;
margin-bottom: 8px;
background-color: white;
border-radius: 12px;
}
.photo-image {
background-position: center;
background-repeat: no-repeat;
background-size: cover;
cursor: zoom-in;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
.photo-text {
padding: 8px;
}
でもこれだと、1カラム目を描画してから、2カラム目を描画していて、コレジャナイ感があります。
Pinterestを観察
ちゃんと高さが低いカラムから埋めてますね。
これがやりたいんですよ。
次にソースコードを見て、どんな感じに実装しているのか観察します。
すべての要素をposition: absolute; left: 0; top: 0;
で左上に固定して、そこからtransform: translateX(**px) translateY(**px)
で配置しているようです。
JSで作る
この方法だとJSで作る必要が有ります。
今回はAngularJSで作ります。汎用性を考えて、ディレクティブで作ります。
ons.bootstrap()
.controller('PhotoController', function($scope, $timeout) {
// CSSで作った、と同じため省略…
}).directive('pinterestLikeGrid', function () {
return {
restrict: 'E',
transclude: true,
replace: true,
template: '<div class="pinterest-like-grid" ng-transclude></div>',
scope: {
gutterWidth: '@',
gutterHeight: '@'
},
link: function (scope, element, attrs) {
var gutterWidth = parseInt(attrs.gutterWidth, 10) || 8;
var gutterHeight = parseInt(attrs.gutterHeight, 10) || 8;
var photosElem = element[0];
scope.$watch(function () {
return photosElem.childNodes.length; // 子要素の件数が変わったらレイアウトを調整
}, function() {
// 2カラムなので、親の横幅÷2。余白分も差っ引く
var width = (photosElem.offsetWidth - gutterWidth*3) / 2;
var photoElems = photosElem.querySelectorAll('.pinterest-like-grid-item');
var firstColHeight = gutterHeight;
var secondColHeight = gutterHeight;
// 子要素を1件ずつ配置していく
photoElems.forEach(function (photoElem) {
photoElem.style.width = width + 'px';
var height = photoElem.offsetHeight;
var x, y;
// 高さの低い方のカラムに配置する
if (firstColHeight <= secondColHeight) {
// 1カラム目
x = gutterWidth;
y = firstColHeight;
firstColHeight = firstColHeight + height + gutterHeight;
} else {
// 2カラム目
x = width+gutterWidth*2;
y = secondColHeight;
secondColHeight = secondColHeight + height + gutterHeight;
}
photoElem.style.transform = photoElem.style.webkitTransform = 'translateX(' + x + 'px) translateY(' + y + 'px)';
photoElem.style.height = height + 'px';
});
});
}
};
}).directive('pinterestLikeGridItem', function () {
return {
restrict: 'E',
transclude: true,
replace: true,
template: '<div class="pinterest-like-grid-item" ng-transclude></div>'
};
});
<ons-page ng-controller="PhotoController as vm">
<div class="header">動物フォトコンテスト開催中</div>
<pinterest-like-grid gutter-width="12" gutter-height="12">
<pinterest-like-grid-item ng-repeat="photo in vm.photos">
<div class="pinterest-like-grid-item-image" ng-style="{ backgroundImage: 'url('+ photo.url +')', height: photo.imageHeight }"></div>
<div class="pinterest-like-grid-item-text">{{ photo.text }}</div>
</pinterest-like-grid-item>
</pinterest-like-grid>
</ons-page>
.header {
padding: 16px;
background-color: white;
text-align: center;
font-size: 24px;
font-weight: bold;
}
.pinterest-like-grid {
position: relative;
}
.pinterest-like-grid-item {
-webkit-column-break-inside: avoid;
page-break-inside: avoid;
break-inside: avoid;
display: inline-block;
width: 100%;
margin-bottom: 8px;
background-color: white;
border-radius: 12px;
position: absolute;
left: 0;
top: 0;
}
.pinterest-like-grid-item-image {
background-position: center;
background-repeat: no-repeat;
background-size: cover;
cursor: zoom-in;
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
.pinterest-like-grid-item-text {
padding: 8px;
}
ポイントとしては、widthのスタイルを変更してから、高さをoffsetHeightで取得している点です。
widthを変える前は1/1カラムなので、widthを1/2カラム分の変更してから高さを取得します。
後は、transform には、translateXとtranslateYだけ設定します。translateZを0にするとiOSのMobile Safafiでスクロールした際に画面がチラつきます。(Safafiのバグっぽい)
ちなみに、topとleftの位置を変更しても良いですが、transformの方がGPUレイヤーで描画されるので速そうです。(検証はしてない)
画面
うん、なんかいい感じ。
次回へ続く
UIができたので、次回はサーバー側を作ってみます。(気が向いたら)