Как известно, современные телевизоры всё чаще имеют возможность выхода в Интернет. Это выражается не только в наличии встроенного браузера (ходить по сайтам с телевизора не слишком удобно), но и в возможности устанавливать приложения из специального "магазина", по аналогии с Android Market / App Store на смартфонах. В ряде телевизоров (LG, Samsung, некоторых других), интенсивно продаваемых сейчас в мире, функции
SmartTV реализованы сходным образом, на похожих аппаратных платформах.
Данная статья описывает разработку для телевизоров
LG SmartTV, в конце будет короткое сравнение с
Samsung SmartTV.
Итак, платформа LG SmartTV:
Broadcom BCM35230 MIPS32, 500 MHz. 512 MB RAM, 1GB Flash. Аппаратное декодирование видео.
Управление осуществляется одним из двух пультов - либо старым RCU, с традиционными цифровыми кнопками и стрелками, либо новым - так называемым Magic RCU, представляющим собой аналог манипулятора Nintendo Wii. В современных телевизорах в комплекте идут сразу оба пульта.
Крутится на всём этом Linux с Webkit браузером и Flash Lite 3.1 (Flash 8 с некоторыми особенностями). Можно считать устройство обычным компьютером, только очень медленным и с маленькой памятью. Нативные приложения разрабатывать нельзя - только Flash и Javascript.
ПРИЛОЖЕНИЕ
Click to view
Стояла задача разработать приложение Shoptimus (уже существовавшее для Android и IPhone) - некий интегратор интернет-магазинов, включающий каталог товаров с категориями, поиск товаров, фильтрацию результатов поиска, заказ товара. Серверная часть уже была реализована и отдавала данные в JSON.
Учитывая древность версии Flash и неторопливость платформы, для разработки был выбран HTML/CSS/JS. Использовался
Dojo. Фактически, процесс во многом аналогичен разработке сайта, только заточенного под конкретную платформу и поддерживающего два способа навигации.
Разрабатываемое приложение-сайт размещается на любом хостинге и во встроенном в телевизор специальном приложении указывается его URL (правда, в стандартном телевизоре это несколько не так, но примерная суть остаётся).
Существует SDK с эмулятором, но слишком нестабильный для практического применения. В основном, отладка происходила в Safari, как наиболее близкому по возможностям к встроенному браузеру LG SmartTV. (пульты RCU и Magic RCU с точки зрения Javascript выглядят совершенно идентично управлению с традиционных компьютерных клавиатуры и мыши соответственно).
Поскольку с телевизором работают не как с компьютером, обычные ссылки и контролы здесь неудобны и неуместны. Кроме того, дизайн изначально был определён заданием, поэтому в итоге было решено не использовать реализацию контролов которые LG предоставляет в комплекте с SDK, а написать свои (что, собственно, и стало наиболее сложным во всей работе - фактически пришлось писать собственный UI).
Логически экран был разделён на три части-контейнера - постоянное нижнее меню ("Назад", "Настройки", "Помощь", "Выход"), постоянное верхнее меню ("Предложения дня", "Каталог товаров", "Поиск"), информационная строка и центральный основной блок, где отображается текущий раздел.
Перечисленные четыре контейнера с точки зрения HTML - обычные
.
Программная часть реализована следующим образом:
ОСНОВНЫЕ КЛАССЫ
Model.js - различные общие и/или однократно загружаемые данные (регионы, категории каталога, история переходов между экранами, карточки продуктов, матрицы для навигации и пр.) и в конструкторе - получение и сохранение данных об устройстве и браузере.
Controller.js - методы для загрузки списка регионов, категорий каталога, товаров, предложений (через dojo.xhrPost). Методы для смены текущего View (Предложения, Каталог, Поиск, Настройки, Заказ), переход на предыдущее View и пр.
Keypad.js - навигация через кнопочный пульт RCU
ViewContentOffers, ViewContentCatalog, ViewContentCard, ViewContentOrder, ViewContentProducts, ViewContentSearch, ViewContentSettings, ViewContentWelcome - реализация, соответственно, разделов "Предложения дня", "Каталог товаров", "Карточка товара", "Заказ товара", "Список товаров", "Поиск товаров", "Настройки", "Стартовый экран нового пользователя"
CommonInput, CommonField, CommonDialog, CommonKeyboard, CommonList, CommonPopup, CommonTextArea - реализация UI контролов: различные кнопки, поля ввода, простой диалог с одной-двумя кнопками, экранная клавиатура с переключением языков, списки и текстовые поля с гладким скроллингом, попапы.
В index.html создаются экземпляры Keypad, Model, Controller, из web storage достаются настройки текущего пользователя (через dojox.storage), на сервер отправляется общая статистика.
Последним действием либо (для нового пользователя) осуществляется переход к экрану выбора региона, либо "предложений дня".
ЛОГИКА
Каждое View состоит из двух файлов - html и js (а также css, при необходимости). В html файле находится статическая часть - обычно пустой div и, иногда, некоторые постоянные элементы управления, типа стрелок для скроллинга:
js файл (собственно, класс) выглядит примерно следующим образом:
dojo.provide('classes.ViewContentOffers');
dojo.require('dijit._Widget');
dojo.require('dijit._Templated');
dojo.declare('classes.ViewContentOffers', [dijit._Widget, dijit._Templated],
{
templatePath: dojo.moduleUrl('classes', 'ViewContentOffers.html'), // html файл со статическим контентом
widgetsInTemplate: false,
isContainer: false,
eventsConnected : null, // обработчики событий, для последующей их очистки
eventsSubscribed : null, // обработчики событий, для последующей их очистки
...
constructor: function(params)
{
...
}//constructor
postCreate: function()
{
.. подписки на события, создание матрицы для кнопочной навигации, ...
},//postCreate
_onKeyClick: function(evt)
{
... Обработка нажатия на кнопки ...
},//_onKeyClick
_updateNavigation: function()
{
... Обновление navMatrix после изменений элементов в контейнере ...
},//_updateNavigation
_onClick...: function (evt)
{
... реакция на нажатие какого-либо элемента...
}
...
// Вызывать после окончания работы view
kill: function()
{
...
}//kill
});//declare()
Переходы между экранами происходят так:
1.генерируем событие для смены View:
dojo.publish("EVENT_ViewChange", [{item:'itemOffers'}]);
2.на это событие подписан Controller:
dojo.subscribe("EVENT_ViewChange", this, this._onViewChange);
3.в результате в нём вызывается _onViewChange(), где происходят следующие действия:
а) Из стека model.contentHistory достаётся предыдущий экземпляр View
б) Вызывается viewPrev.instance.kill(), который уничтожает все следы предыдущего экземпляра View.
kill() делает две вещи: прибивает все events созданные в данном View через connect и subscribe.
Поскольку в dojo это не отслеживается автоматически, всё приходится вручную сохранять и потом чистить:
this.eventsSubscribed.push(dojo.subscribe("EVENT_OffersLoaded", this, this._draw));
this.eventsConnected.push(dojo.connect(itemNode, "onmouseenter", this, function .. ());
Т.е. в kill() делаем:
dojo.forEach(this.eventsConnected, function(event)
{
dojo.disconnect(event);
}, this);
dojo.forEach(this.eventsSubscribed, function(event)
{
dojo.unsubscribe(event);
}, this);
при необходимости, прибиваем созданные контролы:
this.viewShopsButton.kill();
this.viewBrandsButton.kill();
this.viewPricesButton.kill();
и наконец:
this.destroyRecursive();
в) Загружается новый _viewLoad(params), внутри которого создаётся экземпляр View, с передачей в его конструктор необходимых параметров (id товара, категории и пр.)
var viewParams = {item:params.item,path:'PViewContentOffers/'};
this.viewInstance = new classes.ViewContentOffers(viewParams);
Созданный экземпляр помещается в соответствующий div:
this.viewInstance.placeAt('ui-content', 'only');
г) Новый View помещается в стек model.contentHistory, вместе с параметрами (для кнопки "Назад")
СУЩЕСТВЕННЫЕ МОМЕНТЫ
Контролы Common*
Каждый элемент управления (к примеру, кнопка) представляет собой класс. При создании экземпляра ему передаются различные характеристики и метод, который будет вызван после клика
this.viewShopsButton = new classes.CommonInput( {path:this.path+'PCommonInput/ci_Shops',left:'80', top:'420', width:'403', height:'60' ,type:'standard',buttonStr:'Все магазины', subtype:'select', selected: false, onclick: this._onClickFilterShop, context: this});
Созданный экземпляр помещается в зарезервированный для этого (в текущем контейнере)
:
this.viewShopsButton.placeAt(this.apFilters, 'after');
Для кнопок реализованы методы get(), put(), disable(), enable() и обрабатываются наведение мыши _onEnter(), _onLeave и клик - _onClick(). Выключенные кнопки имеют другой цвет и не реагируют на нажатие.
Очень существенно, что по требованиям LG необходимо было поддерживать не только MRCU (мышь), но и старый RCU (кнопочный пульт). Другими словами, интерфейс должен полностью поддерживать управление при помощи кнопок-стрелок и кнопки ENTER. Этого момента стоит коснуться подробнее..
Кнопочная навигация
Навигация при помощи курсоров существенно отличается от мышиной тем, что должно существовать понятие текущего элемента, на котором мы в данный момент находимся и соседних, на которые возможен переход.
Причём, если рассматривать даже простейший экран, всегда существуют как минимум три контейнера (верхнее меню, нижнее меню, текущий раздел) которые являются разными объектами, но между всеми этими кнопками и пунктами меню мы должны свободно перемещаться. А ведь существуют ещё ситуации, когда внутри раздела есть скролящиеся списки со стрелками, не говоря уже об отдельных попапах.
Задача была решена следующим образом:
- Все нажатия кнопок пульта (четыре стрелки, ENTER, BACK, EXIT) централизованно перехватываются в classes.Keypad через dojo.connect(dojo.body(), "onkeydown", function(evt) { ... }.
BACK и EXIT обрабатываются отдельно, а для стрелок и ENTER генерируется событие
dojo.publish("EVENT_KeyPress", [{keyCode:keyCode}]);
- На это событие подписаны все объекты, которые либо могут содержать контролы (в этом случае информация о нажатии передаётся далее по иерархии), либо сами контролами являются. В каждом из них это событие запускает _onKeyClick(), в котором:
а) проверяется, соответствует ли глобальный keypad.currentKeyzone.container этому объекту - т.е. надо ли конкретно ему обрабатывать это нажатие (keypad.currentKeyzone.container меняется при переходе стрелками между контейнерами - например, от верхнего или нижнего меню к текущему разделу).
б) если мы перешли из другого контейнера (например, из одного из пунктов верхнего меню к карточками товаров) выясняется, на какую из карточек мы должны попасть. Ведь вместо карточек товаров текущим разделом может оказаться, например, каталог категорий товаров. Кроме того, их может быть любое количество и расположены они могут быть по-разному.
Чтобы как-то учесть все возможные ситуации и обеспечить осмысленность перемещений, при переходе к новому контейнеру передаётся информация о том, где примерно был фокус на предыдущем контейнере (left, center, right).
Соответственно, keypad.nodeByAlign(left,center,right) выбирает, на какой элемент попадём.
в) теперь надо определить, какой элемент (в соответствии с переданным кодом кнопки) станет следующим, подсветить этот элемент и снять подсветку с предыдущего. Вызываем keypad.processKeyPress(evt.keyCode, this.navMatrix, this, this._updateNavigation);
(причём, если в результате мы выйдем за край контейнера, нужно переключить фокус на соседний).
Наиболее существенное здесь - определение следующего элемента внутри текущего контейнера. Поскольку, как сказано выше, может быть несколько уровней вложенности элементов, при создании каждого экземпляра ему передаётся идентификатор предыдущего. Таким образом, формируется некий путь до любого элемента - вида "PViewContentProducts/cardItem-1254844".
После того, как сформирован весь раздел, расставлены все кнопки, в глобальном model.navPaths накапливается массив таких путей.
По этому массиву строится матрица this.navMatrix для навигации внутри каждого конкретного контейнера.
Определение, какой элемент находится справа, слева, сверху, снизу от текущего осуществляется в разных случаях по-разному - если это статически размещенные кнопки, их взаимное расположение известно заранее. Если подгружаются карточки товаров или категорий, оно определяется после.
В ситуациях типа скроллинга с подгрузкой дополнительных порций данных, в нужные моменты вызывается this.updateNavigation и матрица перестраивается заново.
В самых простых случаях она может быть статический. Например:
this.navMatrix = [
{current : this.path + 'butMainLeft', next : [this.path + 'butMainRight',card_1,'ViewMenuTop,left','ViewMenuBottom,left']},
{current : card_1, next : [this.path + 'butMainLeft',card_2,'ViewMenuTop,left','ViewMenuBottom,left']},
{current : card_2, next : [card_1,card_3,'ViewMenuTop,left','ViewMenuBottom,left']},
{current : card_3, next : [card_2,this.path + 'butMainRight','ViewMenuTop,left','ViewMenuBottom,left']},
{current : this.path + 'butMainRight', next : [card_3,this.path + 'butMainLeft','ViewMenuTop,left','ViewMenuBottom,left']}
];
("*,left" - переходы в соседние контейнеры, null - запрет перехода в этом направлении)
Подсветка элементов реализована программно, путём навешивания событий на вызовы функции this._hilite() , которой передаётся действие (ON,OFF,CLICK) и путь к элементу который должен быть подсвечен, погашен или нажат. Далее в ней проверяется допустимость данного действия для этого контрола (enabled/disabled) и затем меняется его стиль.
В ситуации нажатия эмулируется клик мышью на элементе. Функция выглядит так:
simulateClick : function(el)
{
var evt;
if (document.createEvent)
{
evt = document.createEvent("MouseEvents");
evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
el.dispatchEvent(evt);
}
else
if (el.fireEvent)
{
el.fireEvent('onclick');
}//if
},//simulateClick()
Также при обновлениях списков, переходах между разделами и т.п., массив путей чистится по маске (т.е., к примеру, вызывается keypad.pathDelete() для всех путей? начинающихся на "PVIewContentOffers/").
Попапы
Попапы используются в немногих случаях - для выдачи важных сообщений, выбора значения из списка, показа экранной клавиатуры при редактировании полей ввода.
Содержимое попапа помещается в отдельный div ui-popup, находящийся на переднем плане (через z-index). Под ним находится div ui-underlay, представляющий собой полупрозрачный png размером во весь экран, который полностью затемняет интерфейс (кроме попапа находящегося перед ним) и, дополнительно, при клике вне попапа вызывает событие попап закрывающее.
При показе попапа обоим div'ами делается display: block, а при закрытии display: none. Вложенные попапы не поддерживаются.
Для создания попапа в качестве параметра методу передаются характеристики объекта, который надо будет в него поместить:
var content = {classname:'CommonTextArea', path: 'PCommonPopup/PCommonTextArea/',data:helpText,width:'440',height:'454',left:'10',top:'8',bheight:60};
controller.popupShow({path:'', width:505, height:467, left:380, top: 140, content: content},'ViewMenuBottom,left');
Соответственно внутри происходит
this.popupInstance = new classes.CommonPopup(content);
this.popupInstance.placeAt('ui-popup', 'only');
А внутри classes.CommonPopup:
this.viewContent = new classes.CommonTextArea(this.content);
При закрытии попапа вызовом controller.popupHide() всем экземплярам последовательно делается kill() и текущим становится контейнер, указанный ранее при вызове popupShow().
Загрузка данных
Сервер отдаёт данные в виде JSON. Для их получения используется dojo.xhrPost(), после получения данных генерируется событие, на которое подписаны объекты эти данные получающие.
Вопрос с кроссдоменностью решается при помощи
CORS (сервер выдаёт соответствующие http заголовки).
Все операции происходят асинхронно что, естественно, приводит к неудобствам - результат одного запроса нужно использовать в качестве данных для следующего. Это решается при помощи dojo Deferred/Promise, а конкретнее - .then().
Вот пример трёх асинхронных запросов, где результат каждого предыдущего используется в качестве параметра последующего:
// Загрузка данных по одному товару
fetchCardOffers: function(itemId,cbEvent)
{
controller.progressBar(true);
// Получаем информацию по этому конкретному product
var tmpObj =
{
method: "api.getProducts",
region_id: model.currentRegionCity.id,
product_ids: itemId,
token: model.token,
user_token: model.userToken,
device_name: model.deviceName,
screen_size: model.screenSize,
dpi: model.dpi,
os: model.os
};
var jsonpArgs =
{
url: model.api_url,
callbackParamName: "callback",
postData: 'json=' + dojo.toJson(tmpObj),
timeout: model.timeout,
handleAs: 'json'
};
var def1 = dojo.xhrPost(jsonpArgs);
// в response общие данные по product
def1.then(function(response)
{
model.cardStoreProduct =
{
type: 'product',
title : response.data.items[0].title,
brand_title : response.data.items[0].brand_title,
parameters : utils._toString2(response.data.items[0].parameters),
min_price : response.data.items[0].min_price,
max_price : response.data.items[0].max_price,
image : response.data.items[0].images[0]
};
var tmpObj =
{
method: "api.getOffers",
region_id: model.currentRegionCity.id,
product_id: itemId,
token: model.token,
user_token: model.userToken,
device_name: model.deviceName,
screen_size: model.screenSize,
dpi: model.dpi,
os: model.os
};
var jsonpArgs =
{
url: model.api_url,
callbackParamName: "callback",
postData: 'json=' + dojo.toJson(tmpObj),
timeout: model.timeout,
handleAs: 'json'
};
// Получаем список offers для данного product
var def2 = dojo.xhrPost(jsonpArgs);
// в response.items массив offers. Среди прочего по каждому offer там есть shop_id. Их собираем и передаем в api.getShops
def2.then(function(response)
{
var offers = response.data.items;
// выделяем отдельно массив id магазинов
var shop_ids = dojo.map(response.data.items,
function(item)
{
return item.shop_id;
});
var tmpObj =
{
method: "api.getProductShops",
region_id: model.currentRegionCity.id,
shop_ids: shop_ids,
token: model.token,
user_token: model.userToken,
device_name: model.deviceName,
screen_size: model.screenSize,
dpi: model.dpi,
os: model.os
};
var jsonpArgs =
{
url: model.api_url,
callbackParamName: "callback",
postData: 'json=' + dojo.toJson(tmpObj),
timeout: model.timeout,
handleAs: 'json'
};
// после удачного выполнения getOffers вызываем getShops (для всего списка магазинов, сразу для всех offer)
var def3 = dojo.xhrPost(jsonpArgs);
// В response.data.items информация по магазинам, в model.cardStoreOffers - по offer'ам
def3.then(function(response)
{
// теперь надо добавить в model.cardStoreOffers информацию по магазинам из response.data.items
model.cardStoreOffers = dojo.map(offers,
function(item)
{
var shopInfo = dojo.filter(response.data.items,function(itemShop)
{
return itemShop.id == item.shop_id;
});
return { // формируем конечный массив из которого будет показан список предложений
id:item.id,
title:item.title,
brand_title:item.brand_title,
image:item.image,
price:item.price + ' р.',
url:item.url,
shop_delivery_code: shopInfo[0].delivery_code,
shop_delivery_teaser: shopInfo[0].delivery_teaser,
shop_delivery_text: shopInfo[0].delivery_text,
shop_vote_rating: shopInfo[0].vote_rating,
shop_user_vote_rating: shopInfo[0].user_vote_rating,
shop_user_vote_text: shopInfo[0].user_vote_text,
shop_title: shopInfo[0].title,
shop_phone: shopInfo[0].phone,
shop_id: shopInfo[0].id,
shop_body: shopInfo[0].body,
shop_image: shopInfo[0].image
};
});
controller.progressBar(false);
dojo.publish(cbEvent); // сообщаем ViewContentCard что данные готовы
},function(err)
{
// ... error processing ...
});//def3.then
},function(err)
{
// ... error processing ...
});//def2.then
},function(err)
{
// ... error processing ...
});//def1.then
},//fetchCardOffers()
Индикация процесса загрузки
Хотя в LG SmartTV предусмотрены некоторые средства для индикации обращения к сети, работают они только для случаев непосредственной загрузки страниц. Поэтому для Ajax запросов в любом случае нужно собственное решение. Реализация аналогична попапам - скрытый (пока он не нужен) div, расположенный перед всеми остальными, в центре которого находится анимационный GIF, изображающий процесс загрузки.
Про скроллинг
По сравнению со скачкообразным по-элементным скроллингом, гладкий [почти] попиксельный радикально меняет впечатление пользователя от работы с приложением, тем более на экране телевизора. Этот то, что определённо стоит дополнительных усилий.
Существенным препятствием была низкая производительность платформы. Т.е. скроллинг больших фрагментов с использованием прозрачности или фиксированным фоном будет происходить рывками, если не сказать хуже. Поэтому области приходилось ограничивать, вместо прозрачности использовать частично прозрачные png.
В dojo оказалась хорошая (хотя и экспериментальная) функция dojox.fx.smoothScroll().play(). Не вся заявленная в документации функциональность в ней реализована, но скроллинг до указанного узла на телевизоре получался вполне гладкий и равномерный (кстати, в отличие от несколько опробованных перед этим JQuery плагинов).
С переходом к гладкому скроллингу некоторые решения сильно усложняются.
Возьмём любой список. Если при каждом нажатии на кнопку пульта скроллить его гладко но построчно, пользователь устанет уже после нескольких нажатий. А списки бывают и длинные. Соответственно, скроллить надо по несколько (пять) строк. При этом усложняется логика, особенно что касается кнопочной навигации.
Или взять случай со скроллингом текста в окне (описание товара, помощь и т.п.). Упомянутая функция обеспечивает только скроллинг до нужного DOM узла. А текст не имеет внутри себя никаких равномерно распределённых по нему меток. Пришлось пойти на некоторое извращение:
Сначала div заполняется текстом. Измеряется высота получившегося растянутого div'a (свойство scrollHeight которое, кстати, доступно не мгновенно). Затем справа от div'a с текстом формируем невидимую табличку с ячейками, количество которых зависит от высоты div'a. Каждая ячейка имеет свой id, к которому мы уже и можем спокойно скроллить.
В завершении истории про скроллинг упомяну интересный баг LG браузера. Если внутри скроллируемого div'a есть элементы со стилем *-border-radius (скруглённые углы), скроллинг этого div'a начинает сильно тормозить. Невероятно, но факт. Можете себе представить, сколько времени ушло на выяснение, что именно это является причиной проблемы :) В итоге, часть элементов со скруглениями пришлось сделать картинками.
Экранная клавиатура
Для ввода названия товара (при поиске) и редактирования настроек реализована полноценная экранная клавиатура с поддержкой английского и русского языка. Из вариантов QWERTY (ЙЦУКЕН) и ABCDE (АБВГД) был выбран второй.
Класс CommonKeyboard эмулирует функции стандартного INPUT - ввод символов, перемещение курсора влево, вправо, в начало, в конец, удаление символа, строки.
Раскладка для каждого языка задаётся массивом вида:
keybTableRus : [
['Hm','Lt','Rt','Ed','Bs','Cl',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','Eng'],
['1','2','3','4','5','6','7','8','9','0','.','@','#','_','-','/','+'],
['А','Б','В','Г','Д','Е','Ё','Ж','З','И','Й','К','Л','М','Н','О',' '],
['П','Р','С','Т','У','Ф','Х','Ц','Ч','Ш','Щ','Ь','Ы','Ъ','Э','Ю','Я']
],
и при необходимости легко может быть изменена, а число языков - увеличено.
Разное
Существует ряд JS функций, специфичных именно для LG платформы.
Так например, характеристики устройства можно узнать следующим образом (помимо обычного user agent'a):
....
var device = document.getElementById("device");
this.device.serialNumber = device.serialNumber; // уникален для данного телевизора
this.device.version = device.version;
this.device.manufacturer = device.manufacturer;
this.device.modelName = device.modelName;
this.device.swVersion = device.swVersion;
this.device.hwVersion = device.hwVersion;
this.device.osdResolution = device.osdResolution;
this.device.networkType = device.networkType;
this.device.net_macAddress = device.net_macAddress;
this.device.net_dhcp = device.net_dhcp;
this.device.net_isConnected = device.net_isConnected;
this.device.net_hasIP = device.net_hasIP;
this.device.net_ipAddress = device.net_ipAddress;
this.device.net_netmask = device.net_netmask;
this.device.net_gateway = device.net_gateway;
this.device.net_dns1 = device.net_dns1;
this.device.supportMouse = device.supportMouse;
this.device.supportPortalKey = device.supportPortalKey;
Выход из приложения на главный экран:
window.NetCastExit();
Для увеличения скорости загрузки и повышения производительности javascript'a в перспективе можно воспользоваться
dojo builder'ом.
ЗАМЕЧАНИЯ И ВЫВОДЫ
- Нормально отлаживать достаточно сложное приложение интенсивно использующее dojo - очень проблематично. Либо надо писать на чистом js, либо обходиться вообще без ошибок :)
- Если не используется dojo ui, то custom dojo widgets не стоит применять - весь контент лучше создавать динамически. Иначе в неожиданных местах вылезают странные глюки, с которыми неясно как разбираться.
- Полностью создавать свой UI с нуля не следовало. Несмотря на красоты, это [предсказуемо] оказывается основной долей трудозатрат в разработке приложения.
- Учитывая низкую производительность платформы, приходилось балансировать между логически красивыми решениями и их эффективностью. В ряде случаев пришлось избегать иерархий объектов, дублировать код и пр.
- Несмотря на то, что такая задача не ставилась, приложение практически одинаково выглядит и работает не только в браузере LG , но и в свежих версиях десктопных Safari, Chrome и FireFox. При предварительных попытках запустить его на Samsung SmartTV (с браузером Maple) все основные функции также заработали после минимальных доработок (насколько это можно было понять, учитывая другое разрешение экрана).
- После разработок на Flex'e от Javascript'a ощущение такое, будто вернулся в 1990-е. То, что на первом поставлен крест, а на втором пишут всё больше - весьма характеризует IT индустрию (точнее, способность крупных компаний договариваться между собой).
О SAMSUNG SmartTV
В завершении, несколько слов о Samsung SmartTV. По производительности платформа аналогична LG, но есть серьезный недостаток - навигация возможна только при помощи обычных кнопок - стрелок.
Кроме того, в качестве браузера используется Maple (устройства 2010 и 2011 года) и стандартное разрешение для приложений ограничено 960x540. В скором времени ожидается апгрейд - браузером будет Webkit (устройства 2012 г.) и разрешение станет 1280x720. Зато, по сравнению с LG уже сейчас реализован довольно серьезный API, позволяющий использовать свойства самого телевизора (например, накладывать приложение на эфирную трансляцию).
В отличие от LG работа без установленного SDK (TV Apps Editor) и Apache невозможна - приложение после каждого изменения необходимо загружать в телевизор, что является весьма утомительной и небыстрой операцией.
Эмулятор работает неустойчиво.
ССЫЛКИ
http://developer.lgappstv.com/devel/main/main.lge - LG Apps TV Developer Longe
http://www.samsungdforum.com - Samsgung Smart TV SDK и форум
http://smarttvnotes.wordpress.com - чей-то блог про Samsung SmartTV