понедельник, 10 февраля 2014 г.

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

В статье описывается создание функционала синхронизации обложек для альбомов.

Комментарии встречаются как непосредственно в коде, так и в виде пояснений к коммитам.

Будет довольно много кода

Задача:

— сформировать список альбомов

— Найти отсутствующие в системе обложки для каждого альбома

— Сохранить их на устройстве таким образом, чтобы обложки альбомов видел как наш плеер, так и сторонние.


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://ift.tt/nIICcg"
xmlns:FreeAmp="http://ift.tt/GEGVYd">
<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://ift.tt/1eieped", 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);


— Сохраняем ссылку на картинку в таблицу БД (подсмотрено в исходниках, стараемся гадить в те же папки, в которые уже нагадил android)



// 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


— Создаем выделенную область памяти для хранения изображений, которая будет вытесняться по мере наполнения



//Создаем LruCache: http://ift.tt/1cMrpwY
+ 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 для индикации хода загрузки и выводим уведомление о количестве найденных альбумартов.


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


image


image


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий