Статьи

Создание ориентированного на местоположение сайта с помощью Sencha Touch — Отображение местоположений

Конечный продукт
Что вы будете создавать

Из этого туториала Вы узнаете, как разработать мобильный сайт на основе определения местоположения с помощью поисковой системы Google Place и Sencha Touch 2.1. Это вторая серия из двух частей, и сегодня мы узнаем, как отображать маркеры на карте и детали местоположения.

Это руководство является второй и последней частью нашего предыдущего поста « Создание сайта с учетом местоположения с помощью Sencha Touch» . В первой части мы узнали, как находить различные местные предприятия, такие как рестораны, больницы или театры, вблизи текущего местоположения нашего пользователя. Мы сделали это с помощью геолокации HTML5 и API поиска Google Place.

В этом разделе мы рассмотрим следующие темы:

  • Как сохранить историю браузера с помощью маршрутизатора Sencha.
  • Как отобразить несколько маркеров для каждого места на карте Google и, при выборе маркера, отобразить информационный пузырь с информацией о месте.
  • Как показать полную информацию о каждом месте, их отзывы и как показать изображения мест в галерее стилей Pinterest.

Мы возобновим, где мы оставили в предыдущем посте.


В первом посте приложение, которое мы разработали, было довольно простым по своей навигации, и навигационное представление Sencha идеально подходило для такого макета. Однако, хотя вы обнаружите, что вам нужно добавить дополнительные пользовательские элементы управления на свои панели инструментов, использование этого типа представления становится затруднительным, поскольку Sencha поддерживает панель навигации самостоятельно, а показ / скрытие элементов в ней увеличивает сложность.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
/**
 * Main view — holder of all the views.
 * Card layout by default in order to support multiple views as items
 */
Ext.define(‘Locator.view.Main’, {
  extend: ‘Ext.Container’,
  xtype: ‘main’,
  config: {
    cls: ‘default-bg’,
    layout: ‘card’,
    items: [{
      xtype: ‘categories’
    }]
  }
});

Ранее мы отображали все места в виде списка в представлении Списка мест. Теперь мы хотим добавить опцию Map, которая будет отображать все позиции мест в виде маркеров. Итак, мы меняем вторую страницу на макет карты, который содержит две панели — PlaceList и PlaceMap:

  • Места (Макет карты)
  • PlaceList (просмотр списка Sencha)
  • PlaceMap (Контейнер карты Сенча)
01
02
03
04
05
06
07
08
09
10
11
Ext.define(‘Locator.view.Places’,
 {
  extend: ‘Ext.Container’,
  xtype: ‘places’,
  config: {
    layout: ‘card’,
    items: [{
      xtype: ‘placelist’
    }]
  }
});

Контейнер Places имеет дочернюю панель PlacesList . Поскольку мы опустили навигационное представление, мы должны добавить отдельный заголовок для каждой дочерней панели. Также мы добавляем значок карты справа от панели инструментов; нажав эту кнопку, мы покажем панель «Карта» со всеми маркерами мест.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
Ext.define(‘Locator.view.PlaceList’, {
  extend: ‘Ext.List’,
  xtype: ‘placelist’,
  requires: [‘Ext.plugin.PullRefresh’],
  config: {
    cls: ‘default-bg placelist’,
    store: ‘Places’,
    emptyText: Lang.placeList.emptyText,
    plugins: [{
      xclass: ‘Ext.plugin.PullRefresh’,
      pullRefreshText: Lang.placeList.pullToRefresh
    }],
    itemTpl: Ext.create(‘Ext.XTemplate’,
      document.getElementById(‘tpl_placelist_item’).innerHTML, {
        getImage: function (data) {
          // If there is any image available for the place, show the first one in list item
          if (data.photos && data.photos.length > 0) {
            return ‘<div class=»photo»><img src=»‘ + data.photos[0].url + ‘» /></div>’;
          }
 
          // If there is no image available, then we will show the icon of the place
          // which we get from the place data itself
          return [‘<div class=»icon-wrapper»>’,
            ‘<div class=»icon» style=»-webkit-mask-image:url(‘ + data.icon + ‘);»
            ‘</div>’].join(»);
        },
 
        getRating: function (rating) {
          return Locator.util.Util.getRating(rating);
        }
      }),
    items: [{
      xtype: ‘titlebar’,
      docked: ‘top’,
      name: ‘place_list_tbar’,
      items: [{
        xtype: ‘button’,
        ui: ‘back’,
        name: ‘back_to_home’,
        text: ‘Home’
      }, {
        xtype: ‘button’,
        align: ‘right’,
        iconCls: ‘locate4’,
        iconMask: true,
        name: ‘show_map’,
        ui: ‘dark’
      }]
    }]
  }
});

PlaceList выглядит так:

