Задача плотной упаковки блоков

Плотная упаковка блоков не очень частая задача в вебе, но если она встречается в наших проектах, то мы непременно сталкиваемся с определёнными трудностями в её реализации. Дело в том, что все существующие на сегодняшний день CSS-механизмы не позволяют, без неприятных ограничений, решить эту головоломку, поэтому нам приходится прибегать к иным внешним силам и использовать JavaScript.

В этой статье я решил сначала рассмотреть CSS-варианты, которые при фразе «Плотная упаковка блоков» сразу же возникают в голове, обсудить их недостатки и уже после перейти к решению на JavaScript.

Задание

Перед тем, как приступать к вариантам решения, давайте познакомимся с самой задачей.

Допустим у нас есть контейнер произвольной ширины и высоты, в котором находится n-количество блоков разного размера. Наша задача разместить эти блоки так, чтобы они они по-максимуму прижимались друг к другу со всех сторон, т.е. предсталяли собой правильную сетку.

Я набросал примерный вариант того, что нужно получить:

1

На рисунке видно, как блоки пытаются втиснуться в свободные места насколько это возможно, образовывая сетку. К тому же надо учитывать, что при добавлении блоков динамическим или иным путём сетка не должна портиться, а, наоборот, блоки должны перемещаться в правильные координаты.

Исходная разметка

На протяжении всей статьи для основных примеров было бы логично использовать одну и туже разметку. Так мы и поступим.

Основной HTML-код будет состоять из контейнера и восьми блоков внутри него. Чтобы было нагляднее видно куда какой блок перемещается, заголовки блоков будут пронумерованы:

    
<div id="container" class="container">
    <div class="item width15pct">
      <h1>Title 1</h1>
      <p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s. </p>
    </div>
    <div class="item width25pct">
      <h1>Title 2</h1>
      <p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s. </p>
    </div>
    <div class="item width35pct">
      <h1>Title 3</h1>
      <p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s. </p>
    </div>
    ....
</div>

А CSS будет таким:

.container { 
  width: 80%;
  border: 2px dashed #000;
  padding: 5px;
  margin: 0 auto;
  overflow: hidden;
}

.item {
  background: #E76D13;
  margin-bottom: 10px;
  margin-right: 10px;
  
}
.width15pct {
  width: 15%;
  
}
.width25pct {
  width: 25%;
 
  background: #ff4e00;
}
.width35pct {
  width: 35%;
  background: #7a3a0b;
}

За исключением уникальных случаев, от решения к решению мы будет немножко видоизменять CSS.

А теперь к делу!

Настало время познакомиться с всевозможными механизмами? Ну конечно же, вперёд!

