Saturday, April 2, 2011

Менеджер интернет изображений для Android, просто!

Приветствую всех заглянувших.

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

Что же не так, читаем дальше.

Задача
Есть ListView (список), допустим, ваших статусов с twitter или новостей с facebook. Мы пропустим этап получения всего списка, его сортировки и структоризации, сразу перейдем к его корректному отображению. Разумеется хотелось бы видеть в каждом пункте списка, изображение человека или сервиса, на который вы подписаны, который что-то опубликовал. В принципе, первое что приходит в голову, это просто скачать по ссылке изображение и передать в ImageView соответствующего пункта. Но разве это максимум наших возможностей?

Решение и усложнение
И так, у нас есть список ссылок на изображения (URL), т.к. это наши новости с сервисов, то вполне вероятно, что URL могут попросту повторяться, и тут возникает разумный вопрос, зачем скачивать одно и тоже изображение несколько раз? Верно, незачем. Значит, нам потребуется организовать систему, которая бы хранила список всех запрошенных URL, чтобы исключить повторную загрузку. Так как нам потребуется загружать только один раз URL, а запросивших данный URL может быть более одного, значит стоит ввести статус/флаг для каждого из URL:

public static final int STATE_NULL = 0;
public static final int STATE_PENDING = 1;
public static final int STATE_READY = 2;

где STATE_NULL - изображение не запрошено и отсутствует;
STATE_PENDING - изображение запрошено и в данный момент скачивается;
STATE_READY - изображение запрошено и скачено успешно;

Вы могли заметить, что STATE_READY уточняет, что "скачено успешно", т.е. если произойдут какие-либо проблемы при скачивании, мы сбрасываем флаг в STATE_NULL. В принципе это позволит организовать отношение один URL - много запросов.
В полне вероятно, кто-то уже мог догадаться, что если запросить URL, а данный URL имеет статус STATE_PENDING, что делать в таком случае? Для этого нужно создать список/стек/очередь (что душе угодно) где бы каждый URL хранил список всех запросивших данный URL, и после смены статуса на STATE_READY, мы могли бы пройтись по этому списку и сообщить всем запросившим что изображение скачено успешно.

public ArrayList<Object> pendingList;

Как видим, ничего сложного пока не наблюдается. Давайте закончим сущность запроса URL:

private class DrawableInfo {
public static final int STATE_NULL = 0;
public static final int STATE_PENDING = 1;
public static final int STATE_READY = 2;

public String url;
public int state = STATE_NULL;
public Drawable drawable;
public ArrayList<Object> pendingList;

public DrawableInfo() {
state = STATE_NULL;
pendingList = new ArrayList<Object>();
}
}

Система организации списка URL и установки статусов
Что от данной системы нам потребуется?
  1. Получение полной информации по URL (DrawableInfo)
  2. Получение объекта drawable (изображения) по URL
  3. Установление статуса в STATE_READY, получение списка запросивших данный URL и так же указать скаченное изображение
  4. Установление статуса в STATE_PENDING и добавление нового запросившего URL
  5. Сброс статуса в STATE_NULL и отчистка списка запросивших данный URL
  6. Проверка статуса URL на STATE_READY
  7. Проверка статуса URL на STATE_PENDING
В принципе этого достаточно для решения поставленной задачи.
Перейдем к реализации:

private class DrawableInfoList extends ArrayList<DrawableInfo> {
private static final long serialVersionUID = 1L;

public DrawableInfo getByUrl(String url) {
for (DrawableInfo info : this) {
if (info.url != null && info.url.equals(url)) {
return info;
}
}
return null;
}

public Drawable getDrawableByUrl(String url) {
DrawableInfo info = getByUrl(url);
if (info != null && info.state == DrawableInfo.STATE_READY) {
return info.drawable;
}
return null;
}

public List<Object> putDrawableByUrl(String url, Drawable drawable) {
DrawableInfo info = getByUrl(url);
if (info == null) {
info = new DrawableInfo();
info.url = url;
add(info);
}
info.drawable = drawable;
info.state = DrawableInfo.STATE_READY;
return info.pendingList;
}

public void putDrawablePendingByUrl(String url, Object obj) {
DrawableInfo info = getByUrl(url);
if (info == null) {
info = new DrawableInfo();
info.url = url;
add(info);
}
info.state = DrawableInfo.STATE_PENDING;
for (DrawableInfo ninfo : this) {
ninfo.pendingList.remove(obj);
}
info.pendingList.add(obj);
}

public void putDrawableNullByUrl(String url) {
DrawableInfo info = getByUrl(url);
if (info == null) {
info = new DrawableInfo();
info.url = url;
add(info);
}
info.state = DrawableInfo.STATE_NULL;
}

public boolean isDrawableReadyByUrl(String url) {
DrawableInfo info = getByUrl(url);
return info != null && info.state == DrawableInfo.STATE_READY;
}

public boolean isDrawablePendingByUrl(String url) {
DrawableInfo info = getByUrl(url);
return info != null && info.state == DrawableInfo.STATE_PENDING;
}
}

