Комментарии встречаются как непосредственно в коде, так и в виде пояснений к коммитам.
Будет довольно много кода
Задача:
— сформировать список альбомов
— Найти отсутствующие в системе обложки для каждого альбома
— Сохранить их на устройстве таким образом, чтобы обложки альбомов видел как наш плеер, так и сторонние.
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 записывается как бинарный объект.
— Сперва пытаемся создать 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
— Метод для установки текущего трека как рингтон (выдран из сорцов).
— Класс обертка для вызова 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); } }
— Скролим grid вверх вниз и обнаруживаем две вещи:
1. Жуткие лаги (все верно, надо оптимизировать)
2. out of memory (тоже верно, правда словили чуть раньше чем ожидали, так как в сворованный из сорцов код вкрался сюрприз)
— Создаем выделенную область памяти для хранения изображений, которая будет вытесняться по мере наполнения (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 для индикации хода загрузки и выводим уведомление о количестве найденных альбумартов.
А вот теперь будут слайды!

ссылка на оригинал статьи http://habrahabr.ru/company/freeamp/blog/212125/
Добавить комментарий