There is a nice post made by the popular Google developer Romain Guy that shows how to use a rounded corners drawable (called "StreamDrawable" in his code ) on a view efficiently.
The sample itself works very well on my Galaxy S3 when in portrait mode, but I have a few issues with it:
if the screen is small (for example on qvga screens), the shown images get cropped.
if I have an input bitmap that is too small than how I wish to show it, the output image has its edges smeared. Even on the Galaxy S3, when you run the sample code and it's on landscape, it looks awful:
I'm still not sure about it (since I use a workaround of scaling the image for using the sample code), but it think that even this solution is a bit slow when being used in a listView. Maybe there is a renderscript solution for this?
It doesn't matter if I use setImageDrawable or setBackgroundDrawable. It must be something in the drawable itself.
I've tried to play with the variables and the bitmapShader, but nothing worked. Sadly TileMode doesn't have a value for just stretching the image, only tiling it in some way.
As a workaround I can create a new scaled bitmap, but it's just a workaround. Surely there is a better way which will also not use more memory than it should.
How do I fix those issues and use this great code?
I think that the solution that is presented on this website works well.
unlike other solutions, it doesn't cause memory leaks, even though it is based on Romain Guy's solution.
EDIT: now on the support library, you can also use RoundedBitmapDrawable (using RoundedBitmapDrawableFactory ) .
I had some size issues with this code, and I solved it.
Maybe this will help you, too:
1) in the constructor store the bitmap in a local variable (e.g. private Bitmap bmp;)
2) override two more methods:
#Override
public int getIntrinsicWidth() {
return bmp.getWidth();
}
#Override
public int getIntrinsicHeight() {
return bmp.getHeight();
}
Best regards,
DaRolla
There underlying problem is that the BitmapShader's TileMode doesn't have a scaling option. You'll note in the source that it's been set to Shader.TileMode.CLAMP, and the docs describe that as:
replicate the edge color if the shader draws outside of its original bounds
To work around this, there are three solutions:
Constrain the size of the view in which the drawable is used to the size of the bitmap.
Constrain the drawing region; for instance, change:
int width = bounds.width() - mMargin;
int height = bounds.height() - mMargin;
mRect.set(mMargin, mMargin, width, height);
To:
int width = Math.min(mBitmap.getWidth(), bounds.width()) - mMargin;
int height = Math.min(mBitmap.getHeight(), bounds.height()) - mMargin;
mRect.set(mMargin, mMargin, width, height);
Scale the bitmap to the size of the drawable. I've moved creating the shader into onBoundsChange() and have opted to create a new bitmap from here:
bitmap = Bitmap.createScaledBitmap(mBitmap, width, height, true);
mBitmapShader = new BitmapShader(bitmap,
Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
Note that this a potentially slow operation and will be running on the main thread. You might want to carefully consider how you want to implement it before you go for this last solution.
Related
I have no 2D graphics and gaming experience. I taught myself by hundreds of mistakes and tens of lost hours. I started with simple Dress Up game. I used Nexus 5x for development where a screen looked OK. When I finished one milestone I tried game on big Lenovo tablet and tiny Samsung Mini phone. It looked horribly.
An original vector PSD file looks perfect, PNG export does not have any issues either. But when I scale the picture it is bumpy. I know it is bitmap. But there is another picture I scale in other place and it looks fine (both pictures are 32 bit):
When I play some game from Google Play they never have scaling issues. How are they implemented? Do they use vectors? Is there some trick?
I put everything into assets folder and though it took 1 MB. I decompiled some apk and they do not have set of variants for each resolution. Though they scale pictures nicely.
Some source code snippets:
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
this.w = w;
this.h = h;
canvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
drawCanvas = new Canvas(canvasBitmap);
}
protected void onDraw(Canvas canvas) {
if (baseBitmap != null) {
double scale = Math.min((double) w / (double) figure.getW(), (double) h / (double) figure.getH());
figure.setScaleRatio(scale);
int sw = (int) (scale * figure.getW());
int x = ((w - sw) >> 1);
figure.setX(x);
paintDressPart(canvas, figure, figure.getMain());
if (displayedParts != null) {
for (DressPart dressPart : figure.getParts()) {
if (displayedParts.contains(dressPart.getId())) {
paintDressPart(canvas, figure, dressPart);
}
}
}
}
}
private void paintDressPart(Canvas canvas, Figure figure, DressPart part) {
double scale = figure.getScaleRatio();
int sh = (int) (scale * part.getH());
int sw = (int) (scale * part.getW());
int sdx = (int) (scale * part.getDestX());
int sdy = (int) (scale * part.getDestY());
scaledRect.set(figure.getX() + sdx, sdy, figure.getX() + sw + sdx, sh + sdy);
Rect partRect = part.getRect();
canvas.drawBitmap(baseBitmap, partRect, scaledRect, canvasPaint);
}
And if you want to see it yourself I preparred complete project on GitHub so you can download it and run it yourself:
https://github.com/literakl/DressUp
Update
Downscaling sucks as well. But thanks to Paavnet's comment I realized that Paint matters. See difference on a picture:
I ran demo app on 3,2" AVD image. The picture 278x786 is scaled to 107x303.
October Update
I rewrote the app to use resource folder instead of assets. Android scales pictures and then I rescale it again. Though it looks better than when I do not use Android resources scaling. Resources scaled picture looks usually better than unscaled nodpi / assets picture.
I found that mdpi works best. I even had xxhdpi pictures and it looked worse than mdpi. I think that even on xxhdpi device! But it may be a trouble of the picture that it was not painted well. Android resource scaling may smooth edges on lines.
I faced same issue .. that scaling not that smooth .. best standard method
Bitmap resized = Bitmap.createScaledBitmap(yourBitmap, newWidth, newHeight, true);
You can find more optimized size-convertor in web .. but actually best solution I ended with use :
1st. Use Vector SVG resource .. this is 100% working with perfect quality. just be sure that SVG elements are compatible with android ( see : https://developer.android.com/studio/write/vector-asset-studio.html )
pros:
best quality
efficient size
editable easily
cons:
not all elements are supported.
Or
2nd. provide high resolution image (one is enough in xxxhdpi )
pros:
good quality
cons:
might lead to performance issue
I hope that may help,'.
Do they use vectors?
SVG/Vector is a handy solution but vector art requires extremely high precision, making it unsuitable for many art styles.It's easy to use vector art with basic shapes but it can be hard to add a simple detail with SVG symbolic styles although it can added easily with paint with png,jpeg. You can watch performance pattern videos on SVG where googlers suggest SVG for icon designs.Most games don't use it for rich and complex images.
So i won't go for SVG specially where small detailing matter a lot in games.
How are they implemented? Is there some trick?
One way is just publish the app with a single regular assets support (MDPI,HDPI) and if the screen density required high resolution then show a dialog to the user, asking him if he wants to download the high-resolution assets.
You can have a higher density images and scale it down at run-time based on phone density requirement using DisplayMetrics.Ideally you should keep the same aspect ratio.Fitting a game made for a 4:3 display looks hideous on a 16:9, but fitting a 16:9 on to a 16:10, nobody might notice.read for more.
You can use this way to easily get the height and width of screen(considering you are using SurfaceView as best practice )
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
int screenHeight = metrics.heightPixels;
int screenWidth = metrics.widthPixels;
To get new width-height on rotation :
surfaceChanged(SurfaceHolder holder, int format, int width, int height)
That method gives you the width/height of your surface, so you could now use:
Bitmap.createScaledBitmap (Bitmap src, int dstWidth, int dstHeight, boolean filter)
From the Bitmap class to re-size your images in proportion to the size of the surface you're drawing onto.You can look into some technique to find appropriate scaling factors.
OpenGL,kotlin can give you advantage for creating rich graphic app.With these you can control the memory and GPU performance which basically are the back-bone of most games plus you can take advantage of controlling the rendering more efficiently with GPU and native memory space plus there is lot more you can think of, it will take some time if you not familiar with these but once you get it they can do many magical tricks.
Webp will help you to reduce size of large images to a considerable difference.Should try webp. (suggestion , webp has a little issue for alpha channel of image on some devices but too good for image size compression)
Conclusion : Implements best practices using OpenGL, SurfaceView,NDK plus loading images efficiently using scaling factors docs link and should try this too.
you can also take a look into 9patch images where you can put constraints to just scale some portion of the image instead of whole.it's quite useful.
Update : As suggested , if you are going to use higher resolution image then also look into this and this to disable scaling or manually calculation scaling factor.Good Luck
Note: Guys feel free to edit this answer with valid info if you like.
Using MikeOrtiz's awesome ImageView implementation with touch and zoom events, I wanted to crop a picture taken with the camera to match the zoom. Using his method...
// Return a Rect representing the zoomed image.
RectF getZoomedRect();
...I tried cropping the resulting picture bitmap to the zoom size like so:
RectF zoomCoordinates = mTouchImageView.getZoomedRect();
Bitmap croppedBitmapToOverview = Bitmap.createBitmap(
AppResources.sCurrentImage,
((int) zoomCoordinates.left),
((int) zoomCoordinates.top),
((int) zoomCoordinates.width()),
((int) zoomCoordinates.height()));
However I get a "must be bigger than 0" error with this. While debugging I noticed ALL values were 0 due to casting to an Integer. The real values however go something like this:
//Log.d print for each of those fields without the int cast
Left 0.34047672
Top 0.20797288
Width 0.33333334
Height 0.3429547
So there's my problem, but I can't see how to fix this. I've never worked with bitmaps before or canvas, Rect, etc.
Is there some tweaking I could do to these values, or should I take a different approach altogether?
Got around the problem by simply taking a "screenshot" of the View of sorts. This got me a Bitmap with the picture as it was zoomed
mTouchImageView.setDrawingCacheEnabled(true);
AppResources.sCurrentImage = Bitmap.createBitmap(mTouchImageView.getDrawingCache());
multiply the coordinates with the size of your image like
(int)(zoomCoordinates.left * imageSize)
I want to use coverflow view in my app. To get the reflection image part i have used the following code - http://www.androidsnippets.com/create-image-with-reflection
I have seen lot of forums/discussions about dithering and tileMode, I have tried all that discussed but nothing works for me.
FYI - I am creating a bitmap dynamically not using any bitmap in layouts. And I have attached the image to show how bad it is:
I have listed below what I have tried to solve this issue.
1. getWindow().setFlags(WindowManager.LayoutParams.FLAG_DITHER, WindowManager.LayoutParams.FLAG_DITHER);
2. getWindow().setFormat(PixelFormat.RGBA_8888);
3. BitmapDrawable baseImageDawable = new BitmapDrawable(getResources().openRawResource(imageId));
baseImageDawable.setDither(true);
baseImageDawable.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
Bitmap originalImage = baseImageDawable.getBitmap();
int width = originalImage.getWidth();
int height = originalImage.getHeight();
Bitmap reflectionImage = Bitmap.createBitmap(originalImage, 0, 0, width, height, matrix, true);
But still the reflection image is very ugly.
How can I solve this?
Hmmm. I "think" your problem is that your bitmap is not being created as ARGB_8888 (despite your setFormat call). I would "suggest," instead of creating the Bitmap drawable using openRawResource, use BitmapFactory and make sure you specify Bitmap.Config.ARGB_8888. Then make your drawable from the bitmap. Just a thought--hard to guess at this distance without being able to step with eclipse. You might find this article interesting: http://www.curious-creature.org/2010/12/08/bitmap-quality-banding-and-dithering/
Edit: So, the problem, apparently, was in the call to createBitmap being used to allocate the reflectionImage. Moral of the story (IMHO): never use createBitmap methods that do not allow you to explicitly specify Bitmap.Config. Otherwise, as Forrest Gump might say, you never know what you're gonna get.
I found the location where should I set the config. While creating the reflection bitmap set the config as ARGB_8888. Its working now.
I am writing a View that should show a drawable that seems to "never end".
It should be twice or third the displaysize and move slow through the display.
Therefore I studied some samplecode by Google and found the important Lines
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
canvasWidth = width;
canvasHeight = height;
float sf = backgroundImage.getWidth() / canvasWidth;
backgroundImage = Bitmap.createScaledBitmap(backgroundImage,
(int) (canvasWidth * sf), canvasHeight, true);
}
To rescale the image and than
// decrement the far background
backgroundXPos = backgroundXPos - DELTAMOVE;
// calculate the wrap factor for matching image draw
int newFarX = backgroundImage.getWidth() - (-backgroundXPos);
// if we have scrolled all the way, reset to start
if (newFarX <= 0) {
backgroundXPos = 0;
// only need one draw
canvas.drawBitmap(backgroundImage, backgroundXPos, 0, null);
} else {
// need to draw original and wrap
canvas.drawBitmap(backgroundImage, backgroundXPos, 0, null);
canvas.drawBitmap(backgroundImage, newFarX, 0, null);
}
To draw the moving image. The images is already moving, it's fine.
But, and this is the point of my question, the image looks very ugly. Its original is 960*190 pixels by 240ppi. It should be drawn inside a view with 80dip of height and "fill_parent" width.
It should look same (and good) on all devices. I have tried a lot but I don't know how to make the picture look nice.
Thanks for your help.
Best regards,
Till
Since you're saying that it's a never ending drawable, probably you're writing a game of some sort. If your image is a pixel-art type, then you don't want any scaling; pixel-art-type images cannot be scaled and keep its crisp look (you can try using nearest neighbor interpolation and scaling to an integer multiple of the original, which sometimes might work, but sometimes you will still need manual tweaks). This is the rare case where you actually would need to have different image resource for different screen resolutions.
Otherwise you might want to use a vector image, but if -- as you said -- your original is a high resolution image, then vector image probably won't help much here.
btw, you probably want to show some screenshot. "Looks ugly" is just as helpful as saying my code does not work.
Just a guess, but instead of passing a null paint to your drawBitmap() calls, try making a paint with bitmap filtering disabled:
Paint p = new Paint();
p.setFilterBitmap(false);
canvas.drawBitmap(backgroundImage, backgroundXPos, 0, p);
Hope that helps.
Say I have a somewhat large (i.e. not fit in most phones' memory) bitmap on disk. I want to draw only parts of it on the screen in a way that isn't scaled (i.e. inSampleSize == 1)
Is there a way to load/draw just the part I want given a Rect specifying the area without loading the entire bitmap content?
I'm quite confident this is possible since you can load a really large bitmap file into an ImageView without problems so there must be some sort of a built-in way to handle large bitmaps... and after a few attempts, I've found a solution:
Instead of loading the entire bitmap and manually draw it yourself, load it as a Drawable instead:
InputStream mapInput = getResources().openRawResource(
R.drawable.transit_map);
_map = Drawable.createFromStream(mapInput, "transit_map");
_map.setBounds(0, 0, _mapDimension.width(), _mapDimension.height());
I'm using a resource file but since you can use Drawable.createFromStream to load image from any InputStream, it should works with arbitrary bitmap.
Then, use the Drawable.draw method to draw it onto the desired canvas like so:
int left = -(int) contentOffset.x;
int top = -(int) contentOffset.y;
int right = (int) (zoom * _mapDimension.width() - contentOffset.x);
int bottom = (int) (zoom * _mapDimension.height() - contentOffset.y);
_map.setBounds(left, top, right, bottom);
_map.draw(canvas);
As in the above case, You can also scale and translate the bitmap as well by manipulating the drawable's bounds and only the relevant parts of the bitmap will be loaded and drawn onto the Canvas.
The result is a pinch-zoomable view from just one single 200KB bitmap file. I've also tested this with a 22MB PNG file and it still works without any OutOfMemoryError including when screen orientation changes.
Now it's very relevant: BitmapRegionDecoder.
Note: available since Android SDK 10
It can easily be done by using RapidDecoder.
import rapid.decoder.BitmapDecoder;
Rect bounds = new Rect(10, 20, 30, 40);
Bitmap bitmap = BitmapDecoder.from("your-file.png")
.region(bounds)
.decode();
imageView.setImageBitmap(bitmap);
It supports down to Android 2.2 (API Level 8).
Generally speaking, that isn't possible, particularly since most image formats are compressed, so you don't even know which bytes to read until you've extracted the uncompressed form.
Break your image up into small tiles and load just the tiles you need to cover the region you want to display at runtime. To avoid jittery scrolling, you might also want to preload tiles that are just out of sight (the ones that border the visible tiles) on a background thread.