I have a problem that is driving me nuts.
I had 4 functions that would resize images correcly throuhout UI using density (1.5) and width/height values reported back by views, bitmaps, screen etc.
This worked excellent on SG II. But on a HTC Wildfire which reports density 0.75 ... some images got about 25% too small visually on screen... But when I decided to override the density to 1.0 those images suddenly fit while other became crisp, but no longer sized as correctly... And it is driving me nuts...
I can only conlcude hat I am some places comparing apples to pears (pixels and DIPs), but I can not get my tests to fit with what I read, so in an effort to be 100% sure of some things:
...
Will I get pixels underneath here? or DIPs? :
int bitmapWidth = bitmap.getWidth();
Suppose underneat is an imageview containing a bitmap. Pixels or DIPs?
int widthParent = view.getWidth();
Underneath is actually DIPs, right?
getContext().getResources().getDisplayMetrics().widthPixels;
...
Here is my code that is not working (this code is giving a bitmap that visually takes maybe 2/3s width where it should take 100%) If you check comments the pixels values are apparently correct, but end result is not correct:
vto.addOnGlobalLayoutListener(
new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
mainLogo.getViewTreeObserver().removeGlobalOnLayoutListener(this);
mainLogo.setScaleType(ImageView.ScaleType.CENTER_INSIDE); // mainLogo = ImageView
SharedCode.sharedUtilScaleImage_Width(mainLogo, false);
}
}
);
// ...
public static void sharedUtilScaleImage_Width(ImageView view, boolean tryBackground)
{
Drawable drawing = null;
boolean useBackground = false;
if (drawing == null) {
drawing = view.getDrawable();
}
if (drawing == null) {
if (tryBackground) {
drawing = view.getBackground();
useBackground = true;
}
}
if (drawing == null) {
return;
}
if (!(drawing instanceof BitmapDrawable)) {
return;
}
Bitmap bitmap = ((BitmapDrawable)drawing).getBitmap();
if (bitmap == null) {
return;
}
//--
int bitmapWidth = bitmap.getWidth(); // returns 770 (which is correct checking on disk)
int bitmapHeight = bitmap.getHeight();
// float density = 1;
// density = MicApp.getContext().getResources().getDisplayMetrics().density; // 1.5
float widthScreen = MicApp.getContext().getResources().getDisplayMetrics().widthPixels; // returns 480
int widthParent = view.getWidth(); // returns 480, should be same as screen
//--
float xScale = ( (float) widthParent / (float) bitmapWidth);
Matrix matrix = new Matrix();
matrix.postScale(xScale, xScale);
//--
Bitmap scaledBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmapWidth, bitmapHeight, matrix, true);
int bitmapWidth2 = scaledBitmap.getWidth(); // 480
int bitmapHeight2 = scaledBitmap.getHeight();
//--
BitmapDrawable result = new BitmapDrawable(scaledBitmap);
//--
if (useBackground) {
LayoutParams layoutparams = new LayoutParams(bitmapWidth, bitmapHeight);
view.setLayoutParams(layoutparams);
view.setBackgroundDrawable(result);
}
else {
view.setImageDrawable(result);
}
}
There are several steps between decoding an image and displaying it. Those steps are typically used to display images from resources at the correct scale. If you put an image just in /res/drawable-mdpi/ but not in other folders it can still displayed at the correct size on devices with other densities.
If you want to load an image that is meant for mdpi(160dpi) on an x-hdpi(320dpi) device and want it to appear at the same size you'll need to double it's pixel size in effect. E.g. a 100x100 pixel image would need to be displayed at 200x200 pixels.
The places that are involved in scaling are
Decoding the image (see e.g. BitmapFactory.Options#inDensity). BitmapFactory could already decode a 100x100 png image into a 200x200 image.
The Bitmap's own density. This is a property of every bitmap and it is used to store for what density an image is meant. If BitmapFactory would decode the 100x100 mdpi image to 200x200 already that densitiy would essentially say "I'm x-hdpi". You could also disable scaling while decoding and would get a 100x100 image with mdpi density. Displaying that image would require drawing the images scaled at 2x.
The BitmapDrawable's target density which is generally the device's density. If a BitmapDrawable has to draw a Bitmap where densities don't match it will draw a scaled version.
Code from BitmapDrawable, mTargetDensity should be device density.
private void computeBitmapSize() {
mBitmapWidth = mBitmap.getScaledWidth(mTargetDensity);
mBitmapHeight = mBitmap.getScaledHeight(mTargetDensity);
}
And relevant pieces from Bitmap, mDensity is the bitmap's density
public int getScaledWidth(int targetDensity) {
return scaleFromDensity(getWidth(), mDensity, targetDensity);
}
static public int scaleFromDensity(int size, int sdensity, int tdensity) {
if (sdensity == DENSITY_NONE || sdensity == tdensity) {
return size;
}
// Scale by tdensity / sdensity, rounding up.
return ((size * tdensity) + (sdensity >> 1)) / sdensity;
}
Scaling options of e.g. ImageView. If you want to display an ImageDrawable that represents an image that is (due to scaling or actually) 200x200 px inside an ImageView that is actually 400x300 px on the screen you need to scale the image again.
Using ImageView.ScaleType.CENTER_INSIDE would draw the image at 200x200 px because CENTER_INSIDE is only scaling the image if it is larger than the view. FIT_CENTER would scale it up to 300x300. Just CENTER does not scale at all. It just centers.
CENTER_INSIDE + a 100x100 mdpi image from above would still draw it as 200x200px because of all the scaling factors stored in BitmapDrawable / Bitmap.
There are also several other places in the BitmapFactory > Bitmap > BitmapDrawable > ImageView chain like the BitmapDrawable(Bitmap bitmap) constructor that says
This constructor was deprecated in API level 4. Use BitmapDrawable(Resources, Bitmap) to ensure that the drawable has correctly set its target density.
which could affect image scaling in undesired ways.
-> to prevent all the scaling
decode without scaling
Make sure your bitmap's density is either the current device density or simply Bitmap#setDensity(Bitmap.DENSITY_NONE)
Don't apply scaling while drawing via ImageView.ScaleType.CENTER.
Related
I want to have a background image that scales to fit any screen size in Android. The image is static and doesn't need to scroll. I made the image at 4K resolution to cover what is a likely resolution to exist on tablets in the next 2-3 years (2560 x 1600 already exist). The image is a JPG with a 137KB file size. Similar resolution images seem to work fine in Android web browsers. Why am I getting a lot of slow down in Android (on Samsung Galaxy S3, which should have plenty of CPU/RAM to handle an image like this)? I don't feel like I am doing anything out of the ordinary.
This loads the image in the XML layout. The image is currently stored in drawable-nodpi.
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/logo_background"
android:scaleType="centerCrop" />
Making different size images for each category of screen resolution is difficult as I cannot find information on what the current maximum resolution for a device in each category is only a minimum.
I want to use the same background image again and again between a variety of fragments. Is there a way to have the image resized once to the width of the screen (preferably asynchronously) and then load that resized image each time? Could this be done with Picasso?
Please don't give answers like "of course larger images result in performance issues" or link me to Google's Supporting Different Densities. This is a real issue that is going to become more of an issue as screen resolutions continue to increase. I am amazed that handling and resizing large images is not already optimised in the ImageView class, which makes me think I am doing something wrong.
The problem is that what you are trying to do is not relying on the SDK. By having one image and having to change the image on runtime, you are causing more work to be done on the UI thread in onDraw().
Of course you would be able to create a Bitmap for a specific size, but why do such complicated work when you can rely on the SDK?
Currently there are a bunch of different folders that you can use in order to get what you are looking for, and then in the future you can get a 4k image put into a specific folder. Things like this might work:
drawable-xhdpi
drawable-xxhdpi
drawable-xlarge-xhdpi - May not be specific enough for what you are trying to accomplish
drawable-sw600dp - This allows you to specify a folder for an image where the screen width is greater than 600dp. This qualifier will probably be helpful for your case, in the future where you will be using 4k images.
You dont even need Picasso mate.Here you get the screen size:
LinearLayout layout = (LinearLayout)findViewById(R.id.YOUR_VIEW_ID);
ViewTreeObserver vto = layout.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
this.layout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int width = layout.getMeasuredWidth();
int height = layout.getMeasuredHeight();
}
});
And here you resize your image with your new dimensions:
public Bitmap getResizedBitmap(Bitmap bm, int newHeight, int newWidth){
int width = bm.getWidth();
int height = bm.getHeight();
float scaleWidth = ((float) newWidth) / width;
float scaleHeight = ((float) newHeight) / height;
// create a matrix for the manipulation
Matrix matrix = new Matrix();
// resize the bit map
matrix.postScale(scaleWidth, scaleHeight);
// recreate the new Bitmap
Bitmap resizedBitmap = Bitmap.createBitmap(bm, 0, 0, width, height, matrix, false);
return resizedBitmap;
}
Using a matrix to resize is relatively fast. Although user1090347s answer would be best practice.
The problem is that android uses Bitmap to render images to canvas. It is like BMP image format for me. So, you have no gain from JPG format, cuz all information lost from jpg conversion are lost forever and you will end up will fullsize bitmap anyway. The problem with big resolution is that, you have to address few bytes for every pixel, no conversion applied! In particular, smaller devices have lower memory class as bigger ones. So, you have to handle the image resolution based on device screen size and memory class.
You can properly convert your background bitmap at runtime with these helper functions:
public void getScreenSizePixels(Resources resources, int widthHeightInPixels[/*2*/])
{
Configuration config = resources.getConfiguration();
DisplayMetrics dm = resources.getDisplayMetrics();
double screenWidthInPixels = (double)config.screenWidthDp * dm.density;
double screenHeightInPixels = screenWidthInPixels * dm.heightPixels / dm.widthPixels;
widthHeightInPixels[0] = (int)(screenWidthInPixels + .5);
widthHeightInPixels[1] = (int)(screenHeightInPixels + .5);
}
--
public static Bitmap getBitmap(byte[] imageAsBytes, int reqWidth, int reqHeight) {
BitmapFactory.Options opt = new BitmapFactory.Options();
opt.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(
imageAsBytes,
0,
imageAsBytes.length,
opt);
int width = opt.outWidth;
int height = opt.outHeight;
int scale = 1;
while (reqWidth < (width / scale) || reqHeight < (height / scale)) {
scale++;
}
//bitmap.recycle();
opt.inJustDecodeBounds = false;
opt.inSampleSize = scale;
Bitmap bitmap = BitmapFactory.decodeByteArray(
imageAsBytes,
0,
imageAsBytes.length,
opt);
return bitmap;
}
I'm using a SurfaceView in Android. The application is always in landscape mode.
In my res/drawable folder I have a large background that is also in landscape mode but larger than most display sizes.
Now I would like to set it as the background of the SurfaceView so that it perfectly fits into the display. So the image's height should be equal to the display's height.
How do I do that? Do I have to use alternative resources for ldpi, mdpi, hdpi and xhdpi? But even then I don't know the exact height of the display.
I'm currently using:
canvas.drawBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.background), 0, 0, null);
I tried to resize the bitmap using a matrix. But then the quality of the image becomes really poor, doesn't it?
(Of course, the solution should be performant, too.)
protected Bitmap scaled = null;
public void surfaceCreated(SurfaceHolder arg0) {
Bitmap background = BitmapFactory.decodeResource(getResources(), R.drawable.background);
float scale = (float)background.getHeight()/(float)getHeight();
int newWidth = Math.round(background.getWidth()/scale);
int newHeight = Math.round(background.getHeight()/scale);
Bitmap scaled = Bitmap.createScaledBitmap(background, newWidth, newHeight, true);
}
public void onDraw(Canvas canvas) {
canvas.drawBitmap(scaled, 0, 0, null); // draw the background
}
I am working on a simple wallpaper app of some images that I have. They are .png files in drawable folders.
Here are some code snippets:
WallpaperManager myWallpaperManager = WallpaperManager.getInstance(getApplicationContext());
....
myWallpaperManager.setResource(R.drawable.image1);
No matter what size or resolution I seem to use, when the wallpaper is set it crops the original image. When I use the same image as a background it is the correct size and shows the entire image. I thought it might be a problem with my emulator so I have tried running it on an actual device (HTC eris) and I am having the same problem. I have made the image the screen size and resolution for the eris and it is still cropping it. I then made the image only one inch high and a resolution of 100 dpi. It was very pixelated on the eris, but still cropped the image.
Any help would be greatly appreciated.
I attempted to add some images to show the before and after, but as a newer user I was prevented from doing so.
Check the values returned by http://developer.android.com/reference/android/app/WallpaperManager.html#getDesiredMinimumWidth() and http://developer.android.com/reference/android/app/WallpaperManager.html#getDesiredMinimumHeight() and try to use these values as the dimensions of your wallpaper.
Maybe I can help.
// 1. Get screen size.
DisplayMetrics metrics = new DisplayMetrics();
Display display = getWindowManager().getDefaultDisplay();
display.getMetrics(metrics);
final int screenWidth = metrics.widthPixels;
final int screenHeight = metrics.heightPixels;
// 2. Make the wallpaperManager fit the screen size.
final WallpaperManager wallpaperManager = WallpaperManager.getInstance(ViewWallpaperActivity.this);
wallpaperManager.suggestDesiredDimensions(screenWidth, screenHeight);
// 3. Get the desired size.
final int width = wallpaperManager.getDesiredMinimumWidth();
final int height = wallpaperManager.getDesiredMinimumHeight();
// 4. Scale the wallpaper.
Bitmap bitmap = getBitmap(); // getBitmap(): Get the image to be set as wallpaper.
Bitmap wallpaper = Bitmap.createScaledBitmap(bitmap, width, height, true);
// 5. Set the image as wallpaper.
try {
wallpaperManager.setBitmap(wallpaper);
} catch (IOException e) {
e.printStackTrace();
}
Note that you should call suggestDesiredDimensions, then call getDesiredMinimumWidth and getDesiredMinimumHeight to get the size to be scaled to.
I had the same problem. I made an image that is the size of the screen and adding padding to the image so that it is as large as the WallpaperManager getDesiredMinimumWidth() and getDesiredMinimumHeight().
It seemed better to have some code automatically add the padding so that is what I used below. Make sure the image is the same size as the screen.
private void setWallpaper() {
try {
WallpaperManager wallpaperManager = WallpaperManager.getInstance(this);
//import non-scaled bitmap wallpaper
BitmapFactory.Options options = new BitmapFactory.Options();
options.inScaled = false;
Bitmap wallpaper = BitmapFactory.decodeResource(getResources(), R.drawable.wallpaper, options);
if (wallpaperManager.getDesiredMinimumWidth() > wallpaper.getWidth() &&
wallpaperManager.getDesiredMinimumHeight() > wallpaper.getHeight()) {
//add padding to wallpaper so background image scales correctly
int xPadding = Math.max(0, wallpaperManager.getDesiredMinimumWidth() - wallpaper.getWidth()) / 2;
int yPadding = Math.max(0, wallpaperManager.getDesiredMinimumHeight() - wallpaper.getHeight()) / 2;
Bitmap paddedWallpaper = Bitmap.createBitmap(wallpaperManager.getDesiredMinimumWidth(), wallpaperManager.getDesiredMinimumHeight(), Bitmap.Config.ARGB_8888);
int[] pixels = new int[wallpaper.getWidth() * wallpaper.getHeight()];
wallpaper.getPixels(pixels, 0, wallpaper.getWidth(), 0, 0, wallpaper.getWidth(), wallpaper.getHeight());
paddedWallpaper.setPixels(pixels, 0, wallpaper.getWidth(), xPadding, yPadding, wallpaper.getWidth(), wallpaper.getHeight());
wallpaperManager.setBitmap(paddedWallpaper);
} else {
wallpaperManager.setBitmap(wallpaper);
}
} catch (IOException e) {
Log.e(TAG,"failed to set wallpaper");
}
}
I've created a function that scales a bitmap directly to a specific surface area. The function first gets the width and height of the bitmap and then finds the sample size closest to the required size. Lastly the image is scaled to the exact size. This is the only way I could find to decode a scaled bitmap. The problem is that the bitmap returned from BitmapFactory.createScaledBitmap(src,width,height,filter) always comes back with a width and height of -1. I've already implemented other functions that use the createScaledBitmap() method with out this error and I can not find any reason why creating a scaled bitmap would produce invalid output. I've also found that if I create a copy of the image bitmap that is mutable causes the same error. Thanks
public static Bitmap load_scaled_image( String file_name, int area) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file_name, options);
double ratio = (float)options.outWidth / (float)options.outHeight;
int width, height;
if( options.outWidth > options.outHeight ) {
width = (int)Math.sqrt(area/ratio);
height = (int)(width*ratio);
}
else {
height = (int)Math.sqrt(area/ratio);
width = (int)(height*ratio);
}
BitmapFactory.Options new_options = new BitmapFactory.Options();
new_options.inSampleSize = Math.max( (options.outWidth/width), (options.outHeight/height) );
Bitmap image = BitmapFactory.decodeFile(file_name, new_options);
return Bitmap.createScaledBitmap(image, width, height, true);
}
I added this function to scale large camera images to a specific number of mega pixels. So a typical area passed in would be 1000000 for 1 megapixel. The camera image after being decoded yields a outWidth of 1952 and a outHieght of 3264. I then calculate the ratio this way I can keep the same height to width ratio with the scaled image, in this case the ratio is 0.598... Using the ratio and the new surface area I can find the new width which is 773 and a height of 1293. 773x1293=999489 which is just about 1 megapixel. Next I calculate the sample size for which to decode the new image, in this case the sample size is 4 and the image is decoded to 976x1632. So I'm passing in a width of 773 a height of 1293.
I was having a similar problem (getting -1 for height and width of the scaled bitmap).
Following this stackOverflow thread:
Android how to create runtime thumbnail
I've tried to use the same bitmap twice while calling the function:
imageBitmap = Bitmap.createScaledBitmap(imageBitmap, THUMBNAIL_SIZE,
THUMBNAIL_SIZE, false);
For some reason, this solved my problem, perhaps it would solve yours too.
I have a Bitmap that is larger than the ImageView that I'm putting it in. I have the ScaleType set to center_inside. How do I get the dimensions of the scaled down image?
Ok. I probably should have been clearer. I needed the height and width of the scaled Bitmap before it's ever drawn to the screen so that I could draw some overlays in the correct position. I knew the position of the overlays in the original Bitmap but not the scaled. I figured out some simple formulas to calculate where they should go on the scaled Bitmap.
I'll explain what I did in case someone else may one day need this.
I got the original width and height of the Bitmap. The ImageView's height and width are hard-coded in the xml file at 335.
int bitmap_width = bmp.getWidth();
int bitmap_height = bmp.getHeight();
I determined which one was larger so that I could correctly figure out which one to base the calculations off of. For my current example, width is larger. Since the width was scaled down to the the width of the ImageView, I have to find the scaled down height. I just multiplied the ratio of the ImageView's width to the Bitmap's width times the Bitmap's height. Division is done last because Integer division first would have resulted in an answer of 0.
int scaled_height = image_view_width * bitmap_height / bitmap_width;
With the scaled height I can determine the amount of blank space on either side of the scaled Bitmap by using:
int blank_space_buffer = (image_view_height - scaled_height) / 2;
To determine the x and y coordinates of where the overlay should go on the scaled Bitmap I have to use the original coordinates and these calculated numbers. The x coordinate in this example is easy. Since the scaled width is the width of the ImageView, I just have to multiply the ratio of the ImageView's width to the Bitmap's width with the original x coordinate.
int new_x_coord = image_view_width * start_x / bitmap_width;
The y coordinate is a bit trickier. Take the ratio of the Bitmap's scaled height to the Bitmap's original height. Multiply that value with the original y coordinate. Then add the blank area buffer.
int new_y_coord = scaled_height * start_y / bitmap_height + blank_space_buffer;
This works for what I need. If the height is greater than the width, just reverse the width and height variables.
Here's how I do it:
ImageView iv = (ImageView)findViewById(R.id.imageview);
Rect bounds = iv.getDrawable().getBounds();
int scaledHeight = bounds.height();
int scaledWidth = bounds.width();
You can use Drawable's getIntrinsicWidth() or height method if you want to get the original size.
EDIT: Okay, if you're trying to access these bounds at the time onCreate runs, you'll have to wait and retrieve them till after the layout pass. While I don't know that this is the best way, this is how I've been able to accomplish it. Basically, add a listener to the ImageView, and get your dimensions just before it's drawn to the screen. If you need to make any layout changes from the dimensions you retrieve, you should do it within onPreDraw().
ImageView iv = (ImageView)findViewById(R.id.imageview);
int scaledHeight, scaledWidth;
iv.getViewTreeObserver().addOnPreDrawListener(
new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
Rect rect = iv.getDrawable().getBounds();
scaledHeight = rect.height();
scaledWidth = rect.width();
iv.getViewTreeObserver().removeOnPreDrawListener(this);
return true;
}
});
Try this code:
final ImageView iv = (ImageView) findViewById(R.id.myImageView);
ViewTreeObserver vto = iv.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
Toast.makeText(MyActivity.this, iv.getWidth() + " x " + iv.getHeight(), Toast.LENGTH_LONG).show();
iv.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
});
Otherwise you can use onSizeChange() by making this view a custom one.