Создание галереи альбомов

от автора

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

Задача:
— сформировать список альбомов
— Найти отсутствующие в системе обложки для каждого альбома
— Сохранить их на устройстве таким образом, чтобы обложки альбомов видел как наш плеер, так и сторонние.

118: создаем дочернее активити, в котором будем отображать загруженные обложки альбомов

— Создаем пустое активити-заглушку
— Добавляем контекстное меню в actionbar, состоящее из «колесика» обновить.
— При создании меню — «колесико начинает крутиться» (заменяется на прогрессбар).
— Определяем кастомный ActionBar background

Далее, я буду оставлять преимущественно ссылки на коммиты, с небольшими пояснениями. Так как иначе статья будет слишком объёмна.

/**  * Created by recoil on 26.01.14.  */ public class ActArtworks extends Activity {      private AQuery aq;     private Menu optionsMenu;     private boolean refreshing = true;     private Activity activity;      public void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         //устанавливаем кастомный бэкграунд акшенбара         getActionBar().setBackgroundDrawable(getResources().getDrawable(R.drawable.ab_bgr));         //добавляем кнопку назад         getActionBar().setDisplayHomeAsUpEnabled(true);          activity = this;         aq = new AQuery(activity);     }      @Override     public boolean onCreateOptionsMenu(Menu menu) {         this.optionsMenu = menu;         //создаем меню в акшенбаре         MenuInflater inflater = getMenuInflater();         inflater.inflate(R.menu.artwork, menu);          //только после того как меню создано - запускаем обновление         update();         return super.onCreateOptionsMenu(menu);     }      public void update() {          AQUtility.debug("Update progress");         //устанавливаем статус в "обновляется"         refreshing = true;         //раскручиваем колесеко         setRefreshActionButtonState();     }      @Override     public boolean onOptionsItemSelected(MenuItem item) {         switch (item.getItemId()) {             case android.R.id.home:                 //закрываем активити на нажатие кнопки домой                 finish();                 return true;         }         return super.onOptionsItemSelected(item);     }      public void setRefreshActionButtonState() {          //если статус обновляется - заменяем иконку обновить на крутящийся прогрессбар         if (optionsMenu != null) {             final MenuItem refreshItem = optionsMenu                     .findItem(R.id.menu_refresh);             if (refreshItem != null) {                 if (refreshing) {                     refreshItem.setActionView(R.layout.actionbar_indeterminate_progress);                 } else {                     refreshItem.setActionView(null);                 }             }         }     } } 

118: создаем класс fillmediastoretracks для считывания всех треков из медиабиблиотеки

— Класс считывает записи из таблицы MediaStore базы данных.
При перезагрузке телефона в системе стартует сервис, осуществляющий сканирование добавленных файлов. Файлы, попадающие под определение «media» добавляются в базу данных.
— По завершении считывания всех данных из таблицы треков — сериализованный ArrayList записывается как бинарный объект.

118: считываем треки

— Сперва пытаемся создать ArrayList всех треков, сериализовав его из сохраненного объекта.
— Если «save»’а — нет, формируем список треков и «сэйв»

118: отображаем список альбомов

— Добавляем GridView в activity
— Добавляем адаптер, для отображения данных
— В адаптере определяем getView, состоящий из единственного текстового поля
— Выводим список всех наименований альбомов, известных андроиду

118: выкидываем дубли альбомов

— Сортируем лист треков по альбому.
— Запускаем итератор, и идем по списку. При дублировании альбома в списке — выкидываем его из массива.

118: добавлена совместимость с андроид 8+ (уф!)

— Добавляем исходный код библиотеки совместимости appcompat
-Переделываем все контекстные меню — добавляем свою схему и в рамках нее определяем как показывать пункт меню

<menu xmlns:android="http://schemas.android.com/apk/res/android"       xmlns:FreeAmp="http://schemas.android.com/apk/res-auto">     <item android:id="@+id/menu_refresh"           android:icon="@drawable/ic_action_refresh"           android:title=""           android:alphabeticShortcut="r"           android:orderInCategory="1"           FreeAmp:showAsAction="always" />  </menu> 

— Наследуемся от встроенного в библиотеку стиля

<style name="theme" parent="@style/Theme.AppCompat"> 

— Все activity наследуем от ActionBarActivity
— Переопределяем создание менюитемов

-                    refreshItem.setActionView(R.layout.actionbar_indeterminate_progress); +                    MenuItemCompat.setActionView(refreshItem, R.layout.actionbar_indeterminate_progress); 

— Импортируем библиотеку ListPopupWindow — из библиотеки совместимости

Пришлось немножко отвлечься на несколько слабо связанных модификаций (преимущественно баги)
118: нотификейшен бэкграунд
118: ape без тегов обработан. Проверить на других ape
118: добавлена поддержка файлов .opus

119: добавлена библиотека curl

curl for android
libcurl.so size:
(default is ftp, https with ares)
https: ~169K (including http, https)
ares: ~28K (adding to https, with ares support)
ipv6: ~0K (no extra size)
+full: ~278K (all protocols, with ares)
Это достаточно смелое и во многом экспериментальное решение у которого есть как и безусловные достоинства, так и безусловные недостатки. Так как я планирую написать плеер под все популярные платформы — по возможности пробую кроссплатформенные библиотеки. Выйдет ли из curla толк — посмотрим.

Опять небольшое отсупление от основной линии:
119: set ringtone

— Метод для установки текущего трека как рингтон (выдран из сорцов).

119: easy curl request

— Класс обертка для вызова curl. Пока все довольно аскетично. На входе url — на выходе массив байтов (ByteArrayOutputStream). Хочешь — делай из него строка, а не хочешь — делай bitmap. Или еще что ть.

Пришли люди и начали просить сделать поддержку внешних sd cart

121: выкидываем не найденный альбумарт и формируем грид со списком альбомов

— Оборачиваем генерацию списка обложек в асинхронную задачу
121: генерим картинки для гридвью

— Считываем json с обложкой альбома с last.fm через обертку вокруг curl

                     String url = String.format("http://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key=0cb75104931acd7f44f571ed12cff105&artist=%s&album=%s&format=json", Uri.encode(track.getArtist()),Uri.encode(currentAlbum));                      getHttpData = new GetHttpData();                      getHttpData.setUrl(url);                      getHttpData.request();                      String result = new String(getHttpData.getByteArray()); 

— Парсим ответ

JSONObject jsonObject = new JSONObject(result);                             jsonObject = jsonObject.getJSONObject("album");                             JSONArray image = jsonObject.getJSONArray("image");                             for (int i=0;i<image.length();i++) {                                 jsonObject = image.getJSONObject(i);                                 if (jsonObject.getString("size").equals("extralarge")) {                                     albumArtImageLink = Uri.decode(jsonObject.getString("#text"));                                      AQUtility.debug(track.getArtist()+":"+currentAlbum,albumArtImageLink);                                 }                             } 

— Загружаем картинку

//download image                                 getHttpData = new GetHttpData();                                 getHttpData.setUrl(albumArtImageLink);                                 getHttpData.request();                                  ContentResolver res = activity.getContentResolver();                                 Bitmap bm = BitmapFactory.decodeByteArray(getHttpData.getByteArray(),0,getHttpData.getByteArray().length); 

— Сохраняем ссылку на таблицу (подсмотрено в исходниках)

// Put the newly found artwork in the database.                                     // Note that this shouldn't be done for the "unknown" album,                                     // but if this method is called correctly, that won't happen.                                      // first write it somewhere                                     String file = Environment.getExternalStorageDirectory()                                             + "/albumthumbs/" + String.valueOf(System.currentTimeMillis());                                     if (FileUtils.ensureFileExists(file)) {                                         try {                                             OutputStream outstream = new FileOutputStream(file);                                             if (bm.getConfig() == null) {                                                 bm = bm.copy(Bitmap.Config.RGB_565, false);                                                 if (bm == null) {                                                     //return getDefaultArtwork(context);                                                 }                                             }                                             boolean success = bm.compress(Bitmap.CompressFormat.JPEG, 75, outstream);                                             outstream.close();                                             if (success) {                                                 ContentValues values = new ContentValues();                                                 values.put("album_id", track.getAlbumId());                                                 values.put("_data", file);                                                 Uri newuri = res.insert(MediaUtils.sArtworkUri, values);                                                 if (newuri == null) {                                                     // Failed to insert in to the database. The most likely                                                     // cause of this is that the item already existed in the                                                     // database, and the most likely cause of that is that                                                     // the album was scanned before, but the user deleted the                                                     // album art from the sd card.                                                     // We can ignore that case here, since the media provider                                                     // will regenerate the album art for those entries when                                                     // it detects this.                                                     success = false;                                                 }                                             }                                             if (!success) {                                                 File f = new File(file);                                                 f.delete();                                                 iterator.remove();                                             }                                         } catch (FileNotFoundException e) {                                             AQUtility.debug( "error creating file", e);                                         } catch (IOException e) {                                             AQUtility.debug( "error creating file", e);                                         }                                     } 

121: out of memory

— Скролим grid вверх вниз и обнаруживаем две вещи:
1. Жуткие лаги (все верно, надо оптимизировать)
2. out of memory (тоже верно, правда словили чуть раньше чем ожидали, так как в сворованный из сорцов код вкрался сюрприз)

121: LRU Cashe

— Создаем выделенную область памяти для хранения изображений, которая будет вытесняться по мере наполнения (http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html)

//Создаем LruCache: http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html +    int cacheSize = 20 * 360000; // <7MiB = 300width * 300heigth * 4bytesperpixel * 20images +    LruCache bitmapCache = new LruCache(cacheSize) { +        protected int sizeOf(int key, Bitmap value) { +            return value.getRowBytes() * value.getHeight();//здесь по чесноку считаем +        } +    }; + +    public void addBitmapToMemoryCache(int key, Bitmap bitmap) { +        synchronized (bitmapCache) { +            if (getBitmapFromMemCache(key) == null) { +                bitmapCache.put(key, bitmap); +            } +        } +    } +  +    public Bitmap getBitmapFromMemCache(int key) { +        return (Bitmap) bitmapCache.get(key); +    } 

— Считываем ранее загруженные картинки из кеша, добавляем новые картинки в кеш при загрузке

121: загружаем изображения асинхронно

После перехода на вытесняющий кеш произошли две вещи:
1. Пропал out of memory (что логично)
2. Но тормоза при скролле то остались

— Считываем картинки асинхронно

121: onScrollStateChanged, onConfigurationChanged

— Отключаем загрузку картинку, в момент когда пользователь «катнул» скролл и он «парит» по инерции

— Пересоздаем адаптер при изменении ориентации устройства

void applyAdapter() {         if (tracks == null) return;         adapter = new AdpArtworks(activity,tracks);         int iDisplayWidth = getResources().getDisplayMetrics().widthPixels ;         int numColumns = (iDisplayWidth / 310);         gridView.setColumnWidth( (iDisplayWidth / numColumns) );         gridView.setNumColumns(numColumns);         gridView.setStretchMode( GridView.NO_STRETCH ) ;         gridView.setAdapter(adapter);         gridView.setOnScrollListener(new AbsListView.OnScrollListener() {             @Override             public void onScrollStateChanged(AbsListView view, int scrollState) {                 adapter.setScrollState(scrollState);                 if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {                     adapter.notifyDataSetChanged();                 }             }              @Override             public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {              }         });     } 

121: convert drawable 2 bitmap, update

— Конвертируем xml placeholder’а в изображение
— Вешаем вызов обновления на клик по «колесику» в меню

+        final Drawable imgBgr = activity.getResources().getDrawable(R.drawable.row_bgr); +        final Bitmap bitmap = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888); +        Canvas canvas = new Canvas(bitmap); +        imgBgr.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); +        imgBgr.draw(canvas); +        this.placeHolder = bitmap; 

121: индикатор загрузки
121: уведомление

— Добавляем progress для индикации хода загрузки и выводим уведомление о количестве найденных альбумартов.

А вот теперь будут слайды!

imageimage

ссылка на оригинал статьи http://habrahabr.ru/company/freeamp/blog/212125/


Комментарии

Добавить комментарий

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