Поддержка нескольких экранов с использованием параметров Bitmap Factory?
Редактировать один:
Изменения сделаны на основе ответа Иосифа:
В bytesToDrawable(byte[] imageBytes):
Изменено следующее: Использование BitmapDrawable(Ресурсы res, Растровое изображение) вместо BitmapDrawable(Растровое изображение):
return new BitmapDrawable(ApplicationConstants.ref_currentActivity.getResources(),BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, options));
Это результат этого изменения: Немного другая проблема:
Вопрос:
Если я использую новый конструктор для растрового рисования, и он масштабирует изображения для требуемой целевой плотности, нужно ли мне по-прежнему использовать мой метод CalculateSize?
Оригинальный вопрос:
Привет, друзья,
Мое приложение основано на модуле, и поэтому изображения, специфичные для этого модуля, загружаются только из jar (модуля), который их содержит, а не из основного приложения.
Каждый модуль имеет свой собственный ModularImageLoader - который в основном позволяет мне выбирать Drawables на основе имени изображения, найденного в банке.
Конструктор принимает zipFile (модуль A) и список имен файлов (любой файл, заканчивающийся на ".png" из zip).
Исследования проведены:
Я использовал следующее: Ссылка на страницу разработчика при эффективной загрузке растровых изображений
Изначально я создавал изображения размером для каждой плотности, но теперь у меня есть только один набор иконок размером 96x96.
Если плотность экрана меньше xhdpi, я загружаю меньшие размеры выборки изображения 96x96 - 36x36(для ldpi), 48x48(для mdpi), 72x72(для hdpi). В противном случае я просто возвращаю изображение 96х96. (Посмотрите на метод CalculateSize() и bytesToDrawable())
Я думаю, что легче понять концепцию с помощью кода: так вот, ModularImageLoader
Код:
public class ModularImageLoader
{
public static HashMap<String, Drawable> moduleImages = new HashMap<String, Drawable>();
public static int reqHeight = 0;
public static int reqWidth = 0;
public ModularImageLoader(ZipFile zip, ArrayList<String> fileNames)
{
float sdpi = ApplicationConstants.ref_currentActivity.getResources().getDisplayMetrics().density;
if(sdpi == 0.75)
{
reqHeight = 36;
reqWidth = 36;
}
else if(sdpi == 1.0)
{
reqHeight = 48;
reqWidth = 48;
}
else if (sdpi == 1.5)
{
reqHeight = 72;
reqWidth = 72;
}
else if (sdpi == 2.0)
{
reqHeight = 96;
reqWidth = 96;
}
String names = "";
for(String fileName : fileNames)
{
names += fileName + " ";
}
createByteArrayImages(zip, fileNames);
}
public static Drawable findImageByName(String imageName)
{
Drawable drawableToReturn = null;
for (Entry<String, Drawable> ent : moduleImages.entrySet())
{
if(ent.getKey().equals(imageName))
{
drawableToReturn = ent.getValue();
}
}
return drawableToReturn;
}
private static void createByteArrayImages(ZipFile zip, ArrayList<String> fileNames)
{
InputStream in = null;
byte [] temp = null;
int nativeEndBufSize = 0;
for(String fileName : fileNames)
{
try
{
in = zip.getInputStream(zip.getEntry(fileName));
nativeEndBufSize = in.available();
temp = toByteArray(in,nativeEndBufSize);
// get rid of .png
fileName = fileName.replace(".png", "");
fileName = fileName.replace("Module Images/", "");
moduleImages.put(fileName, bytesToDrawable(temp));
}
catch(Exception e)
{
System.out.println("getImageBytes() threw an exception: " + e.toString());
e.printStackTrace();
}
}
try
{
in.close();
}
catch (IOException e)
{
System.out.println("Unable to close inputStream!");
e.toString();
e.printStackTrace();
}
}
public static byte[] toByteArray(InputStream is, int length) throws IOException
{
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int l;
byte[] data = new byte[length];
while ((l = is.read(data, 0, data.length)) != -1)
{
buffer.write(data, 0, l);
}
buffer.flush();
return buffer.toByteArray();
}
public static Drawable bytesToDrawable(byte[] imageBytes)
{
try
{
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, options);
String imageType = options.outMimeType;
Log.d("ImageInfo : ", "Height:" + imageHeight +",Width:" +imageWidth + ",Type:" + imageType);
options.inJustDecodeBounds = false;
//Calculate sample size
options.inSampleSize = calculateSampleSize(options);
return new BitmapDrawable(BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length, options));
}
catch(Exception e)
{
Message.errorMessage("Module Loading Error", "The images in this module are too large to load onto cell memory. Please contact your administrator",
"Source of error: ModularImageLoader - bytesToDrawable method", e.toString());
return null;
}
}
public static int calculateSampleSize(BitmapFactory.Options options)
{
// raw height and width of the image itself
int sampleSize = 1;
int height = options.outHeight;
int width = options.outWidth;
if(height > reqHeight || width > reqWidth)
{
if(width > height)
{
sampleSize = Math.round((float)height / (float)reqHeight);
}
else
{
sampleSize = Math.round((float)width / (float)reqWidth);
}
}
return sampleSize;
}
}
Проблема:
На рисунке ниже показаны 4 запущенных эмулятора, их характеристики и то, как я их установил в AVD затмения:
LDPI: плотность 120, скин QVGA MDPI: плотность 160, скин HVGA HDPI: плотность 240, кожа WVGA800 XHDPI: плотность 320, кожа 800x1280
Проблема с изображением:
Вопрос:
Исходя из кода - в окне XHDPI почему изображение контактов такое маленькое? Изображение News также имеет размер 96x96 (за исключением того, что оно загружено из основного приложения - поэтому оно находится под res>XHDPI). Дело в том, что я вижу, что он отлично загружается для экранов MDPI и HDPI, но для остальных это странно. Есть идеи?
4 ответа
ldpi - 36x36 mdpi - 48x48 hdpi - 72x72 xhdpi - 96x96
Было бы замечательно, если бы все они были чистыми кратными друг другу, поскольку фабрика растровых изображений обрабатывает размер выборки в целых числах, и поэтому размер выборки должен быть целым числом (без конечных десятичных дробей, чтобы быть полностью точными).
Решение:
До того, как я начал сэмплирование, у меня было 1 изображение для "каждого" типа экрана, и если бы у этого изображения было нажатое состояние, у меня было бы 2 отдельных изображения для этого.
Поэтому для одного изображения - мне фактически нужно 4 в приложении, и если бы это изображение имело нажатые состояния - одному изображению понадобилось бы 8 изображений.
Моя главная цель состояла в том, чтобы сократить количество изображений, чтобы я не перегружал выделение кучи растровых изображений и, возможно, выбрасывал исключение из нехватки памяти. Я видел это исключение для меня, когда размер моего растрового изображения вполне приемлем (Я считаю, что это как-то связано с количеством изображений в куче, а также с их соответствующими размерами (поправьте меня, если я ошибаюсь, пожалуйста...)), и я хотел, конечно, сократить размер моего модуля.
Поэтому я решил следующее: иметь 2 изображения для каждого изображения - одно размером 72 и одно размером 96 - таким образом, у меня будут значки, необходимые для экранов с плотностью xhdpi и hdpi, и я смогу (просто) сэмплировать до лдпи и мдпи при необходимости.
72/2 = 36 96/2 = 48
Таким образом, у меня было бы только 2 изображения для каждого изображения, и в худшем случае, если бы у этого изображения были нажатые состояния, у меня было бы 4 изображения. Это сокращает размер банка изображений почти на 50% и делает мои модули намного меньше. Я заметил изменение размера модуля с 525 кбайт до 329.
Это действительно то, к чему я стремился.
Спасибо всем за помощь! Если у кого-то есть какие-либо вопросы, пожалуйста, не стесняйтесь оставлять комментарии, и я свяжусь с вами, как только смогу.
BitmapFactory может масштабировать изображения для вас, если вы предоставляете информацию о плотности с помощью BitmapFactory.Options. Если вы сделаете это, вы сможете удалить пользовательский код масштабирования в вашем ModularImageLoader.
Укажите inDensity и inTargetDensity - что-то вроде следующего:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inDensity = DisplayMetrics.DENSITY_MEDIUM;
options.inTargetDensity = activityRef.getResources().getDisplayMetrics().densityDpi;
options.inScaled = true;
return BitmapFactory.decodeStream(openByteStream(), null, options);
Очевидно, есть ошибка в BitmapFactory.decodeByteArray, которая игнорирует некоторые параметры масштабирования, поэтому вам может понадобиться обернуть массив байтов в ByteArrayInputStream и использовать BitmapFactory.decodeStream, как показано выше (см. http://code.google.com/p/android/issues/detail?id=7538).
Вы должны использовать BitmapDrawable(Resources res, Bitmap bitmap)
конструктор, который будет гарантировать, что у drawable задана правильная целевая плотность, используемый вами конструктор устарел.
Если вы посмотрите на свой LDPI
Экран изображения контактов там на самом деле немного слишком маленький, и немного слишком маленький в вашем HDPI
экран. Только на MDPI
экран выглядит совершенно правильно (потому что целевая плотность по умолчанию MDPI
).
Вы можете создать желаемую высоту и ширину, используя dp
а затем преобразовать его в px
чтобы получить правильный размер для масштабированного изображения.
Скажем, вы хотите, чтобы ваше изображение было 32x32dp.
int reqWidth = dpToPx(context, 32);
int reqHeight = dpToPx(context, 32);
public static int dpToPx(Context context, int dp) {
return (int) (dp * (context.getResources().getDisplayMetrics().densityDpi / 160f) + 0.5f);
}