Обратите внимание на значок карты в правом верхнем углу. Нажав на это, мы создаем и активируем панель «Карта». Давайте создадим представление PlaceMap, которое будет простым контейнером с картой и панелью инструментов, в которой есть еще одна кнопка для возврата в PlaceList.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Ext.define(‘Locator.view.PlaceMap’, {
  extend: ‘Ext.Container’,
  xtype: ‘placemappanel’,
  config: {
    layout: ‘fit’,
    items: [{
      xtype: ‘map’,
      name: ‘place_map’,
      mapOptions: {
        zoom: 15
      }
    }, {
      xtype: ‘titlebar’,
      docked: ‘top’,
      name: ‘place_map_tbar’,
      items: [{
        xtype: ‘button’,
        ui: ‘back’,
        name: ‘back_to_home’,
        text: ‘Home’
      }, {
        xtype: ‘button’,
        align: ‘right’,
        iconCls: ‘list’,
        iconMask: true,
        name: ‘show_place_list’,
        ui: ‘dark’
      }]
    }]
  }
});

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

Давайте перечислим все ссылки, которые нам нужны внутри контроллера.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
refs: {
  // Containers
  categoriesList: ‘categories’,
  main: ‘main’,
  placesContainer: ‘places’,
  placeList: ‘placelist’,
  placeMapPanel: ‘placemappanel’,
  placeMap: ‘map[name=»place_map»]’,
 
  // Buttons
  showMapBtn: ‘button[name=»show_map»]’,
  showPlaceListBtn: ‘button[name=»show_place_list»]’,
  backToHomeBtn: ‘button[name=»back_to_home»]’,
  backToPlaceListBtn: ‘button[name=»back_to_placelist»]’
}

У нас будет метод showHome, который мы вызываем, пока нажаты кнопки возврата на панели PlaceList и PlaceMap. Он возвращает пользователя на домашний экран.

01
02
03
04
05
06
07
08
09
10
/**
 * Show Home Panel
 **/
showHome: function () {
  var me = this;
 
  if (me.getMain().getActiveItem() !== me.getCategoriesList()) {
    me.util.showActiveItem(me.getMain(), me.getCategoriesList());
  }
}

Пока элемент категории нажат, мы создаем контейнер Places, устанавливаем заголовок каждой панели на имя этой категории и активируем панель PlaceList внутри. Последняя часть необходима для того, чтобы каждый раз, когда вы попадете на страницу списка мест с домашней страницы.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * Show places container
 */
showPlacesContainer: function (type) {
  var me = this,
    name = me.util.toTitleCase(type.split(‘_’).join(‘ ‘));
 
  if (!me.getPlacesContainer()) {
    this.getMain().add({
      xtype: ‘places’
    });
  }
 
  // Set the placelist title to Category name
  Ext.each(me.getPlacesContainer().query(‘titlebar’), function (titleBar) {
    titleBar.setTitle(name);
  }, me);
 
  me.getPlacesContainer().setActiveItem(0);
  me.util.showActiveItem(me.getMain(), me.getPlacesContainer());
}

Мы показываем / скрываем контейнеры PlaceList и PlaceMap в зависимости от того, что нажимается — значок списка или значок карты.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
 * Show place list
 */
showPlaceList: function () {
  var me = this;
 
  me.util.showActiveItem(me.getPlacesContainer(), me.getPlaceList(), {
    type: ‘flip’,
    reverse: true
  });
},
 
/**
 * Show places map
 */
showPlacesMap: function () {
  var me = this;
 
  if (!me.getPlaceMapPanel()) {
    me.getPlacesContainer().add({
      xtype: ‘placemappanel’
    });
  }
 
  // Get the active category type and set it to title after changing to titlecase
  me.getPlaceMapPanel().down(‘titlebar’).setTitle(me.util.toTitleCase(me.activeCategoryType.split(‘_’).join(‘ ‘)));
 
  me.util.showActiveItem(me.getPlacesContainer(), me.getPlaceMapPanel(), {
    type: ‘flip’
  });
}

this.activeCategoryType — эта переменная устанавливается, пока пользователь выбирает категорию. Мы придем к этому через минуту. В контроллере контроллера мы добавляем события нажатия кнопки:

1
2
3
4
5
6
7
showMapBtn: {
  tap: ‘showPlacesMap’
},
 
showPlaceListBtn: {
  tap: ‘showPlaceList’
}

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


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

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

Мы добавим два маршрута для страниц категорий и мест соответственно.

1
2
3
4
routes: {
  »: ‘showHome’,
  ‘categories/:type’: ‘getPlaces’
}

В Sencha Controller есть метод с именем redirectTo, который принимает относительный URL-адрес и вызывает соответствующую ему функцию. Мы будем использовать пустую строку для перехода на первую страницу (т. Е. Список категорий) и определенный тип категории для перехода на страницу списка мест.

Итак, хотя мы хотим вернуться к списку категорий из контейнера Places, вместо вызова функции showHome () , мы просто перенаправляем на определенный маршрут, и он автоматически вызывает требуемый метод.

1
2
3
4
5
backToHomeBtn: {
  tap: function () {
    this.redirectTo(»);
  }
}

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

