I'm showing 96x96 pixel art sprites in imageviews and can't get it to display without applying anti-aliasing (or at least some form of interpolation, if it isn't anti-aliasing), which ruins the sharp edge of the sprites.
How can this be done? I've tried the following methods I've picked up from around the internet, but none work.
METHOD ONE
creating an xml resource file with a bitmap, and setting the anti-alias flag to false. Didn't work at all
METHOD TWO
creating a bitmap object from the resource, and setting the anti-alias flag to false. Didn't work at all.
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dryad_back_large);
BitmapDrawable drawable = new BitmapDrawable(getResources(), bitmap);
drawable.setAntiAlias(false);
playerMonsterImageView.setImageDrawable(drawable);
METHOD THREE
Using sprites which are large enough so that they will never need to be scaled down, only up (512x512 worked for me)
So this third method WORKS, but it seems really ham-fisted, and I was hoping there was a better way of going about this.
Maybe there's something other than anti-aliasing going on here that I've missed?
There are multiple things that may be scaling your image with anti-aliasing.
One is BitmapFactory. For example, if you have a drawable-mdpi resource and the current resource configuration is xhdpi, it will scale at decode time. You can provide options to avoid this, but the easiest way around it is to put these resources in drawable-nodpi.
The canvas also scales the image with bilinear sampling by default. To avoid that, use a Paint with .setFilterBitmap(false) when drawing to the Canvas. One way to do that is by using a custom Drawable which wraps the underlying .draw() call with a DrawFilter modifying the desired paint flags.
public class AliasingDrawableWrapper extends DrawableWrapper {
private static final DrawFilter DRAW_FILTER =
new PaintFlagsDrawFilter(Paint.FILTER_BITMAP_FLAG, 0);
public AliasingDrawableWrapper(Drawable wrapped) {
super(wrapped);
}
#Override
public void draw(Canvas canvas) {
DrawFilter oldDrawFilter = canvas.getDrawFilter();
canvas.setDrawFilter(DRAW_FILTER);
super.draw(canvas);
canvas.setDrawFilter(oldDrawFilter);
}
}
imageView.setDrawable(new AliasingDrawableWrapper(getDrawable(R.drawable.bitmap)));
In "METHOD TWO", set the filter of the Drawable to false with
drawable.setFilterBitmap(false);
instead of using the setAntialias method you mention.
Related
I have reviewed many posts about determining size of images loaded with Glide into an Android ImageView, but they all seem to either refer to the ImageView size, or the original size of the source image. I need to be able to find out what the resulting BOUNDS is of the drawable INSIDE the ImageView.
I am using Glide 4.11.0 with Android targeting API21+. My layout is quite simple:
<ImageView
android:id="#+id/media_image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
app:srcCompat="#drawable/grandparents_grandchildren_translucent" />
The app:srcCompat value is defined in the layout as a sample, at runtime this ImageView has different images chosen by the user to display. The code to do this is also straight-forward:
GlideApp.with(context)
.load(Uri.parse(path))
.apply(RequestOptions.fitCenterTransform())
.error(R.drawable.ic_image_error)
.into(imageView);
I don't have the option to specify a "hard-coded" height/width, as this app runs on many different devices and densities. I am counting on the combination of the ImageView/Glide to size the image appropriately to fit.
Loading and viewing all works fine - but I need to know the actual bounds in pixels on the screen of the device running this app of just the drawable. It is a similar ask to this question from many years ago.
I had this working fine in a small test app (not using Glide). In that code, I just used the image associated with the layout to do my testing of deriving the drawable bounds. I had been doing the following (after layout was complete and the ImageView loaded the image):
RectF bounds = new RectF();
Drawable drawable = mediaImageView.getDrawable();
mediaImageView.getImageMatrix().mapRect(bounds, new RectF(drawable.getBounds()));
Here is a screenshot, with the onDraw() method of our custom drawing surface view which overlays the ImageView adjusted to paint the rectangle it "thinks" represents the bounds of the viewable image:
As you can see the mediaImageView.getImageMatrix().mapRect() method has properly derived the rectangle which represents the bounds of the drawable as visible within the ImageView, as shown by the green rectangle.
When I moved over to the "real" app which uses Glide, the value derived by the mapRect() approach is no longer correct. I suspect that this is because the transform magic that Glide does under the covers and how it communicates with the ImageView are somehow at odds with the above technique for deriving the bounds of the final drawable:
I have also checked via the debugger what the drawable bounds rect is when it comes into the code at this juncture PRIOR to the mapRect() call. In the non-Glide test version the bounds rect matches the original size of the image (4032 pixels high by 3024 wide).
In the Glide version the drawable rect has already been adjusted (presumably by Glide) to be 1332 pixels high and 2000 pixels wide. I presume this is some sort of optimization thing that Glide is doing under the covers. However it does seem odd on the surface because that height/width is A) definitely not how it shows on the mobile device (it is taller than it is wide, as you can see in the screenshot), and B) doesn't match scale wise with the requested transform (fitCenterTransform). However, since it is displaying correctly there must either be some other step involved, or the drawable bounds data is simply not being referenced or used at this point.
How can I get the BOUNDS (not just width/height) of the drawable as viewed?
I have found a work-around thanks to this post.
The key is to intercept the Glide flow prior to it doing any operations on the drawable, but after it has snagged it from the network, and using its intrinsic width/height as the source for calculating the bounds as transformed.
I ended up changing the code that initializes our "DrawView" with the bounds of the drawable. The change involved these steps:
Get the correct width/height of the drawable prior to Glide doing any transformations.
Calculate the drawable bounds manually using a matrix conversion that takes into account the width/height of the ImageView and its scale type (ours is always FitCenter, so the code used is fixed to using that).
Use the resulting bounds to initialize our custom "drawView" view.
Fortunately, I can use a RequestListener within Glide to snag the size after Glide grabs it from the URL but before it performs the "into" the ImageView. Within this listener I can perform the matrix operations that match the transformation approach I have requested in order to get the correct bounds. Then I can call the method that initializes our DrawView custom view object appropriately:
GlideApp.with(this)
.load(Uri.parse(currentMedia.getUriPathToMedia()))
.listener(new RequestListener<Drawable>() {
#Override
public boolean onLoadFailed(#Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
return false;
}
#Override
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
RectF visualizedDrawableRect = new RectF(); // left, top, right, bottom
Matrix m = new Matrix();
RectF srcDrawableRect = new RectF(0, 0, resource.getIntrinsicWidth(), resource.getIntrinsicHeight());
RectF imageViewRect = new RectF(0, 0, mediaImageView.getWidth(), mediaImageView.getHeight());
if ( m.setRectToRect(srcDrawableRect, imageViewRect, Matrix.ScaleToFit.CENTER) ) {
m.mapRect(visualizedDrawableRect,srcDrawableRect);
// INITIALIZE OUR DRAWVIEW HERE
initializeDrawView(visualizedDrawableRect);
}
return false;
}
})
.apply(RequestOptions.fitCenterTransform())
.error(R.drawable.ic_image_error)
.into(mediaImageView);
One remaining question is whether I have to be concerned about using an anonymous listener in this way? Am I incurring a memory leak?
I'm having problems using Bitmap.createScaledBitmap in a custom widget's canvas in Android.
The widget is supposed to display a scaled non anti-aliased version of the given resource.
There's the following code in the widgets class:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Bitmap bmp = BitmapFactory.decodeResource(this.getResources(), R.drawable.btn);
bmp = Bitmap.createScaledBitmap(bmp, bmp.getWidth()*3, bmp.getHeight()*3, false);
canvas.drawBitmap(bmp, 0, 0, null);
}
The "false" parameter on createScaledBitmap is supposed to turn off anti-aliasing filtering. The result is scaled, but smooth. Changing the value to "true" makes no difference.
Is there another way to achieve the result I want?
After much fiddling, it seems that the problem is getting bitmaps via getResources(). They will always get filtered, no matter which method of drawing is used.
My workaround was to put all my images in the /assets folder and pull them via getAssets(). (Still not sure how bad of a practice this is...)
To place a custom font on my widget I am creating a bitmap with the font inside of it, and then placing that bitmap into my RemoteViews. However the text on the bitmap is pretty fuzzy, and looks really pixelated compared to how crystal clear it is within an Activity.
There are quite a few apps already that use custom fonts on a widget. I haven't found a solid way to do this yet though. If anyone has a better solution to this problem I would love to hear it!
Right now this is what I am using to create the bitmap and place it on the widget:
RemoteViews widgetView = new RemoteViews(this.getPackageName(), R.layout.widget);
widgetView.setImageViewBitmap(R.id.widgetImage, buildBitmap());
with
public Bitmap buildBitmap() {
Bitmap bitmap = Bitmap.createBitmap(160, 84, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
TextPaint textPaint = new TextPaint(TextPaint.LINEAR_TEXT_FLAG | TextPaint.ANTI_ALIAS_FLAG);
textPaint.setTypeface(getFont());
textPaint.setStyle(Style.FILL);
textPaint.setColor(fontColor);
textPaint.setTextSize(fontSize);
StaticLayout staticLayout = new StaticLayout(textString, textPaint, bitmap.getWidth(), Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
staticLayout.draw(canvas);
return bitmap;
}
The culprit Widgets! The one on the top is with the custom font, the middle image is with the default Sans font on a bitmap, to show that the text is fuzzy no matter what font is used.
The Last image is what the text looks like when using the default remoteView.setTextViewText("Text"); instead of a bitmap.
Any assistance on this problem would be greatly appreciated!
You can't assume 160x84. That's a very small size. When scaled it will likely be fuzzy as you can see. Instead, you'll want to actually measure out the bitmap size based on measurements of your text at a given sp. Paint.measureText is useful sometimes for these kinds of things to get the width, but you'll need height as well to do it right, so getTextBounds might be more useful. You'll probably need to add some padding too, but this should get you started. As you can see in your pictures, the clarity is not the only problem you have to deal with. You'll have to figure out word wrapping too (perhaps here is where Paint.measureText will come in handy).
The ImageView you are using requires android:scaleType="matrix" attribute to be added. Edit your widget layout xml and add that attribute where needed. The ImageView will then not try and scale your image to fit the view.
As previous posts have said your pixel dimensions are wrong. You should be maintaining a record of the size of each of your widgets so you can later use it for creating the bitmap.
If you want help working this bit out I can post some code. Accept this answer and post another about that question specifically and I will answer there with the code.
I'm sorry if this topic has been brought before, but all my searches on the web and google groups did not help me.
I'm currently developing a little game with the Android SDK, and use hi-res bitmaps that I resize accordingly to match the device's resolution (letting the system do it for me is
not "crisp" enough).
I use a SurfaceView, on which I paint in one pass a canvas filling the whole surface. The paint uses setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)) to allow masking.
Beforehand, I retrieve various bitmaps -- which are resized at initialization with createScaledBitmap() and put in a cache -- and I apply the bitmaps with a paint on this canvas, before drawing this canvas on the SurfaceView.
My problem is, whatever I try, whatever paint settings I use (dithering, antialias, etc..), the resized bitmaps are not antialiased and the drawing present jagged edges. I tried everything.
The only little success I had was using inSampleSize to approach the desired scaled size and force a first pass of antialiasing, before invoking createScaledBitmap on the retrieved
hi-res bitmap, but it is not beautiful enough. I just can't allow to create multitudes of pre-sized bitmaps for every combination of resolution. What did I miss ?
Thanks a lot in advance
First when you load your bitmap you make sure that you don't lose any image quality by settings options to argb_8888:
Options options = new Options();
options.inScaled = false;
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
Bitmap pic = BitmapFactory.decodeResource(getResources(), R.id.pic, options);
When you scale the bitmap turn on the filter:
pic = Bitmap.createScaledBitmap(pic, screenW, screenH, true);
However if one streaches the image too much inevitably it degrades in quality.
When you use paint you can improve quality but lose on speed with turning on ditherig and filtering:
Paint paint = new Paint();
paint.setFlags(Paint.DITHER_FLAG);
paint.setFilterBitmap(true);
Finally the entire activity window could be set on argb_4444 instead on argb_8888 (OS < 2.3). You can chage this if you instert this line before setContentView:
getWindow().setFormat(PixelFormat.RGBA_8888);
If it comes down to it, you can manually antialias without too much trouble. Just apply a simple lowpass filter (something like an NxN average) to the pixel data before asking the bitmap object to rescale itself.
you may clear canvas buffer by youself! such as follows:
canvas.drawColor(Color.TRANSPARENT, android.graphics.PorterDuff.Mode.CLEAR);
I would like to create a 'graph paper' look to the Bitmap I am drawing via a Canvas, and trying to figure out the best way to do this.
I can't pass a source Bitmap containing the graph paper background to the Canvas constructor, as I am getting the Canvas in a SurfaceView via the .lockCanvas() call.
Some solutions I've tried:
I've tried implementing this solution in my SurfaceView's Thread.run(), but the issue I believe is when the BitmapDrawable is converted to a Bitmap... it loses the tiling properties.
canvas = mSurfaceHolder.lockCanvas(null);
BitmapDrawable TileMe = new BitmapDrawable(BitmapFactory.decodeResource(getResources(), R.drawable.editor_graph));
TileMe.setTileModeX(Shader.TileMode.REPEAT);
TileMe.setTileModeY(Shader.TileMode.REPEAT);
Bitmap b = TileMe.getBitmap();
canvas.drawBitmap(b, 0, 0, null);
If I use the Canvas.drawBitmap method that takes a destination RectF as a parameter, it looks like the bitmap will be tiled to fill the RectF... but how do I declare a RectF reliably that fills the entire view area?
Setting the Activities background to the desired graph paper look also doesn't work, as the bitmap/canvas layout is opaque and blocks that from being seen.
Any ideas how to achieve this?
You have two easy solutions:
Either use a BitmapDrawable, but instead of extracting the Bitmap, just call BitmapDrawable.draw(Canvas). Don't forget to set the drawable's bounds to fill your drawing area.
Create a Paint with a BitmapShader and draw a rectangle with it (this is basically what BitmapDrawable does).
I'm sure there is a way to get a tiled effect using a SurfaceView. Unfortunately, it looks like you can't use the BitmapDrawable with a canvas. So you would probably have to implement you own custom tiling method by creating your own series of Rect's on the Canvas and drawing a scaled bitmap to each one.
It honestly wouldn't be that hard. Just get the width/height of the view, and create an array of Rect's based on this data that you will draw the Bitmap to.
Alternatively, if you don't need to make modifications to the actual tiled background on the fly, just draw it as a background and draw the SurfaceView on top of it. That post you linked provided multiple solutions to tiling a BitmapDrawable that you could implement.