Статьи

Создайте AudioPlayer с PhoneGap: логика приложения

Это вторая часть серии про Audero Audio Player . В этой статье мы собираемся создать бизнес-логику нашего игрока. Я также объясню некоторые API Cordova, которые были представлены в предыдущей статье .



В этом разделе я покажу вам класс Player , который позволяет нам играть, останавливать, перематывать и перематывать вперед. Класс сильно зависит от Media API; без его методов наш игрок будет совершенно бесполезен. В дополнение к Media API этот класс использует метод alert() API уведомлений . Внешний вид оповещения варьируется в зависимости от платформы. Большинство поддерживаемых операционных систем используют собственное диалоговое окно, но другие, такие как Bada 2.X, используют классическую функцию браузера alert() , которая менее настраиваема. Первый метод принимает до четырех параметров:

  1. сообщение : строка, содержащая сообщение, чтобы показать
  2. alertCallback : обратный вызов для вызова при закрытии диалогового окна предупреждения.
  3. title : заголовок диалогового окна (значение по умолчанию «Alert»)
  4. buttonName : текст кнопки, включенный в диалог (значение по умолчанию «ОК»)

Имейте в виду, что Windows Phone 7 игнорирует имя кнопки и всегда использует значение по умолчанию. Windows Phone 7 и 8 не имеют встроенного оповещения браузера, поэтому, если вы хотите использовать alert('message'); , вы должны назначить window.alert = navigator.notification.alert .

Теперь, когда я объяснил API, используемые Player , мы можем взглянуть на то, как он сделан. У нас есть три свойства:

  • media : ссылка на текущий звуковой объект
  • mediaTimer : который будет содержать уникальный идентификатор интервала, созданный с помощью функции setInterval() который мы передадим в clearInterval() чтобы остановить таймер звука
  • isPlaying : переменная, которая указывает, воспроизводится ли текущий звук или нет. В дополнение к свойству у класса есть несколько методов.

Метод initMedia() инициализирует свойство Media объектом Media который представляет звук, выбранный пользователем. Последний уведомляется с помощью API уведомлений в случае ошибки. Цель методов playPause , stop() и seekPosition() должна быть очевидна, поэтому я буду двигаться дальше. resetLayout() и changePlayButton() очень просты. Они используются для сброса или обновления макета проигрывателя в соответствии с действием, выполненным пользователем. Последний оставшийся метод — updateSliderPosition() , аналогичный ползунку времени. Последний имеет ноль (начало ползунка) в качестве значения по умолчанию для текущей позиции, установленного с помощью атрибута value="0" . Это должно быть обновлено соответствующим образом во время воспроизведения звука, чтобы дать пользователю визуальную обратную связь относительно истекшего времени воспроизведения.