Мы видели метод loadPlaces () в первой части этого руководства, который отправляет Ajax-запрос для получения данных о местах. На этот раз мы добавим туда немного для сохранения текущего типа категории в экземпляре контроллера, который понадобится в будущем. Также мы хотим отключить кнопку карты, пока все места не будут загружены. Мы хотим отобразить маркер, как только карта будет отрисована, и для этого места должны быть загружены первыми.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
launch: function () {
  var me = this;
 
  me.getApplication().on({
    categorychange: {
      fn: function (newCategory) {
        me.activeCategoryType = newCategory;
      }
    },
 
    placechange: {
      fn: function (newPlaceReference) {
        me.activePlaceReference = newPlaceReference;
      }
    }
  });
},
 
/**
 * Retrieve all the places for a particlur category
 */
getPlaces: function (type) {
  var me = this;
 
  // Show the place list page
  me.showPlacesContainer(type);
 
  // Disable the show map button until the list gets loaded
  me.getShowMapBtn().disable();
 
  // Keep a reference of the active category type in this controller
  me.getApplication().fireEvent(‘categorychange’, type);
 
  var store = Ext.getStore(‘Places’),
    loadPlaces = function () {
 
      me.util.showLoading(me.getPlaceList(), true);
 
      // Set parameters to load placelist for this ‘type’
      store.getProxy().setExtraParams({
        location: me.util.userLocation,
        action: me.util.api.nearBySearch,
        radius: me.util.defaultSearchRadius,
        sensor: false,
        key: me.util.API_KEY,
        types: type
      });
 
      // Fetch the places
      store.load(function () {
        me.util.showLoading(me.getPlaceList(), false);
 
        // Enable show map button
        me.getShowMapBtn().enable();
      });
    }
 
    // If user’s location is already not set, fetch it.
    // Else load the places for the saved user’s location
  if (!me.util.userLocation) {
    Ext.device.Geolocation.getCurrentPosition({
      success: function (position) {
        me.util.userLocation = position.coords.latitude + ‘,’ + position.coords.longitude;
        me.util.userLocation = me.util.defaultLocation;
        loadPlaces();
      },
      failure: function () {
        me.util.showMsg(Lang.locationRetrievalError);
      }
    });
  } else {
    // Clean the store if there is any previous data
    store.removeAll();
    loadPlaces();
  }
}

В launch () мы добавляем событие « categorychange » к объекту Application и запускаем его, как только получаем новый тип категории. Он устанавливает переменную контроллера activeCategoryType для этого типа. Теперь вопрос в том, почему мы устанавливаем переменную через событие, а не назначаем ее напрямую? Это потому, что мы можем захотеть установить тип из других представлений или контроллеров. Делая это таким образом, также значительно повышает выполнимость.

Следовательно, для события касания элемента списка категорий мы также не будем вызывать функцию getPlaces () напрямую. Вместо этого мы будем использовать метод redirectTo для загрузки мест для данного типа категории. Давайте добавим это в контроллере контроллера:

1
2
3
4
5
categoriesList: {
  itemtap: function (list, index, target, record) {
    this.redirectTo(‘categories/’ + record.get(‘type’));
  }
}

Поэтому, пока вы выбираете тип категории, скажем, «art_gallery», URL вашего браузера изменится на «/ locator / locator / # Categories / art_gallery», и откроется панель PlaceList со всеми художественными галереями. Теперь, если вы перезагрузите страницу, она снова откроет ту же страницу PlaceList, а не первую страницу.


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

Кроме того, вместо удаления маркеров по одному и добавления других, я предпочитаю полностью удалить панель карты, как только пользователь вернется на страницу списка категорий. Итак, давайте добавим эту часть для нажатия кнопки домой:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
backToHomeBtn: {
  tap: function () {
    this.redirectTo(»);
 
    // Destroy the mappanel completely so that we do not need to
    // save and remove existing markers
    if (this.getPlaceMapPanel()) {
      this.getPlacesContainer().remove(this.getPlaceMapPanel());
    }
  }
},
placeMap: {
  // Create the markers in maprender event
  maprender: ‘showPlacesMarkers’
}

Мы создаем маркеры в событии «maprender».

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/**
 * Create markers for user’s location and all the
 * places.
 */
 
