Из этого туториала Вы узнаете, как разработать мобильный сайт на основе определения местоположения с помощью поисковой системы Google Place и Sencha Touch 2.1. Это вторая серия из двух частей, и сегодня мы узнаем, как отображать маркеры на карте и детали местоположения.
Это руководство является второй и последней частью нашего предыдущего поста « Создание сайта с учетом местоположения с помощью Sencha Touch» . В первой части мы узнали, как находить различные местные предприятия, такие как рестораны, больницы или театры, вблизи текущего местоположения нашего пользователя. Мы сделали это с помощью геолокации HTML5 и API поиска Google Place.
В этом разделе мы рассмотрим следующие темы:
- Как сохранить историю браузера с помощью маршрутизатора Sencha.
- Как отобразить несколько маркеров для каждого места на карте Google и, при выборе маркера, отобразить информационный пузырь с информацией о месте.
- Как показать полную информацию о каждом месте, их отзывы и как показать изображения мест в галерее стилей Pinterest.
Мы возобновим, где мы оставили в предыдущем посте.
1. Изменение макета
В первом посте приложение, которое мы разработали, было довольно простым по своей навигации, и навигационное представление Sencha идеально подходило для такого макета. Однако, хотя вы обнаружите, что вам нужно добавить дополнительные пользовательские элементы управления на свои панели инструментов, использование этого типа представления становится затруднительным, поскольку Sencha поддерживает панель навигации самостоятельно, а показ / скрытие элементов в ней увеличивает сложность.
Итак, по мере того, как мы продолжаем добавлять больше функциональности в приложение, нам нужно внести пару изменений в структуру. Сначала мы изменим основную панель на макет карты.
Main.js
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 (Контейнер карты Сенча)
Places.js
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’
}]
}
});
|
PlaceList.js
Контейнер 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.
PlaceMap.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
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’
}]
}]
}
});
|
Теперь в контроллер добавим три функции для создания и активации этих панелей. Я всегда предпочитаю иметь индивидуальную навигационную функцию для каждой страницы, которая отделяет навигацию от функциональной части.
Ссылки в Controller / App.js
Давайте перечислим все ссылки, которые нам нужны внутри контроллера.
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. Он возвращает пользователя на домашний экран.
showHome ()
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 внутри. Последняя часть необходима для того, чтобы каждый раз, когда вы попадете на страницу списка мест с домашней страницы.
showPlacesContainer ()
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());
}
|
showPlaceList () / showPlacesMap ()
Мы показываем / скрываем контейнеры 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’
}
|
Теперь вы сможете протестировать приложение в браузере и посмотреть, как работает изменение макета. Мы сделали с представлениями, связанными с несколькими местами. Скоро мы рассмотрим размещение маркеров карты, но перед этим я хочу рассказать о том, как мы можем поддерживать историю браузеров на этих одностраничных сайтах.
2. Вести историю браузера
При создании одностраничного веб-сайта становится абсолютно необходимым поддерживать историю браузера. Пользователи перемещаются назад и вперед на веб-сайте, и много раз проблемы с сетью или отсутствие ответа вынуждают пользователя снова перезагрузить страницу. В этих сценариях, если история не поддерживается, обновление страницы в конечном итоге вернет пользователя на первую страницу, что в некоторых случаях становится действительно раздражающим.
В этом разделе мы увидим, как мы можем использовать функциональность маршрутизации 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-запрос для получения данных о местах. На этот раз мы добавим туда немного для сохранения текущего типа категории в экземпляре контроллера, который понадобится в будущем. Также мы хотим отключить кнопку карты, пока все места не будут загружены. Мы хотим отобразить маркер, как только карта будет отрисована, и для этого места должны быть загружены первыми.
launch () / getPlaces ()
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, а не первую страницу.
3. Показать места на карте
Мы закончили со всеми видами, необходимыми для показа мест. Мы будем создавать и показывать контейнер 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».
showPlaceMarkers ()
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-адреса категории /: тип /: ссылка . Мы будем обрабатывать эту часть в другом контроллере, предназначенном для размещения деталей.
4. Детали места
Страница сведений о месте открывается, когда пользователь выбирает место из списка или с карты. Я классифицировал детали в 3 частях — информация, галерея изображений и обзоры. TabPanel больше всего подходит для такой раскладки. Давайте посмотрим, как могут быть структурированы виды:
Взгляды
- details.Main
- details.Info
- details.Gallery
- details.GalleryCarousel
- details.Review
контроллер
- PlaceDetails
Мы создаем другое подмножество пространства имен и помещаем все эти файлы в каталог «details» папки «view». Кроме того, мы создаем отдельный контроллер, который поддерживает только связанные с деталями функции. Не забудьте добавить все эти представления и контроллер в файл app.js.
Посмотреть / 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
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+ и карте с указанием только этого места.
Посмотреть / Details.Info
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.
Контроллер / 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 довольно проста, как мы использовали ранее:
getPlaceDetails ()
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.
loadPlaceDetails ()
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, отображает ее на панели «Информация» и отображает маркер для места внутри карты.
showPlaceLocation ()
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 используется здесь, чтобы получить мозаичный макет для изображений.
Посмотреть / Details.Gallery
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();
}
});
|
Посмотреть / Details.GalleryCarousel
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>
|
Вид / details.Review
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;
}
|
Используйте Pretty Date
Вместо того, чтобы просто показывать дату, мы предпочитаем дату в стиле 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 » в конец текста после многоточия.
applyExpandable ()
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. Давайте добавим метод, который заботится о том же.
handleReviewExpansion ()
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+.
5. Новая тема
Возможно, вы уже заметили, что я изменил тему из моей предыдущей версии приложения. Это потому, что фон приложения темный, а светлый цвет панели инструментов сделает его красивым. Кроме того, я использовал темные кнопки на панели инструментов для лучшей контрастности. Вот файл SASS, который я использовал:
app.scss
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 мы можем сделать приложение красивым и профессиональным. Обратите внимание, как мы структурировали приложение при запуске, использовали ленивый рендеринг компонентов, когда это было необходимо, уничтожали компонент, когда он не используется, и тщательно комментировали наш код. Эти вещи значительно улучшают производительность вашего приложения и стандарты кодирования. Удачного кодирования!