Плавающие блоки (float'ы)

Первое, что пришло мне на ум, это упаковать блоки при помощи плавающих элементов, т.е. старых добрых float'ов . Поэтому давайте немножко изменим CSS в пользу этого варианта, а точнее добавим к селектору .item правило float:left; и посмотрим, что из этого выйдет.

.item {
  background: #E76D13;
  margin-bottom: 10px;
  margin-right: 10px;
  
  float:left;
}

Предлагаю сразу тестировать результат на живом примере, созданном в Codepen:

Если сделать меньше/больше ширину экрана, то можно увидеть, как все блоки, пытаясь уместиться в свободное пространство, неуклюже прижимаются друг к другу справа, создавая при этом много свободных областей.

Почему это происходит? Дело в том, что согласно спецификации, именно так и работает механизм плавающих блоков. В нашем случае, т.к. пунктам задано float:left;, перемещаемый влево элемент должен сместиться влево и вверх на максимальное возможное для него пространство. Если же блоку не хватает места (напр. его ширина больше, чем доступное пространство), то он спускается ниже до той области, где его размеры позволяют ему уместиться, и прижимается к первому удобному блоку, вместо того, чтобы, например, опуститься ещё ниже и заполнить собой пустую область, которая более выгодная в отношении плотной упаковки блоков.

Т.к. плавающий механизм не приспособлен работать иным, выгодном для нас способом, то этот вариант, к сожалению, нам не подходит, поскольку создаётся слишком много свободных мест и упаковка блоков выглядит рваной и некрасивой. Поэтому давайте искать решение дальше…

Чередование плавающих блоков

Пока я писал эту статью, мой коллега Илья Стрельцын (@SelenIT2) предложил мне протестировать ещё один вариант. Суть его в том, чтобы задать элементам разное значение float, в зависимости от их чётной и нечётной позиции в контейнере, например, чётным float : left, а нечётным float : right. Я так и сделал:

.item:nth-child(odd) {float: left;}
.item:nth-child(even) {float: right;}

И вот что из этого вышло:

Что касается меня, то этот вариант мне кажется не лучше предыдущего, поскольку, как видно из результата, блоки просто расходятся в разные стороны в зависимости от от значения float, а в середине контейнера остаются пустые области. Это особенно хорошо видно на полноэкранном примере.

Можно конечно задать каждому блоку своё обтекание, но в этом случае мы должны чётко знать размеры каждого блока, их расположение в потоке и тоже самое учитывать при динамическом добавлении элементов. Но этот вариант слишком сложный и бессмысленный, поэтому давайте переходём к следующему.

Модный и современный flex-box

Более везучие разработчики, у которых есть возможность применять в своих проектах очень модную, современную и набирающую последнее время обороты, технологию flex-box, наверняка прибегнут именно к ней.

Давайте снова немножко изменим наш CSS с учётом flex-box’a и посмотрим результат. Flexible Box сама по себе простая, но в тоже время очень мощная спецификация, и предлагает большое количество интересных возможностей, о которых ранее мы могли только мечтать. Например, мы легко можем выровнять flex-элемент/ы сверху, по середине или внизу контейнера, или одним движением руки решить задачу с равномерным выравниванием блоков по ширине и это только цветочки.

Возможно в это трудно поверить, но, несмотря на свою мощь, эта спецификация не способна решить нашу задачу и сейчас вы узнаете почему.

Чтобы flex-двигатель запустился и наши пункты превратились во flex-элементы, достаточно просто задать контейнеру display: flex;, а также ещё несколько свойств для того, чтобы по-максимуму приблизить раскладку flex-элементов к решению нашей задачи.

.container { 
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: flex-start;
  align-content: flex-start; 
}

И вот что у меня вышло:

Если честно, то на мой взгляд, этот результат даже менее симпатичен, чем предудущий с применением float’ов. Здесь мы видим, как блоки выстраиваются в ряд друг за другом, пока у них хватает места справа, а когда свободное место заканчивается, то блоки переходят на другой ряд. Причём отчётливо видно, что каждый ряд представляет собой строку, высота которой зависит от высоты самого длинного в ней элемента. В связи с этим flex-элемент, который не умещается в первой строке, перемещается вниз на вторую строку, а следовательно под самый высокий элемент в предыдущем ряду-строке, а следующие элементы идут за ним, и так далее.

Т.е. выходит, что flex-элемент не может нарушить правила своей строки и выйти за её пределы, а значит в любом случае появится много пустых областей и наша плотная упаковка блоков будет выглядеть угрюмо.

Раз и это решение нам тоже не подходит, придётся искать другие.

Grid Layout

Ну и последний из CSS-механизмов, который лучше всего подходит для нашей задачи, а даже скорее решает её, это CSS-модуль Grid Layout, который, как видно из его названия, как раз и предназначен для сеток, но с помощью него можно также плотно упоковать блоки. К сожалению, на данный момент, мы не можем его применить, т.к. во-первых, его поддержка браузерами слишком скудна ― он работает только лишь в браузере IE10+ (что удивительно) и в последних версиях Chrome, но только если пройти по ссылке chrome://flags и включить параметр «Включить экспериментальные инструменты разработчика», а во-вторых, этот модуль ещё разрабатывается и когда он будет готов и поддерживаться во всех браузерах, его синтаксис может сильно поменяться.

Поэтому мы не будем тратить на эту спецификацию время, а просто возьмём её на карандаш для будущих свершений, и перейдём, наконец, к самому правильному на сегодняшний день варианту, воспользовавшись одной интересной JavaScript-библиотекой.

Masonry

Если у вышеописанных механизмов были недостатки, то это всё потому, что ни один из них не был предназначен для упоковки блоков. Но существует одна вещь, которая специально создана для этой цели. Встречайте Masonry ― JavaScript-библиотека для выстраивания элементов по сетке и также может решить задачу с плотно прижатыми блоками без выравнивания. Она размещает элементы в самых оптимальних координатах, основываясь на свободном вертикальном пространстве, подобно каменщику, который подгоняет камни для стены.

Я не стану описывать все функции этой библиотеки, поскольку всё уже сделали до меня, просто рассмотрю самые важные моменты.

Перед тем, как начать работать с Masonry, нужно скачать один из двух предлагаемых пакетов установки, либо masonry.pkgd.min.js (минифицированный, для пользователей), либо файл masonry.pkgd.js (для разработчиков).

Далее следует подключить скаченный файл к странице. Это можно сделать несколькими способами, которые описаны на оф. сайте. Здесь же я буду рассматривать вариант, который учитывает, что для своего проекта я использую jQuery. И мой код подключения выглядит так:

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.0/jquery.min.js"></script>
<script src="путь к папке с файлом/masonry.pkgd.min.js"></script>

Я просто добавил эти файлы в раздел <head> своего сайта.

Теперь осталось только запустить двигатель библиотеки, чтобы она применилась к нашим пунктам. Возможно, вы уже заметили, что в основном HTML-коде у контейнера блоков есть id="container", а у самих  пунктов класс .item. Именно на них мы будем ссылаться для вызова Masonry:

// получаем ссылку на контейнер
var $container = $('#container');

// инициализация
$container.masonry({
  columnWidth: 1,

  // обращаемся к пунктам
  itemSelector: '.item'
});

Это всё, что требуется для вызова. А вот и сам результат:

По-моему, очень даже ничего. Независимо от размера экрана, правильно упаковываются, пытаясь втиснуться в свободные области, насколько это возможно. Благодаря этому, пустых мест почти не остаётся. Так что смело можно сказать, что эта js-библиотека справилась с нашим заданием.

Тем не менее, поигравшись с Masonry, я обнаружил несколько любопытных нюансов, которыми я хочу поделиться.

  • Начнём с того, что у этой библиотеки есть одна неприятнаю особенность. Если ширина и высота элементов задана в процентах (или они растягиваются в зависимости от содержимого), то при инициализации Masonry некоторые элементы могут слипнуться по горизонтали. Это поведение можно видеть на следующим скриншоте:

    2

    Решается эта проблема очень легко:

    function masonryFunc(){
        // получаем ссылку на контейнер
        var $container = $('#container');
    
        // инициализация
        $container.masonry({
            columnWidth: 1,
    
            // обращаемся к пунктам
            itemSelector: '.item'
        });
    } 
    masonryFunc()
    setTimeout(masonryFunc,300)
    

    Просто оберните вызов библиотеки в отдельную функцию и после этого вызовите её два раза, первый раз для её инициализации masonryFunc(), а второй вызов поместите в таймер setTimeout, чтобы вызвать библиотеку через несколько миллисекунд (300мс вполне сойдёт). Это решает данную проблема.

  • В функции вызова есть параметр «columnWidth». Как видно из его названия, он служит для установки ширины каждой колонке. Этот параметр нужен скорее для горизонтальной сеточной раскладки, поэотому в нашем случае, чтобы блоки максимально прижимались друг к другу по горизонтали, я решил, что правильно будет выставлять этому параметру значение «1«, т.е. минимальное из возможных.
  • Есть также параметр «gutter», который необходим для выставления горизонтальных интервалов между элементов. Этот параметр также, как и предудущий, служит для сеточной раскладки. поэтому я думаю, что было бы логично использовать вместо него старый добрый margin, заданный, как для горизонтальных, так и для вертикальных отступов между блоками (напр. margin: 10px 10px 0 0;).

Вот и всё!

Мне удалось найти решение задачи плотной упаковки блоков. Но тем не менее я очень надеюсь, что в комментариях добрые люди подскажут нам всем ещё и другие решения, особенно сделанные на чистом CSS.

P.S. Это тоже может быть интересно:

8 Комментарии

  1. cyklop77

    ох… нет это далеко не всё. сколько я намучался в своё время чтобы при помощи любого плагина типа masonry сделать в резиновом блоке плотную упаковку блоков с шириной их заданной в процентах. при этом(важно!) необходимо было сделать так чтобы по краям от блоков ни в какой момент времени не оставалось пустое пространство… но похоже, это задада нерешаемая, даже на htmlforume никто не смог помочь её решить

    1. Антон Шигаев

      Однозначно нерешаемая. Весь вэпчик изначально под жозге листы типографики заточен, куда тут решаться.

  2. Мимо проходил

    Модный и современный flex-box

    Его результаты легко и просто повторяются с помощью старого доброго
    display: inline-block;
    vertical-align: top;

    1. SelenIT

      И какой-нибудь «магии» для устранения зависимости от межтеговых пробелов:). Которая при неосторожном применении может быть чревата.

      1. Мимо проходил

        ИМХО, проще всего тут просто не ставить эти пробелы. Вырезать шаблонизатором, или писать стили без пробелов. Тогда никакой уличной магии не нужно.

        1. SelenIT

          Безусловно. Но сам факт необходимости думать об этом (что и есть зависимость) огорчает. Всё-таки инлайн-блоки — не инструмент для раскладки изначально, в отличие от флексбоксов.

  3. Андрей

    Masonry — не единственный модуль, решающий эту задачу. Есть, например, не менее мощный Isotope

  4. Andy

    Не вводите людей в заблуждение!!!

    Перед тем как статью писать, хотябы проверили.

    Не давно столкнулся с темже глюком Masonry — цитата: «Если ширина и высота элементов задана в процентах (или они растягиваются в зависимости от содержимого), то при инициализации Masonry некоторые элементы могут слипнуться по горизонтали.»

    Только один нюанс — у меня были изображения в каждом блоке, и пока они загружались, плагин «криво» расставлял блоки.

    И тоже попытался решить проблему с повторной инициализацией плагина через задержку…

    FAIL — на десктопных компьютерах всё ехало, однако эта задержка в .2s была визуально видна. Но на мобильных девайсах, в частности iPhone6 (оказался самым тормозным), всё равно этой задержки было не достаточно!

    Решил вопрос по другому, повесил событие на onload тега img, которое инициализировало плагин при каждой загрузке картинки, и нет задержки (визуально). Радует то, что плагин «помнит» все элементы и тормозов не обнаружилось, даже на медленном iPhone6.

Оставить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Получать новые комментарии по электронной почте. Вы можете подписаться без комментирования.