showPlacesMarkers: function (extMap, gMap)
{
  var me = this,
    location, data, marker,
    userLocation = me.util.userLocation.split(‘,’),
    currentPosition = new google.maps.LatLng(userLocation[0], userLocation[1]),
 
    image = new google.maps.MarkerImage(‘resources/images/marker.png’,
      new google.maps.Size(32, 32),
      new google.maps.Point(0, 0)
    ),
 
    // Create an InfoBubble instance
    ib = new InfoBubble({
      shadowStyle: 1,
      padding: 0,
      backgroundColor: ‘rgb(0,0,0)’,
      borderRadius: 4,
      arrowSize: 15,
      borderWidth: 1,
      borderColor: ‘#000’,
      disableAutoPan: true,
      hideCloseButton: true,
      arrowPosition: 30,
      backgroundClassName: ‘infobubble’,
      arrowStyle: 2
    }),
 
    /*
     * Showing InfoBubble
     **/
    setupInfoBubble = function (data, _marker) {
      google.maps.event.addListener(_marker, ‘mousedown’, function (event) {
        // Close existing info bubble
        if (ib) {
          ib.close();
        }
 
        // Kepp an instance of the active place’s id in
        // the infobubble instance for accessing it later
        ib.placeReference = data.reference;
 
        // Set teh content of infobubble
        ib.setContent([
          ‘<div class=»infobubble-content»>’,
          data.name,
          ‘</div>’
        ].join(»));
 
        ib.open(gMap, this);
      });
    };
 
  /**
   * Tap on InfoBubble handled here
   */
  google.maps.event.addDomListener(ib.bubble_, ‘click’, function () {
    if (me.activeCategoryType) {
      me.redirectTo(‘categories/’ + me.activeCategoryType + ‘/’ + ib.placeReference);
    }
  });
 
  // For all the places create separate markers
  me.getPlaceList().getStore().each(function (record) {
    data = record.getData(),
    location = data.geometry.location,
    marker = new google.maps.Marker({
      position: new google.maps.LatLng(location.lat, location.lng),
      map: gMap,
      icon: image
    });
 
    setupInfoBubble(data, marker);
  }, me);
 
  // Create a different marker for user’s current location
  new google.maps.Marker({
    position: currentPosition,
    map: gMap,
    icon: new google.maps.MarkerImage(‘resources/images/currentlocation.png’,
      new google.maps.Size(32, 32),
      new google.maps.Point(0, 0)
    )
  });
 
  // Center the map at user’s location.
  // second time onward it doesn’t center the map at user’s position
  Ext.defer(function () {
    gMap.setCenter(currentPosition);
  }, 100);
}

Множество аспектов функциональности обрабатываются в этой функции:

  • Создавайте маркеры и показывайте
  • Показать текущее местоположение пользователя
  • Показывать инфо-пузырь при нажатии на маркер
  • Нажав на информационный пузырь, перейдите на страницу сведений о месте.

Показывать маркеры довольно просто. Просто создайте маркер, передающий позицию и экземпляр карты Google. Вы также можете использовать другое изображение в маркере, как мы делали здесь. Для каждой записи в хранилище Places мы создаем маркер, а при нажатии кнопки мыши (которая фактически работает как событие нажатия) мы вызываем метод setupInfoBubble (), передающий данные и экземпляр маркера.

Мы извлекаем текущее местоположение пользователя в методе getPlaces () и сохраняем его в одноэлементном классе Util. Мы создаем другой маркер для этой позиции.

В служебной библиотеке карты Google есть отличный компонент Infobubble для отображения пользовательских информационных окон. Вы можете получить подробное обсуждение реализации на iPhone, например, информационную рассылку с Sencha Touch . В основном, мы создаем новый экземпляр InfoBubble с необходимыми новыми параметрами конфигурации. Поскольку в определенный момент времени показывается только один инфопузырь, мы просто заменяем содержимое инфопузырька каждый раз, когда он открывается. Мы сохраняем ссылку на свойство » reference » этого места, которое потребуется для перехода на страницу PlaceDetails.

1
2
3
4
5
6
7
8
/**
 * Tap on InfoBubble handled here
 */
google.maps.event.addDomListener(ib.bubble_, ‘click’, function () {
  if (me.activeCategoryType) {
    me.redirectTo(‘categories/’ + me.activeCategoryType + ‘/’ + ib.placeReference);
  }
});

При нажатии на информационную панель мы перенаправляем браузер на URL-адреса категории /: тип /: ссылка . Мы будем обрабатывать эту часть в другом контроллере, предназначенном для размещения деталей.


Страница сведений о месте открывается, когда пользователь выбирает место из списка или с карты. Я классифицировал детали в 3 частях — информация, галерея изображений и обзоры. TabPanel больше всего подходит для такой раскладки. Давайте посмотрим, как могут быть структурированы виды:

  • details.Main
  • details.Info
  • details.Gallery
  • details.GalleryCarousel
  • details.Review
  • PlaceDetails

Мы создаем другое подмножество пространства имен и помещаем все эти файлы в каталог «details» папки «view». Кроме того, мы создаем отдельный контроллер, который поддерживает только связанные с деталями функции. Не забудьте добавить все эти представления и контроллер в файл app.js.

Основная панель представляет собой простую панель вкладок с тремя дочерними панелями и заголовком.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Ext.define(‘Locator.view.details.Main’, {
  extend: ‘Ext.tab.Panel’,
  xtype: ‘detailspanel’,
  config: {
    cls: ‘details-tabpanel default-bg’,
    tabBar: {
      docked: ‘bottom’
    },
    items: [{
      xtype: ‘info’
    }, {
      xtype: ‘gallery’
    }, {
      xtype: ‘review’
    }, {
      xtype: ‘titlebar’,
      docked: ‘top’,
      title: ‘Details’,
      items: [{
        text: ‘Places’,
        ui: ‘back’,
        name: ‘back_to_placelist’
      }]
    }]
  }
});