Думаю тут объяснять ничего не нужно.

Менеджер, наконец то
Начнем с описания методов первой необходимости.

public class DrawableManager {
private final DrawableInfoList drawableList;

public DrawableManager() {
drawableList = new DrawableInfoList();
}

public void clear() {
drawableList.clear();
}

public boolean isDrawableAvaiable(String url) {
return drawableList.isDrawableReadyByUrl(url);
}
}

Список статусов и url, конструктор, отчистка списка и проверка доступности изображения по URL. Ничего сложного, пока.

Для реализации следующего метода, а именно, загрузки изображения и установки его в ImageView, нам потребуется создать Listener, вдруг пользователю захочется что-то сделать по перед установкой изображения, анимация, к примеру. Для этого создадим соответствующий интерфейс:

public interface DrawableManagerListener {
public void drawableComplete(String url, Drawable drawable, ImageView imageView);
}

Теперь нам потребуются методы для загрузки изображения, распределим на получение потока, преобразования потока в drawable, url в drawable, а так же одна хитрость, если на целевом устройстве будет медленный интернет, то вполне вероятно некоторые изображения не будут загружены успешно. И так, все это реализуем сейчас:

public static Drawable getDrawableFromUrl(String url) throws Exception {
return Drawable.createFromStream(new FlushedInputStream(getInputStreamFromUrl(url)), null);
}

public static Drawable getDrawableFromStream(InputStream inputStream) {
return Drawable.createFromStream(new FlushedInputStream(inputStream), null);
}

public static InputStream getInputStreamFromUrl(String url) throws Exception {
DefaultHttpClient httpClient = new DefaultHttpClient();
HttpGet request = new HttpGet(url);
HttpResponse response = httpClient.execute(request);
return response.getEntity().getContent();
}

private static class FlushedInputStream extends FilterInputStream {
public FlushedInputStream(InputStream inputStream) {
super(inputStream);
}

@Override
public long skip(long n) throws IOException {
long totalBytesSkipped = 0L;
while (totalBytesSkipped < n) {
long bytesSkipped = in.skip(n - totalBytesSkipped);
if (bytesSkipped == 0L) {
int b = read();
if (b < 0) {
break;
} else {
bytesSkipped = 1;
}
}
totalBytesSkipped += bytesSkipped;
}
return totalBytesSkipped;
}
}

где класс-обвертка FlushedInputStream - та самая хитрость для загрузки всех изображений без исключений.

И так, перейдем непосредственно к главному методу нашего DrawableManager. Задача данного метода, получить на входе URL, ImageView для установки изображения и listener (callback) опционально (может быть и null). Логика для нашего метода должна быть такой же, какой мы описали ее в начале, т.е. проверить загружено ли изображение, если нет то поставить в очередь или же начать загрузку, как в первые запрошенное изображение. После успешной загрузки установить статус STATE_READY и пробежаться по списку запросивших вызывая listener или сразу установку ImageView загруженного изображения. А вот и код:

public void getDrawableFromUrl(final String url, ImageView imageView, final DrawableManagerListener listener) {
if (drawableList.isDrawableReadyByUrl(url)) {
imageView.setImageDrawable(drawableList.getDrawableByUrl(url));
} else if (drawableList.isDrawablePendingByUrl(url)) {
imageView.setImageDrawable(null);
drawableList.putDrawablePendingByUrl(url, imageView);
} else {
imageView.setImageDrawable(null);
drawableList.putDrawablePendingByUrl(url, imageView);

new AsyncTask<Void, Void, InputStream>() {
@Override
protected InputStream doInBackground(Void... params) {
InputStream inputStream;
try {
inputStream = getInputStreamFromUrl(url);
} catch (Exception e) {
e.printStackTrace();
inputStream = null;
}
return inputStream;
}

@Override
protected void onPostExecute(InputStream result) {
if (result != null) {
Drawable drawable = getDrawableFromStream(result);
List<Object> pendingList = drawableList.putDrawableByUrl(url, drawable);
for (Object imageView : pendingList) {
try {
if (listener == null) {
((ImageView) imageView).setImageDrawable(drawable);
} else {
listener.drawableComplete(url, drawable, (ImageView) imageView);
}
} catch (Exception e) {
e.printStackTrace();
try {
((ImageView) imageView).setImageDrawable(drawable);
} catch (Exception e2) {
e2.printStackTrace();
}
}
}
pendingList.clear();
} else {
drawableList.putDrawableNullByUrl(url);
}
super.onPostExecute(result);
}
}.execute();
}
}

Всем спасибо, думаю данный код поможет и ускорит чью-то работу.

No comments:

Post a Comment