どうぶつフォトコンテストアプリを作る（PinterestライクなUI編）

何となくどうぶつフォトコンテストアプリを作りたくなったので、PinterestライクなUIをMonaca IDEでサクッと作ります。

## こんなの
https://www.pinterest.jp/

<img width="240" alt="ファイル_001.png" src="https://qiita-image-store.s3.amazonaws.com/0/51815/b770ad23-cb4a-8ae5-f380-7c2f3243a080.png">


## ライブラリはあるけど・・
Masonry
https://masonry.desandro.com/

スマホだから2カラムで良いし、何か大袈裟な気がする。

## CSSで作る
まずはCSSで作ってみます。
段組みレイアウト（CSS Columns）を使えばよさそうです。

```js
ons.bootstrap()
  .controller('PhotoController', function() {
    this.photos = [
      {
        url: './img/214hariIMGL9360_TP_V.jpg',
        text: '１．かわいいやろ？かわいいやろ？かわいいやろ？',
        imageHeight: '200px'
      },
      {
        url: './img/214hariIMGL9876_TP_V.jpg',
        text: '２．かわいいやろ？かわいいやろ？かわいいやろ？かわいいやろ？かわいいやろ？',
        imageHeight: '600px'
      },
      {
        url: './img/C784_edawokuwaerugorira_TP_V.jpg',
        text: '３．かわいいやろ？かわいいやろ？かわいいやろ？',
        imageHeight: '180px'
      },
      {
        url: './img/inosisiFTHG2811_TP_V.jpg',
        text: '４．かわいいやろ？かわいいやろ？かわいいやろ？かわいいやろ？',
        imageHeight: '150px'
      },
      {
        url: './img/miyaDSC_4461_TP_V.jpg',
        text: '５．かわいいやろ？かわいいやろ？かわいいやろ？かわいいやろ？',
        imageHeight: '240px'
      },
      {
        url: './img/nuko-9_TP_V.jpg',
        text: '６．かわいいやろ？',
        imageHeight: '300px'
      },
      {
        url: 'https://pbs.twimg.com/profile_images/581025665727655936/9CnwZZ6j.jpg',
        text: '７．かわいいやろ？かわいいやろ？かわいいやろ？',
        imageHeight: '300px'
      }
    ];
  });
```

```html
<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>
```

```css
.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;
}
```

<img width="240" alt="キャプチャ.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/51815/55fa4832-c7fc-613c-09c4-fbe3906762bf.jpeg">

でもこれだと、1カラム目を描画してから、2カラム目を描画していて、コレジャナイ感があります。

## Pinterestを観察
ちゃんと高さが低いカラムから埋めてますね。
これがやりたいんですよ。

<img width="240" alt="キャプチャ.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/51815/10c7a337-779d-708e-27fc-48baae819501.png">


次にソースコードを見て、どんな感じに実装しているのか観察します。
すべての要素を`position: absolute; left: 0; top: 0;`で左上に固定して、そこから`transform: translateX(**px) translateY(**px)`で配置しているようです。

<img width="912" alt="キャプチャ.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/51815/5f509282-2dad-ae04-a0f1-d60e35af3e62.png">

## JSで作る
この方法だとJSで作る必要が有ります。
今回はAngularJSで作ります。汎用性を考えて、ディレクティブで作ります。

```js
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>'
    };
  });
```

```html
<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>
```

```css
.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レイヤーで描画されるので速そうです。（検証はしてない）

### 画面
<img width="240" alt="キャプチャ.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/51815/21096cb0-20df-400d-ce66-7bd6f9b43bc8.jpeg">

うん、なんかいい感じ。

## 次回へ続く
UIができたので、次回はサーバー側を作ってみます。（気が向いたら）