На информационной странице мы покажем основную информацию о месте, его веб-сайте, телефоне, профиле Google+ и карте с указанием только этого места.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
Ext.define(‘Locator.view.details.Info’, {
  extend: ‘Ext.Container’,
  xtype: ‘info’,
  config: {
    cls: ‘transparent details-info’,
    iconCls: ‘info’,
    title: Lang.info,
    scrollable: true,
    tpl: Ext.create(‘Ext.XTemplate’,
      document.getElementById(‘tpl_place_details_info’).innerHTML, {
        getRating: function (rating) {
          return Locator.util.Util.getRating(rating);
        }
      })
  }
});
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!— Place Details Info Page —>
<script type=»text/template» id=»tpl_place_details_info»>
  <div class=»block content»>
    <div class=»container»>
      <div class=»name»>{name}</div>
      <div class=»address»>{formatted_address}</div>
      {rating:this.getRating}
    </div>
    <div class=»actions»>
      <div class=»website»>
        <a href=»{website}» target=»_blank»>
          <div class=»icon»></div>
          <div class=»text»>Website</div>
        </a>
      </div>
      <div class=»phone»>
        <a href=»tel:{phone}»>
          <div class=»icon»></div>
          <div class=»text»>Call</div>
        </a>
      </div>
      <div class=»profile»>
        <a href=»{url}» target=»_blank»>
          <div class=»icon»></div>
          <div class=»text»>Profile</div>
        </a>
      </div>
    </div>
  </div>
 
  <div class=»block map»></div>
</script>

Элемент DIV с классом css «map» используется позже для рендеринга карты Google. Я использовал разные стили на всех страницах с подробностями — я не включаю все эти детали CSS в содержание этого учебника, чтобы держать его в чистоте. Вы получите весь CSS правильно структурированный в файлах проекта.

Представление информации создано. Теперь нам нужно загрузить информацию о месте и применить его данные в шаблоне страницы Info. Давайте определим контроллер PlaceDetails.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
Ext.define(‘Locator.controller.PlaceDetails’, {
    extend: ‘Ext.app.Controller’,
    util: Locator.util.Util,
    config: {
      routes: {
        ‘categories/:type/:reference’: ‘showDetails’
      },
 
      refs: {
        main: ‘main’,
        placeDetailsPanel: ‘detailspanel’,
 
        placeDetailsInfo: ‘info’,
        placeDetailsGallery: ‘gallery’,
        placeDetailsReview: ‘review’
      },
 
      control: {}
    }
});

Нам понадобится метод showDetails, который создаст и откроет панель вкладок details.Main. При выборе определенного места у нас есть как тип категории, так и ссылка на место. Нам нужно сохранить оба этих значения — ссылка на место будет использоваться для получения дополнительной информации о месте, а тип категории потребуется, если пользователь непосредственно откроет страницу сведений о месте и затем попытается вернуться на страницу списка мест.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * Maintain details panel routes and show the details panel
 */
showDetails: function (categoryType, placeReference) {
  var me = this;
 
  if (!me.getPlaceDetailsPanel()) {
    me.getMain().add({
      xtype: ‘detailspanel’
    });
  }
 
  me.util.showActiveItem(me.getMain(), me.getPlaceDetailsPanel());
 
  // Load place data
  me.loadPlaceDetails(placeReference);
 
  // Fire the category change and place change events
  // so that the category id and place reference can be kept
  // which is needed if users press back button
  me.getApplication().fireEvent(‘categorychange’, categoryType);
  me.getApplication().fireEvent(‘placechange’, placeReference);
}

Мы запускаем события « categorychange » и « placechange » для объекта Application, чтобы сохранить тип и ссылку. Также мы вызываем метод loadPlaceDetails (), передавая ответ мест, чтобы получить полную информацию об этом месте.

Функция PHP довольно проста, как мы использовали ранее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
  * Get place details along with all the photo urls.
  * @return String
  */
public static function getPlaceDetails() {
  try {
    $data = json_decode(self::sendCurlRequest());
    $item = $data->result;
 
    if (isset($item->photos)) {
      for ($i = 0; $i < count($item->photos); $i++) {
        $item->photos[$i]->url = BASE_API_URL .
                .
                .
                .
                .
      }
    }
 
    return json_encode($data);
  } catch (Exception $e) {
    print «Error at getPlaceDetails : » .
  }
}

Изображения для места не приходят автоматически — скорее, все изображения должны запрашиваться отдельно с их ссылкой, ключом API и размером. Полную информацию об этом можно найти здесь . Мы сохраняем все URL-адреса фотографий в свойстве «фотографии» данных Place.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
 * Load the complete details of the place
 * and apply the details to all the panels’ template in the tabpanel
 */
loadPlaceDetails: function (placeReference) {
  var me = this;
 
  me.util.showLoading(me.getPlaceDetailsPanel(), true);
 
  Ext.Ajax.request({
    url: me.util.api.baseUrl,
    method: ‘GET’,
    params: {
      action: me.util.api.details,
      reference: placeReference,
      key: me.util.API_KEY,
      sensor: true
    },
    success: function (response) {
      me.util.showLoading(me.getPlaceDetailsPanel(), false);
      var json = me.currentLocation = Ext.decode(response.responseText);
 
      // Apply the data in panel templates
      me.getPlaceDetailsInfo().setData(json.result);
      me.getPlaceDetailsGallery().setData(json.result);
      me.getPlaceDetailsReview().setData(json.result);
 
      // CShow the location of the place as a marker
      me.showPlaceLocation();
    },
    failure: function () {
      me.util.showMsg(Lang.serverConnectionError);
    }
  });
}