Мы раскрыли все детали этого класса, поэтому вот исходный код файла:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
var Player = {
   media: null,
   mediaTimer: null,
   isPlaying: false,
   initMedia: function(path) {
      Player.media = new Media(
         path,
         function() {
            console.log(‘Media file read succesfully’);
            if (Player.media !== null)
               Player.media.release();
            Player.resetLayout();
         },
         function(error) {
            navigator.notification.alert(
               ‘Unable to read the media file.’,
               function(){},
               ‘Error’
            );
            Player.changePlayButton(‘play’);
            console.log(‘Unable to read the media file (Code): ‘ + error.code);
         }
      );
   },
   playPause: function(path) {
      if (Player.media === null)
         Player.initMedia(path);
 
      if (Player.isPlaying === false)
      {
         Player.media.play();
         Player.mediaTimer = setInterval(
            function() {
               Player.media.getCurrentPosition(
                  function(position) {
                     if (position > -1)
                     {
                        $(‘#media-played’).text(Utility.formatTime(position));
                        Player.updateSliderPosition(position);
                     }
                  },
                  function(error) {
                     console.log(‘Unable to retrieve media position: ‘ + error.code);
                     $(‘#media-played’).text(Utility.formatTime(0));
                  }
               );
            },
            1000
         );
         var counter = 0;
         var timerDuration = setInterval(
            function() {
               counter++;
               if (counter > 20)
                  clearInterval(timerDuration);
 
               var duration = Player.media.getDuration();
               if (duration > -1)
               {
                  clearInterval(timerDuration);
                  $(‘#media-duration’).text(Utility.formatTime(duration));
                  $(‘#time-slider’).attr(‘max’, Math.round(duration));
                  $(‘#time-slider’).slider(‘refresh’);
               }
               else
                  $(‘#media-duration’).text(‘Unknown’);
            },
            100
         );
 
         Player.changePlayButton(‘pause’);
      }
      else
      {
         Player.media.pause();
         clearInterval(Player.mediaTimer);
         Player.changePlayButton(‘play’);
      }
      Player.isPlaying = !Player.isPlaying;
   },
   stop: function() {
      if (Player.media !== null)
      {
         Player.media.stop();
         Player.media.release();
      }
      clearInterval(Player.mediaTimer);
      Player.media = null;
      Player.isPlaying = false;
      Player.resetLayout();
   },
   resetLayout: function() {
      $(‘#media-played’).text(Utility.formatTime(0));
      Player.changePlayButton(‘play’);
      Player.updateSliderPosition(0);
   },
   updateSliderPosition: function(seconds) {
      var $slider = $(‘#time-slider’);
 
      if (seconds < $slider.attr(‘min’))
         $slider.val($slider.attr(‘min’));
      else if (seconds > $slider.attr(‘max’))
         $slider.val($slider.attr(‘max’));
      else
         $slider.val(Math.round(seconds));
 
      $slider.slider(‘refresh’);
   },
   seekPosition: function(seconds) {
      if (Player.media === null)
         return;
 
      Player.media.seekTo(seconds * 1000);
      Player.updateSliderPosition(seconds);
   },
   changePlayButton: function(imageName) {
      var background = $(‘#player-play’)
      .css(‘background-image’)
      .replace(‘url(‘, »)
      .replace(‘)’, »);
 
      $(‘#player-play’).css(
         ‘background-image’,
         ‘url(‘ + background.replace(/images\/.*\.png$/, ‘images/’ + imageName + ‘.png’) + ‘)’
      );
   }
};

В этом разделе показан класс AppFile который будет использоваться для создания, удаления и загрузки звуков с помощью API-интерфейса веб-хранилища . Этот API имеет две области, Session и Local , но Cordova использует последнюю. Все звуки хранятся в элементе под названием «файлы», как вы можете видеть, просматривая свойства _tableName .

Обратите внимание, что этот API-интерфейс может хранить только основные данные. Поэтому, чтобы удовлетворить наши потребности в хранении объектов, мы будем использовать формат JSON. JavaScript имеет класс для работы с этим форматом, который называется JSON. Он использует методы parse() для анализа строки и воссоздания соответствующих данных, а также stringify() для преобразования объекта в строку. В заключение, я не буду использовать точечную нотацию API, потому что Windows Phone 7 не поддерживает ее, поэтому мы будем использовать методы setItem() и getItem() для обеспечения совместимости для всех устройств.

Теперь, когда у вас есть обзор того, как мы будем хранить данные, давайте поговорим о данных, которые нам нужно сохранить. Единственная информация, которая нам нужна для каждого найденного звука — это имя (свойство name ) и абсолютный путь (свойство fullPath ). Класс AppFile также имеет «константу», называемую EXTENSIONS , где мы устанавливаем расширения, которые будут проверяться для каждого файла. Если они совпадают, файл будет собран приложением. У нас есть метод для добавления файла ( addFile() ), один метод для удаления файла ( deleteFile() ), один метод для удаления всей базы данных ( deleteFiles() ) и, наконец, два метода, которые извлекают файл из база данных: getAppFiles() чтобы получить все файлы, и getAppFile() чтобы получить только один. В классе также есть четыре метода сравнения: два статических ( compare() и compareIgnoreCase() ) и два нестатических ( compareTo() и compareToIgnoreCase() ). Последний метод используется для получения индекса определенного файла, getIndex() . Класс AppFile позволяет вам выполнять все основные операции, которые вам могут понадобиться.

Код, который реализует то, что мы обсуждали, можно прочитать здесь:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
function AppFile(name, fullPath)
{
   var _db = window.localStorage;
   var _tableName = ‘files’;
 
   this.name = name;
   this.fullPath = fullPath;
 
   this.save = function(files)
   {
      _db.setItem(_tableName, JSON.stringify(files));
   }
 
   this.load = function()
   {
      return JSON.parse(_db.getItem(_tableName));
   }
}
 
AppFile.prototype.addFile = function()
{
   var index = AppFile.getIndex(this.fullPath);
   var files = AppFile.getAppFiles();
 
   if (index === false)
      files.push(this);
   else
      files[index] = this;
 
   this.save(files);
};
 
AppFile.prototype.deleteFile = function()
{
   var index = AppFile.getIndex(this.fullPath);
   var files = AppFile.getAppFiles();
   if (index !== false)
   {
      files.splice(index, 1);
      this.save(files);
   }
 
   return files;
};
 
AppFile.prototype.compareTo = function(other)
{
   return AppFile.compare(this, other);
};
 
AppFile.prototype.compareToIgnoreCase = function(other)
{
   return AppFile.compareIgnoreCase(this, other);
};
 
AppFile.EXTENSIONS = [‘.mp3’, ‘.wav’, ‘.m4a’];
 
AppFile.compare = function(appFile, other)
{
   if (other == null)
      return 1;
   else if (appFile == null)
      return -1;
 
   return appFile.name.localeCompare(other.name);
};
 
AppFile.compareIgnoreCase = function(appFile, other)
{
   if (other == null)
      return 1;
   else if (appFile == null)
      return -1;
 
   return appFile.name.toUpperCase().localeCompare(other.name.toUpperCase());
};
 
AppFile.getAppFiles = function()
{
   var files = new AppFile().load();
   return (files === null) ?
};
 
AppFile.getAppFile = function(path)
{
   var index = AppFile.getIndex(path);
   if (index === false)
      return null;
   else
   {
      var file = AppFile.getAppFiles()[index];
      return new AppFile(file.name, file.fullPath);
   }
};
 
AppFile.getIndex = function(path)
{
   var files = AppFile.getAppFiles();
   for(var i = 0; i < files.length; i++)
   {
      if (files[i].fullPath.toUpperCase() === path.toUpperCase())
         return i;
   }
 
   return false;
};
 
AppFile.deleteFiles = function()
{
   new AppFile().save([]);
};

Файл utility.js очень короткий и понятный. У него есть только два метода. Один из них используется для преобразования миллисекунд в форматированную строку, которая будет отображаться в проигрывателе, а другой — реализацию JavaScript хорошо известного метода Java endsWith .

Вот источник:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
var Utility = {
   formatTime: function(milliseconds) {
      if (milliseconds <= 0)
         return ’00:00′;
 
      var seconds = Math.round(milliseconds);
      var minutes = Math.floor(seconds / 60);
      if (minutes < 10)
         minutes = ‘0’ + minutes;
 
      seconds = seconds % 60;
      if (seconds < 10)
         seconds = ‘0’ + seconds;
 
      return minutes + ‘:’ + seconds;
   },
   endsWith: function(string, suffix) {
      return string.indexOf(suffix, string.length — suffix.length) !== -1;
   }
};

В этом разделе обсуждается последний файл JavaScript проекта, application.js , который содержит класс Application . Его цель — прикрепить события к элементам страницы приложения. Эти события будут использовать преимущества классов, которые мы видели до сих пор, и позволят игроку работать должным образом.

Код показанной функции приведен ниже:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
var Application = {
   initApplication: function() {
      $(document).on(
         ‘pageinit’,
         ‘#files-list-page’,
         function()
         {
            Application.initFilesListPage();
         }
      );
      $(document).on(
         ‘pageinit’,
         ‘#aurelio-page’,
         function()
         {
            Application.openLinksInApp();
         }
      );
      $(document).on(
         ‘pagechange’,
         function(event, properties)
         {
            if (properties.absUrl === $.mobile.path.makeUrlAbsolute(‘player.html’))
            {
               Application.initPlayerPage(
                  JSON.parse(properties.options.data.file)
               );
            }
         }
      );
   },
   initFilesListPage: function() {
      $(‘#update-button’).click(
         function()
         {
            $(‘#waiting-popup’).popup(‘open’);
            setTimeout(function(){
               Application.updateMediaList();
            }, 150);
         }
      );
      $(document).on(‘endupdate’, function(){
         Application.createFilesList(‘files-list’, AppFile.getAppFiles());
         $(‘#waiting-popup’).popup(‘close’);
      });
      Application.createFilesList(‘files-list’, AppFile.getAppFiles());
   },
   initPlayerPage: function(file) {
      Player.stop();
      $(‘#media-name’).text(file.name);
      $(‘#media-path’).text(file.fullPath);
      $(‘#player-play’).click(function() {
         Player.playPause(file.fullPath);
      });
      $(‘#player-stop’).click(Player.stop);
      $(‘#time-slider’).on(‘slidestop’, function(event) {
         Player.seekPosition(event.target.value);
      });
   },
   updateIcons: function()
   {
      if ($(window).width() > 480)
      {
         $(‘a[data-icon], button[data-icon]’).each(function() {
            $(this).removeAttr(‘data-iconpos’);
         });
      }
      else
      {
         $(‘a[data-icon], button[data-icon]’).each(function() {
            $(this).attr(‘data-iconpos’, ‘notext’);
         });
      }
   },
   openLinksInApp: function()
   {
      $(«a[target=\»_blank\»]»).on(‘click’, function(event) {
         event.preventDefault();
         window.open($(this).attr(‘href’), ‘_target’);
      });
   },
   updateMediaList: function() {
      window.requestFileSystem(
         LocalFileSystem.PERSISTENT,
         0,
         function(fileSystem){
            var root = fileSystem.root;
            AppFile.deleteFiles();
            Application.collectMedia(root.fullPath, true);
         },
         function(error){
            console.log(‘File System Error: ‘ + error.code);
         }
      );
   },
   collectMedia: function(path, recursive, level) {
      if (level === undefined)
         level = 0;
      var directoryEntry = new DirectoryEntry(», path);
      if(!directoryEntry.isDirectory) {
         console.log(‘The provided path is not a directory’);
         return;
      }
      var directoryReader = directoryEntry.createReader();
      directoryReader.readEntries(
         function (entries) {
            var appFile;
            var extension;
            for (var i = 0; i < entries.length; i++) {
               if (entries[i].name === ‘.’)
                  continue;
 
               extension = entries[i].name.substr(entries[i].name.lastIndexOf(‘.’));
               if (entries[i].isDirectory === true && recursive === true)
                  Application.collectMedia(entries[i].fullPath, recursive, level + 1);
               else if (entries[i].isFile === true && $.inArray(extension, AppFile.EXTENSIONS) >= 0)
               {
                  appFile = new AppFile(entries[i].name, entries[i].fullPath);
                  appFile.addFile();
                  console.log(‘File saved: ‘ + entries[i].fullPath);
               }
            }
         },
         function(error) {
            console.log(‘Unable to read the directory. Errore: ‘ + error.code);
         }
      );
 
      if (level === 0)
         $(document).trigger(‘endupdate’);
      console.log(‘Current path analized is: ‘ + path);
   },
   createFilesList: function(idElement, files)
   {
      $(‘#’ + idElement).empty();
 
      if (files == null || files.length == 0)
      {
         $(‘#’ + idElement).append(‘<p>No files to show. Would you consider a files update (top right button)?</p>’);
         return;
      }
 
      function getPlayHandler(file) {
         return function playHandler() {
            $.mobile.changePage(
               ‘player.html’,
               {
                  data: {
                     file: JSON.stringify(file)
                  }
               }
            );
         };
      }
 
      function getDeleteHandler(file) {
         return function deleteHandler() {
            var oldLenght = AppFile.getAppFiles().length;
            var $parentUl = $(this).closest(‘ul’);
 
            file = new AppFile(», file.fullPath);
            file.deleteFile();
            if (oldLenght === AppFile.getAppFiles().length + 1)
            {
               $(this).closest(‘li’).remove();
               $parentUl.listview(‘refresh’);
            }
            else
            {
               console.log(‘Media not deleted. Something gone wrong.’);
               navigator.notification.alert(
                  ‘Media not deleted.
                  function(){},
                  ‘Error’
               );
            }
         };
      }
 
      var $listElement, $linkElement;
      files.sort(AppFile.compareIgnoreCase);
      for(var i = 0; i < files.length; i++)
      {
         $listElement = $(‘<li>’);
         $linkElement = $(‘<a>’);
         $linkElement
         .attr(‘href’, ‘#’)
         .text(files[i].name)
         .click(getPlayHandler(files[i]));
 
         // Append the link to the <li> element
         $listElement.append($linkElement);
 
         $linkElement = $(‘<a>’);
         $linkElement
         .attr(‘href’, ‘#’)
         .text(‘Delete’)
         .click(getDeleteHandler(files[i]));
 
         // Append the link to the <li> element
         $listElement.append($linkElement);
 
         // Append the <li> element to the <ul> element
         $(‘#’ + idElement).append($listElement);
      }
      $(‘#’ + idElement).listview(‘refresh’);
   }
};

В предыдущей части этой серии я упоминал, что интересной точкой на странице кредитов является атрибут target="_blank" применяемый к ссылкам. В этом разделе объясняется, почему метод openLinksInApp() класса Application имеет смысл.

Когда-то давно Cordova открывала внешние ссылки в том же Cordova WebView, в котором работало приложение. Когда ссылка была открыта, и пользователь нажимал кнопку «назад», последняя отображаемая страница отображалась точно так же, как и до того, как пользователь покинул ее. В более новой версии это изменилось. В настоящее время внешние ссылки открываются по умолчанию с помощью Cordova WebView, если URL-адрес находится в белом списке вашего приложения. URL-адреса, которых нет в вашем белом списке, открываются с помощью API InAppBrowser. Если вы не управляете ссылками надлежащим образом или пользователь нажимает на ссылку, которая отображается в InAppBrowser или в системе, а затем решает вернуться, все улучшения jQuery Mobile теряются. Это происходит потому, что файлы CSS и JavaScript загружаются главной страницей, а следующие загружаются с использованием AJAX. Прежде чем раскрывать решение, давайте посмотрим, что такое InAppBrowser.

InAppBrowser — это веб-браузер, который отображается в вашем приложении при использовании вызова window.open.

Этот API имеет три метода:

  • addEventListener(): позволяет прослушивать три события ( loadstart , loadstop и exit) и прикреплять функцию, которая запускается сразу loadstart loadstop этих событий.
  • removeEventListener() : Удаляет ранее присоединенный слушатель.
  • close() : используется для закрытия окна InAppBrowser.

Итак, каково решение? Цель функции openLinksInApp() сочетании с openLinksInApp() списком, указанным в файле конфигурации, состоит в том, чтобы отлавливать щелчки по всем внешним ссылкам, распознанным с помощью атрибута target="_blank" , и открывать их с помощью window.open() метод. С помощью этой техники мы избежим описанной проблемы, и наш игрок продолжит выглядеть и работать так, как ожидалось.


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