Ниже я предложу своё видение критериев оценки библиотек и в качестве бонуса: код весьма полезного и удобного компонента для асинхронной загрузки, кеширования и отображения картинок из сети.
- (И главное!) Простыми. Если вы создали великолепную абстракцию, универсальную архитектуру, применили все свои любимые паттерны и надеетесь, что за это другие разработчики будут обязаны потратить пару дней на изучение вашего продукта - вас ждёт разочарование. Программисты используют библиотеки чтобы упростить себе жизнь. И всё. Эстетическими чувствами они не руководствуются. Когда я выбираю себе библиотеку для решения какой-то задачи, я не буду даже рассматривать то, что не заработало легко, интуитивно, без штудирования мануалов. Исключения есть, но это должны быть ОЧЕНЬ хорошие библиотеки или у них не должно быть альтернатив. В нашем деле это редкость.
- Монофункциональными. Если вам нужно отобразить простенькую анимацию, и у вас есть под рукой только пятимегабайтный "комбайн", который делает всё на свете - лучше сделайте анимацию сами. Или, в крайнем случае, возьмите этого монстра на первый релиз, но потом, когда появится время, обязательно замените его на что-то что делает только эту анимацию и всё.
- Изолированными. Если чтобы впилить библиотеку вам приходится переписать всю Activity, добавить в проект пару Layout-ов и немного "поправить" бизнес-логику - это плохая библиотека. Идеальная библиотека как коробочка с одним универсальным разъёмом: Включаете просто, легко, в любое место одной-двумя строчками кода. И больше нигде ничего не нужно менять. Всё что ей нужно у неё внутри.
Вот и всё. Понятно, что библиотека должна быть без багов, гибкая, с хорошим качеством кода, документированная и т.д. и т.п. Мы же о свободном коде говорим верно? Если в простом удобном и идеально вам подходящем компоненте найдётся баг - исправьте его. И документируйте, если нужна документация.
Последуем своим советам и сделаем что-то полезное
Я заметил как-то что уже дважды менял "любимую" библиотеку для загрузки картинок в ImageView из сети. У одной были проблемы с кешированием, другая была слишком громоздкой. А задача-то тривиальная. Вот и подумалось мне: не пора ли сделать своё, родное?
Требования к компоненту стандартные:
- это должен быть просто наследник ImageView, который без лишних телодвижений можно добавить в xml layout, извлечь через findViewById и вызовом одного метода заставить его работать.
- асинхронно загружать картинку по её URL и отображать прогресс загрузки.
- кэшировать в память и(или) на sd-карту загруженные изображения и при повторном вызове показывать их из без запросов в сеть.
- контролировать размер кэша (особенно памяти) и число одновременных потоков загрузки
Получилось вот что:
import android.content.Context; import android.graphics.*; import android.os.Handler; import android.util.AttributeSet; import android.util.Base64; import android.util.Log; import android.widget.ImageView; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.math.RoundingMode; import java.net.URL; import java.net.URLConnection; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; public class NetImageView extends ImageView { private static final int CONNECT_TIMEOUT = 5000; private static final int READ_TIMEOUT = 10000; private static final String DISK_CACHE_PATH = "/netimage_cache/"; private static final boolean USE_DISK_CASHE = true; private static final boolean USE_MEMORY_CASHE = true; private static final int MEMORY_SIZE_LIMIT = 50; private static final int REQUEST_POOL_SIZE_LIMIT = 5; private String mDiskCachePath; private double mCurrPercent = 0.0D; private boolean mLoaded = false; private boolean mNeedShowProgress = true; private Handler mHandler = new Handler(); private static final String TAG = "netimage"; private static final ConcurrentHashMap<String, Bitmap> memoryCache = new ConcurrentHashMap<String, Bitmap>(); private static final ConcurrentLinkedQueue<String> requestPool = new ConcurrentLinkedQueue<String>(); private static final Object monitor = new Object(); private Bitmap rezbmp; public NetImageView(Context context, AttributeSet attrs) { super(context, attrs); mDiskCachePath = context.getCacheDir().getAbsolutePath() + DISK_CACHE_PATH; File outFile = new File(mDiskCachePath); outFile.mkdirs(); } public void loadImage(final String url, int staticLoaderImageResource, final int failLoadImageResource, boolean needShowProgress) { this.mNeedShowProgress = needShowProgress; setImageResource(staticLoaderImageResource); new Thread(new Runnable() { @Override public void run() { mLoaded = false; final Bitmap bmp; try { bmp = cashedLoad(url); mLoaded = true; mHandler.post(new Runnable() { @Override public void run() { if (bmp==null) setImageResource(failLoadImageResource); else setImageBitmap(bmp); } }); } catch (AlreadyLoadingException e) { e.printStackTrace(); } } }).start(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (!mLoaded && mNeedShowProgress) { int delimiter = new BigDecimal((getMeasuredWidth() - 30) * mCurrPercent).intValue(); if (delimiter<30) delimiter = 30; canvas.drawColor(Color.BLACK); Paint p = new Paint(); p.setColor(Color.WHITE); canvas.drawText(new BigDecimal(mCurrPercent * 100).setScale(2, RoundingMode.HALF_UP).doubleValue() + "%", 30, getMeasuredHeight() / 2 - 10, p); canvas.drawRect(30, getMeasuredHeight() / 2 - 4, delimiter, getMeasuredHeight() / 2 + 4, p); p.setColor(Color.GRAY); canvas.drawRect(delimiter, getMeasuredHeight() / 2 - 4, getMeasuredWidth() - 30, getMeasuredHeight() / 2 + 4, p); } } private Bitmap cashedLoad(String url) throws AlreadyLoadingException { rezbmp = null; String key; try { key = new String(Base64.encode(url.getBytes("UTF-8"), Base64.DEFAULT), "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); return null; } final File dcashef = new File(mDiskCachePath + key); if (USE_MEMORY_CASHE && (rezbmp = memoryCache.get(key))!=null) { Log.v(TAG, "restored from memory cashe key: " + key); if (USE_DISK_CASHE && !dcashef.exists()) { new Thread(new Runnable() { @Override public void run() { try { rezbmp.compress(Bitmap.CompressFormat.PNG, 100, new FileOutputStream(dcashef)); Log.v(TAG, "updated disk cashe file: " + dcashef.getAbsolutePath() + " successfully"); } catch (Exception e) { e.printStackTrace(); } } }).start(); } return rezbmp; } if (USE_DISK_CASHE && dcashef.exists() && (rezbmp = BitmapFactory.decodeFile(dcashef.getAbsolutePath()))!=null) { Log.v(TAG, "restored from cashe file: " + dcashef.getAbsolutePath()); if (USE_MEMORY_CASHE) { if (memoryCache.size()>=MEMORY_SIZE_LIMIT) memoryCache.remove(memoryCache.keySet().iterator().next()); memoryCache.put(key, rezbmp); Log.v(TAG, "updated to memory cashe successfully"); } return rezbmp; } if (requestPool.contains(url)) throw new AlreadyLoadingException(); while (requestPool.size()>=REQUEST_POOL_SIZE_LIMIT) { synchronized (monitor) { try { monitor.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } requestPool.add(url); try { URLConnection conn = new URL(url).openConnection(); conn.setConnectTimeout(CONNECT_TIMEOUT); conn.setReadTimeout(READ_TIMEOUT); FileOutputStream out = new FileOutputStream(dcashef); InputStream in = conn.getInputStream(); byte[] buf = new byte[128]; int readed; int allread = 0; while ((readed = in.read(buf)) > 0) { mCurrPercent = (double) allread / conn.getContentLength(); out.write(buf, 0, readed); allread += readed; postInvalidate(); } out.flush(); out.close(); in.close(); requestPool.remove(url); synchronized (monitor) { monitor.notifyAll(); } Log.v(TAG, "stored to file: " + dcashef.getAbsolutePath() + " successfully"); rezbmp = BitmapFactory.decodeFile(dcashef.getAbsolutePath()); if (!USE_DISK_CASHE) dcashef.delete(); } catch (Exception e) { e.printStackTrace(); dcashef.delete(); } if (USE_MEMORY_CASHE && rezbmp!=null) { if (memoryCache.size()>=MEMORY_SIZE_LIMIT) memoryCache.remove(memoryCache.keySet().iterator().next()); memoryCache.put(key, rezbmp); Log.v(TAG, "stored to memory cashe successfully"); } return rezbmp; } public static void clearCashe() { memoryCache.clear(); } private class AlreadyLoadingException extends Exception {} }
Как этим пользоваться?
Вот так:
netImageView.loadImage(my_cool_image_url, android.R.drawable.ic_popup_sync, android.R.drawable.ic_menu_gallery, true);
Вот так:
netImageView.loadImage(my_cool_image_url, android.R.drawable.ic_popup_sync, android.R.drawable.ic_menu_gallery, true);
...где netImageView - экземпляр вашего view, который вы получили из xml-разметки.
Первым параметром мы передаём URL изображения, вторым - ресурс изображения, которое должно отображаться при загрузке (статический прелоадер), вторым - то изображение что появится в случае если загрузка провалится. Третий параметр определяет, нужно ли вместо статического прелоадера показывать прогресс-бар в процессе загрузки.
Как это настраивать?
В принципе - никак. настройки по умолчанию должны подходить всем. Но если вдруг захотелось - посмотрите на константы в начале листинга:
CONNECT_TIMEOUT - интервал ожидания соединения
READ_TIMEOUT - интервал чтения
DISK_CACHE_PATH - путь к файлам кэша в стандартной директории для кэша приложения
USE_DISK_CASHE - кешировать ли на sd-карту
USE_MEMORY_CASHE - кэшировать ли в память
MEMORY_SIZE_LIMIT максимальное число картинок в кэше памяти
REQUEST_POOL_SIZE_LIMIT - максимальное число потоков загрузки
Первым параметром мы передаём URL изображения, вторым - ресурс изображения, которое должно отображаться при загрузке (статический прелоадер), вторым - то изображение что появится в случае если загрузка провалится. Третий параметр определяет, нужно ли вместо статического прелоадера показывать прогресс-бар в процессе загрузки.
Как это настраивать?
В принципе - никак. настройки по умолчанию должны подходить всем. Но если вдруг захотелось - посмотрите на константы в начале листинга:
CONNECT_TIMEOUT - интервал ожидания соединения
READ_TIMEOUT - интервал чтения
DISK_CACHE_PATH - путь к файлам кэша в стандартной директории для кэша приложения
USE_DISK_CASHE - кешировать ли на sd-карту
USE_MEMORY_CASHE - кэшировать ли в память
MEMORY_SIZE_LIMIT максимальное число картинок в кэше памяти
REQUEST_POOL_SIZE_LIMIT - максимальное число потоков загрузки
Спасибо
ОтветитьУдалитьУ меня есть идея приложения, но написать его может только проф.! Где можно найти таких людей чтоб написали мне приложение?
ОтветитьУдалить