Как только мы получим объект сведений о месте, мы применим результат ко всем дочерним контейнерам панели вкладок, потому что все они используют функциональность шаблона Sencha. Также мы вызываем метод showPlaceLocation (), который создает карту Google, отображает ее на панели «Информация» и отображает маркер для места внутри карты.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
 * Create the map and show a marker for that position in the map
 */
showPlaceLocation: function () {
  var me = this,
    showMarker = function () {
      var location = me.currentLocation.result.geometry.location,
        latLng = new google.maps.LatLng(location.lat, location.lng),
        image = new google.maps.MarkerImage(‘resources/images/marker.png’,
          new google.maps.Size(32, 32),
          new google.maps.Point(0, 0)
        );
 
      // Create the marker for that place
      me.singleMapMarker = new google.maps.Marker({
        position: latLng,
        map: me.gmap,
        icon: image
      });
 
      // Set the marker to center.
      // bring the marker at center.
      Ext.defer(function () {
        me.gmap.setCenter(latLng);
      }, 100);
 
    };
 
  if (me.singleMap) {
    me.singleMap.destroy();
  }
 
  // Create a map and render it to certain element
  me.singleMap = Ext.create(‘Ext.Map’, {
    renderTo: me.getPlaceDetailsInfo().element.down(‘.map’),
    height: 140,
    mapOptions: {
      zoom: 15
    },
    listeners: {
      maprender: function (mapCmp, gMap) {
        me.gmap = gMap;
        showMarker();
      }
    }
  });
}

Итак, мы закончили с основной информационной страницей, и вот как это выглядит:


Мы создадим галерею изображений в стиле Pinterest для изображений Place и покажем полный просмотр изображений в карусели. Я разработал компонент для этого и написал подробный пост в блоге, который вы можете найти здесь, в галерее изображений Mosaic с Sencha Touch . Здесь используются одни и те же компоненты — только свойства данных различны.

Вот как выглядит галерея. -webkit-column свойство CSS3 используется здесь, чтобы получить мозаичный макет для изображений.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
Ext.define(‘Locator.view.details.Gallery’, {
  extend: ‘Ext.Container’,
  xtype: ‘gallery’,
  config: {
    title: Lang.gallery,
    iconCls: ‘photos2’,
    cls: ‘transparent gallery’,
    scrollable: true,
    // Template to show the thumbnail images
    tpl: Ext.create(‘Ext.XTemplate’,
      ‘<tpl if=»this.isEmpty(values)»>’,
      ‘<div class=»empty-text empty-gallery»>’, Lang.placeDetails.emptyGallery, ‘</div>’,
      ‘</tpl>’,
      ‘<div class=»gallery body» id=»photos»>’,
      ‘<tpl for=»photos»>’,
      ‘<img src=»{url}» class=»thumbnail» />’,
      ‘</tpl>’,
      ‘</div>’, {
        isEmpty: function (result) {
          if (!result.photos || result.photos.length === 0) {
            return true;
          }
 
          return false;
        }
      })
  },
 
  initialize: function () {
    var me = this;
 
    // Add tap event on the images to open the carousel
    me.element.on(‘tap’, function (e, el) {
      me.showGalleryCarousel(el);
    }, me, {
      delegate: ‘img.thumbnail’
    });
 
    me.callParent(arguments);
  },
 
  /**
   * Show the gallery carousel with all the images
   */
  showGalleryCarousel: function (clickedImage) {
    var me = this,
      clickedImgIndex = 0;
 
    // Query all the images and save in an array
    me.images = me.element.query(‘img.thumbnail’);
 
    // Create the Gallery Carousel
    var galleryCarousel = Ext.Viewport.add({
      xtype: ‘gallerycarousel’,
      images: me.images
    });
 
    // On clicking close icon, hide the carousel
    // and destroy it after a certain perdiod
    galleryCarousel.element.on(‘tap’, function (e, el) {
      galleryCarousel.hide(true);
 
      Ext.defer(function () {
        Ext.Viewport.remove(galleryCarousel);
      }, 300);
    }, this, {
      delegate: ‘div[data-action=»close_carousel»]’
    });
 
    // Get the image index which is clicked
    while ((clickedImage = clickedImage.previousSibling) != null) {
      clickedImgIndex++;
    }
 
    // Set the clicked image container as the active item of the carousel
    galleryCarousel.setActiveItem(clickedImgIndex);
 
    // Show the carousel
    galleryCarousel.show();
  }
});
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
Ext.define(‘Locator.view.details.GalleryCarousel’, {
  extend: ‘Ext.carousel.Carousel’,
  xtype: ‘gallerycarousel’,
  config: {
    fullscreen: true,
    modal: true,
    images: [],
    html: ‘<div class=»close-gallery» data-action=»close_carousel»></div>’,
    cls: ‘gallery-carousel’,
    showAnimation: ‘popIn’,
    hideAnimation: ‘popOut’,
    indicator: false,
    listeners: {
      initialize: ‘changeImageCount’,
 
      // Call image count checker for each image change
      activeitemchange: ‘changeImageCount’
    }
  },
 
  initialize: function () {
    var me = this,
      images = me.getImages();
 
    // Create a bottom bar which will show the image count
    me.bottomBar = Ext.create(‘Ext.TitleBar’, {
      xtype: ‘titlebar’,
      name: ‘info_bar’,
      title: »,
      docked: ‘bottom’,
      cls: ‘gallery-bottombar’,
      items: [{
        xtype: ‘button’,
        align: ‘left’,
        iconCls: ‘nav gallery-left’,
        ui: ‘plain’,
        handler: function () {
          me.previous();
        }
      }, {
        xtype: ‘button’,
        align: ‘right’,
        iconCls: ‘nav gallery-right’,
        ui: ‘plain’,
        handler: function () {
          me.next();
        }
      }]
    });
 
    // Add the images as separate containers in the carousel
    for (var i = 0; i < images.length; i++) {
      me.add({
        xtype: ‘container’,
        html: ‘<img class=»gallery-item» src=»‘ + images[i].src + ‘» />’,
        index: i + 1
      });
    }
 
    me.add(me.bottomBar);
    me.callParent(arguments);
  },
 
  /**
   * Change image count at bottom bar
   */
  changeImageCount: function () {
    var me = this;
    me.bottomBar.setTitle((me.getActiveIndex() + 1) + ‘ of ‘ + me.getImages().length);
  }
});

Обзоры мест — это простой список отзывов, который я использовал с простым контейнером Sencha вместо представления списка. Добавление некоторого пользовательского стиля становится легким здесь и также заставляет это выглядеть круто. Мы использовали ту же систему рейтинга, чтобы показать отдельные рейтинги.

Изображения пользователей — это их изображения в Google+. Мы используем набор пользовательских шаблонных функций для редактирования контента на лету.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!— Place Details Review Page —>
<script type=»text/template» id=»tpl_place_details_reviews»>
  <div class=»details-item body»>
    <tpl if=»this.isEmpty(values)»>
      <div class=»empty-text»> No review available for this place.
    </tpl>
    <tpl for=»reviews»>
      <div class=»review»>
        <a href=»{author_url}» target=»_blank»>
           <img
             class=»profile-img»
             src=»{author_url:this.getUserImage}»
             <!— Show a default user icon if there is no user image available —>
             onerror=»Locator.util.Util.onBrokenProfileImage(this)»/>
        </a>
        <div class=»heading»>
           By <a href=»{author_url}» target=»_system»>{author_name:this.toTitleCase}</a>
           <span> {time:this.getDate}
        </div>
        <div class=»details»>
          <div class=»rating»>
            <tpl for=»aspects»>
              <div class=»aspect»>
                <div class=»type»>{type:this.toTitleCase}</div>
                <div class=»stars»>{rating:this.getStars}</div>
              </div>
            </tpl>
          </div>
          <div class=»text»>{[this.applyExpandable(values)]}</div>
 
          <!— A hidden element which holds the complete review text —>
          <div class=»full-review»>{text}</div>
        </div>
      </div>
    </tpl>
  </div>
</script>
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
Ext.define(‘Locator.view.details.Review’, {
  extend: ‘Ext.Container’,
  xtype: ‘review’,
  config: {
    cls: ‘transparent’,
    title: Lang.reviews,
    iconCls: ‘chat2’,
    scrollable: true,
    tpl: Ext.create(‘Ext.XTemplate’,
      document.getElementById(‘tpl_place_details_reviews’).innerHTML, {
        toTitleCase: function (str) {
          return Locator.util.Util.toTitleCase(str);
        },
 
        getDate: function (timestamp) {
          return Locator.util.Util.prettyDate(timestamp * 1000);
        },
 
        getUserImage: function (authorUrl) {
          if (authorUrl) {
            var arr = authorUrl.split(‘/’);
            return ‘https://plus.google.com/s2/photos/profile/’ + arr[arr.length — 1] + ‘?sz=50’;
          }
 
          return Locator.util.Util.defaultUserImage;
        },
 
        getStars: function (rating) {
          return Locator.util.Util.getRating(rating, 3, true);
        },
 
        isEmpty: function (result) {
          if (!result.reviews || result.reviews.length === 0) {
            return true;
          }
 
          return false;
        },
 
        applyExpandable: function (data) {
          var text = data.text;
 
          if (text.length > 120) {
            text = Ext.String.ellipsis(text, 120) +
              ‘ <span data-action=»more» class=»resize-action»>more
          }
 
          return text;
        }
      })
  }
});

Из шаблонов и функций XTemplate мы замечаем ряд новых вещей:

Мы добавляем событие onerror в тег IMG. Это событие вызывается, если атрибут src пуст или равен нулю. Мы создаем функцию onBrokenProfileImage () в нашем файле Util и вызываем ее, если есть какая-либо ошибка, и устанавливаем для атрибута src изображение пользователя по умолчанию.

1
2
3
4
5
onBrokenProfileImage: function (image) {
  image.onerror = «»;
  image.src = Locator.util.Util.defaultUserImage;
  return true;
}

Вместо того, чтобы просто показывать дату, мы предпочитаем дату в стиле Twitter, такую ​​как «2 часа назад», «3 минуты назад» и т. Д. Мы добавляем метод prettyDate ( ) в класс Util   который преобразует дату в красивую дату.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
 * Give the date a format like 2 hours ago, 5 mins ago
 */
prettyDate: (function () {
  var ints = {
    second: 1,
    minute: 60,
    hour: 3600,
    day: 86400,
    week: 604800,
    month: 2592000,
    year: 31536000
  };
 
  return function (time) {
 
    time = +new Date(time);
 
    var gap = ((+new Date()) — time) / 1000,
      amount, measure;
 
    for (var i in ints) {
      if (gap > ints[i]) {
        measure = i;
      }
    }
 
    amount = gap / ints[measure];
    amount = gap > ints.day ?
    amount = Math.ceil(amount);
    amount += ‘ ‘ + measure + (amount > 1 ? ‘s’ : ») + ‘ ago’;
 
    return amount;
  };
 
})()

Я получил эту функцию где-то в Интернете во время просмотра. Отлично работает.

Большинство отзывов пользователей представляют собой большой объем текстов, а отображение полного текста в списке не очень удобно для мобильных устройств. Таким образом, мы добавили возможность показать / скрыть полный обзор, если он содержит более 120 символов. Мы добавляем метод XTemplate applyExpandable (), который проверяет длину текста, и добавляем ссылку « more » в конец текста после многоточия.

01
02
03
04
05
06
07
08
09
10
applyExpandable: function (data) {
  var text = data.text;
 
  if (text.length > 120) {
    text = Ext.String.ellipsis(text, 120) +
      ‘ <span data-action=»more» class=»resize-action»>more
  }
 
  return text;
}

Мы обрабатываем действия пользователя по ссылке «more» в контроллере PlaceDetails. Давайте добавим метод, который заботится о том же.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * Handle text expansion of long reviews.
 */
handleReviewExpansion: function (panel) {
  panel.element.on(‘singletap’, function (e, el) {
    el = Ext.get(el),
    textEl = el.parent(‘.text’);
 
    // If «more» is pressed, get the complete text from hidden element and a «less» button
    if (el.getAttribute('data-action') === 'more') {
      textEl.setHtml(textEl.next('.full-review').getHtml() +
      ' <span data-action="less" class="resize-action">less</span>');
    }
 
    // If "less" is pressed, ellipsis the text and show                                            
    if (el.getAttribute('data-action') === 'less') {
      textEl.setHtml(Ext.String.ellipsis(textEl.getHtml(), 120) +
      ' <span data-action="more" class="resize-action">more</span>');
    }
  }, this, {
    delegate: '.resize-action'
  });
}

При однократном нажатии на элемент панели обзора мы фиксируем событие и, если текст уже свернут, разверните его ссылкой «меньше» и наоборот ссылкой «больше». Мы используем ссылку на профиль рецензента, чтобы обернуть как изображение, так и имя пользователя, поэтому, щелкнув по нему, откроется профиль пользователя в Google+.


Возможно, вы уже заметили, что я изменил тему из моей предыдущей версии приложения. Это потому, что фон приложения темный, а светлый цвет панели инструментов сделает его красивым. Кроме того, я использовал темные кнопки на панели инструментов для лучшей контрастности. Вот файл SASS, который я использовал:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Basic color definitions
$base-color: #00E7EB;
$base-gradient: 'matte';
$active-color: #00BABD;
 
// Toolbar styles
$toolbar-base-color: $base-color;
$toolbar-button-color: #111;
 
// Tabbar styles
$tabs-dark: #333;
$tabs-dark-active-color: $base-color;
 
// List styles
$list-header-bg-color : #ddd;
 
@import 'sencha-touch/default/all';
 
// Default styling with only those components which are required
@include sencha-panel;
@include sencha-buttons;
@include sencha-tabs;
@include sencha-toolbar;
@include sencha-indexbar;
@include sencha-list;
@include sencha-layout;
@include sencha-carousel;
@include sencha-loading-spinner;
@include sencha-list-pullrefresh;
 
// Icons
@include pictos-iconmask('locate4');
@include pictos-iconmask('list');
@include pictos-iconmask('photos2');
@include pictos-iconmask('chat2');
 
// Buttons
@include sencha-button-ui('back', #333, 'matte');
@include sencha-button-ui('dark', #333, 'matte');

Это оно. Мы закончили со всеми страницами для приложения, и теперь это полноценный мобильный сайт. Вы можете попробовать обернуть его с помощью Phonegap, и оно должно работать без каких-либо проблем.


В этом последнем посте серии мы рассмотрели ряд интересных тем, которые включают в себя:

  • Сенча пойдут
  • Показать карту с маркерами
  • iPhone как Infobubble
  • Галерея изображений мозаики похожа на макет Pinterest

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