Xml drawable is pixelated when enlarged - android

I am using this image:
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="39dp"
android:viewportWidth="32"
android:viewportHeight="39">
<path
android:pathData="M15.8575,6.5831L15.8575,1.0031C15.8575,0.1031 14.7775,-0.3369 14.1575,0.3031L6.5575,7.8831C6.1575,8.2831 6.1575,8.9031 6.5575,9.3031L14.1375,16.8831C14.7775,17.5031 15.8575,17.0631 15.8575,16.1631L15.8575,10.5831C23.3175,10.5831 29.2175,17.4231 27.5775,25.1631C26.6375,29.7031 22.9575,33.3631 18.4375,34.3031C11.2975,35.8031 4.9375,30.9031 3.9775,24.2831C3.8375,23.3231 2.9975,22.5831 2.0175,22.5831C0.8175,22.5831 -0.1425,23.6431 0.0175,24.8431C1.2575,33.6231 9.6175,40.1231 19.0775,38.2831C25.3175,37.0631 30.3375,32.0431 31.5575,25.8031C33.5375,15.5431 25.7375,6.5831 15.8575,6.5831Z"
android:strokeWidth="1"
android:fillColor="#fff"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M-34,-30h100v100h-100z"
android:strokeWidth="1"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>
as drawableStart in a button
android:drawableStart="#drawable/ic_restart"
I am using focus navigation, so in onFocusChanged of the button I am enlarging it with this extension:
fun View.enlarge(turnOn: Boolean) {
if (turnOn) {
translationZ = 1f
scaleX = 1.2f
scaleY = 1.2f
} else {
translationZ = 0f
scaleX = 1.0f
scaleY = 1.0f
}
}
It works very well, however the drawable is pixelated after I enlarge it even though it is xml. The image loses its quality. What is causing this and how can I avoid it and keed the image looking good?
EDIT: tried different approach according to answer below, but was not successful
override fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
if (gainFocus) {
imageView.layoutParams.height = 60
imageView.layoutParams.width = 60
} else {
imageView.layoutParams.height = 50
imageView.layoutParams.width = 50
}
}
or
override fun onFocusChanged(gainFocus: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
if (gainFocus) {
imageView.drawable.setBounds(0,0,60,60)
} else {
imageView.drawable.setBounds(0,0,50,50)
}
}
None of these two methods are working. This time the icon is pixelated when made smaller.
What is still wrong with VectorDrawable?

Using Scale X/Y to enlarge your views make them lose quality, instead increase the width and height, since you're using a vector drawable it would scale properly with it, to do this in code, use the layout param of the view... but note that the drawable of a button does not scale with the button so you should try something else like an image button or a linear layout with the drawable beside the button.

Related

How to rotate ImageVector’ pathData in Compose

I am trying to implement similar animation like this, with Jetpack Compose
figure: https://dribbble.com/shots/4762799-Microinteraction-Exploration-002
For the play icon’s transformation (from a triangle to a rectangle) , it not difficult to get it done by using path morphing animation.After reading some gist, I found that I can build an ImageVector by supplying the pathData , that's new to me~
While for ****the part on the right, the rotation of the cube object. Well, maybe I should call that a rolling-over or whatever, because the cube changed its pivot after -90 degrees.
I'm wondering that wether I can create a VectorGroup for the cube’s pathData and animate it by updating its group parameters.
re: androidx.compose.ui.graphics.vector.ImageVector.Builder#addGroup
ImageVector.Builder(defaultWidth = 100.dp, defaultHeight = 100.dp, viewportWidth = 100, viewportHeight = 100)
.addGroup(name = "CubeRotation", rotate = params.rotate, pivotX = params.pivotX, pivotY = params.pivotY,)
.addPath(pathData = pathData, fill = SolidColor(Color.LightGray))
.clearGroup()
.build()
Here I trying to update the pivotX halfway through the animation, so I divide it into two section:
// rotate -90 degrees twice with different pivot
val rotate = if (fraction <= 0.5f) {
lerp(0f, -180f, fraction)
} else {
lerp(0f, -180f, fraction - 0.5f)
}
// change the pivotX
val pivotX = if (fraction <= 0.5f) 52f else 30f
GroupParams(
pivotX = pivotX,
pivotY = 100f,
rotate = rotate,
)
Frustratedly, after rotating by the first -90 degrees, the transformation that this rotation performed will NOT persist, -or- its transformation NOT being applied after the half time of the transition.
fraction: 0~0.5
fraction: 0.5~1
The code sample I've uploaded: IndicatorDrawable.kt
Can I implement the rotate transformation by just updating the pathData ?
For me, the VectorGroup in ImageVector looks like creating AnimatedVectorDrawable in xml, or maybe I should try some traditional way like ObjectAnimationSet in View system ?

Reduce tracking window using google mlkit vision samples

I would like to reduce the reduce bar code tracking window when using the google vision api. There are some answers here but they feel a bit outdated.
I'm using google's sample: https://github.com/googlesamples/mlkit/tree/master/android/vision-quickstart
Currently, I try to figure out if a barcode is inside my overlay box inside BarcodeScannerProcessor onSuccess callback:
override fun onSuccess(barcodes: List<Barcode>, graphicOverlay: GraphicOverlay) {
if(barcodes.isEmpty())
return;
for(barcode in barcodes) {
val center = Point(graphicOverlay.imageWidth / 2, graphicOverlay.imageHeight / 2)
val rectWidth = graphicOverlay.imageWidth * Settings.OverlayWidthFactor
val rectHeight = graphicOverlay.imageHeight * Settings.OverlayHeightFactor
val left = center.x - rectWidth / 2
val top = center.y - rectHeight / 2
val right = center.x + rectWidth / 2
val bottom = center.y + rectHeight / 2
val rect = Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt())
val contains = rect.contains(barcode.boundingBox!!)
val color = if(contains) Color.GREEN else Color.RED
graphicOverlay.add(BarcodeGraphic(graphicOverlay, barcode, "left: ${barcode.boundingBox!!.left}", color))
}
}
Y-wise it works perfectly, but the X values from barcode.boundingBox e.g. barcode.boundingBox.left seems to have an offset. Is it based on what's being calculated in GraphicOverlay?
I'm expecting the value below to be close to 0, but the offset is about 90 here:
Or perhaps it's more efficient to crop the image according to the box?
Actually the bounding box is correct. The trick is that the image aspect ratio doesn't match the viewport aspect ratio so the image is cropped horizontally. Try to open settings (a gear in the top right corner) and choose an appropriate resolution.
For example take a look at these two screenshots. On the first one the selected resolution (1080x1920) matches my phone resolution so the padding looks good (17px). On the second screenshot the aspect ratio is different (1.0 for 720x720 resolution) therefore the image is cropped and the padding looks incorrect.
So the offset should be transformed from image coordinates to the screen coordinates. Under the hood GraphicOverlay uses a matrix for this transformation. You can use the same matrix:
for(barcode in barcodes) {
barcode.boundingBox?.let { bbox ->
val offset = floatArrayOf(bbox.left.toFloat(), bbox.top.toFloat())
graphicOverlay.transformationMatrix.mapPoints(offset)
val leftOffset = offset[0]
val topOffset = offset[1]
...
}
}
The only thing is that the transformationMatrix is private, so you should add a getter to access it.
As you know, the preview size of the camera is configurable at the settings menu. This configurable size specifies the graphicOverlay dimensions.
On the other hand, the aspect ratio of the CameraSourcePreview (i.e. preview_view in activity_vision_live_preview.xml) which is shown on the screen, does not necessarily equal to the ratio of the graphicOverlay. Because depends on the size of the phone's screen and the height that the parent ConstraintLayout allows occupying.
So, in the preview, based on the difference between the aspect ratio of graphicOverlay and preview_view, some part of the graphicOverlay might not be shown horizontally or vertically.
There are some parameters inside GraphicOverlay that can help us to adjust the left and top of the barcode's boundingBox in such a way that they start from 0 in the visible area.
First of all, they should be accessible out of the GraphicOverlay class. So, it's just enough to write a getter method for them:
GraphicOverlay.java
public class GraphicOverlay extends View {
...
/**
* The factor of overlay View size to image size. Anything in the image coordinates need to be
* scaled by this amount to fit with the area of overlay View.
*/
public float getScaleFactor() {
return scaleFactor;
}
/**
* The number of vertical pixels needed to be cropped on each side to fit the image with the
* area of overlay View after scaling.
*/
public float getPostScaleHeightOffset() {
return postScaleHeightOffset;
}
/**
* The number of horizontal pixels needed to be cropped on each side to fit the image with the
* area of overlay View after scaling.
*/
public float getPostScaleWidthOffset() {
return postScaleWidthOffset;
}
}
Now, it is possible to calculate the left and top difference gap using these parameters like the following:
BarcodeScannerProcessor.kt
class BarcodeScannerProcessor(
context: Context
) : VisionProcessorBase<List<Barcode>>(context) {
...
override fun onSuccess(barcodes: List<Barcode>, graphicOverlay: GraphicOverlay) {
if (barcodes.isEmpty()) {
Log.v(MANUAL_TESTING_LOG, "No barcode has been detected")
}
val leftDiff = graphicOverlay.run { postScaleWidthOffset / scaleFactor }.toInt()
val topDiff = graphicOverlay.run { postScaleHeightOffset / scaleFactor }.toInt()
for (i in barcodes.indices) {
val barcode = barcodes[i]
val color = Color.RED
val text = "left: ${barcode.boundingBox!!.left - leftDiff} top: ${barcode.boundingBox!!.top - topDiff}"
graphicOverlay.add(MyBarcodeGraphic(graphicOverlay, barcode, text, color))
logExtrasForTesting(barcode)
}
}
...
}
Visual Result:
Here is the visual result of the output. As it's obvious in the pictures, the gap between both left & top of the barcode and the left and top of the visible area is started from 0. In the case of the left picture, the graphicOverlay is set to the size of 480x640 (aspect ratio ≈ 1.3334) and for the right one 360x640 (aspect ratio ≈ 1.7778). In both cases, on my phone, the CameraSourcePreview has a steady size of 1440x2056 pixels (aspect ratio ≈ 1.4278), so it means that the calculation truly reflected the position of the barcode in the visible area.
(note that the aspect ratio of the visible area in one experiment is lower than that of graphicOverlay, and in another experiment, greater: 1.3334 < 1.4278 < 1.7778. So, the left values and top values are adjusted respectively.)

TextView as a Progress Bar in Kotlin

I am trying to replicate the TextView as progress bar described in this question using Kotlin, overriding the TextView class and using the canvas clipping trick in Zielony's comment.
The code almost works: I can see the text change color at the specified percentage (fixed, for now) but the background stays the same (yellow, the last background color I set).
Here's my code:
class ProgressTextView(context: Context): TextView(context) {
override fun onDraw(canvas: Canvas) {
val color1 = ContextCompat.getColor(context, R.color.primaryColor)
val color2 = ContextCompat.getColor(context, R.color.secondaryColor)
var percent = 0.3 // Fixed progress % at 30%, for testing
// first half
canvas.save()
setTextColor(Color.RED)
setBackgroundColor(Color.GREEN)
canvas.clipRect(Rect(0, 0, (width * percent).toInt(), height))
super.onDraw(canvas)
canvas.restore()
// second half
canvas.save()
setTextColor(Color.BLACK)
setBackgroundColor(Color.YELLOW)
canvas.clipRect(Rect((width * percent).toInt(), 0, width, height))
super.onDraw(canvas)
canvas.restore()
}
}
What am I missing?
I managed to get it to work painting a rectangle instead of clipping for the first half, but I am still curious about how it should be done "properly", as this solution seems more elegant.

CollapsingToolbarLayout - Dim/Blur the background image

I've got a collapsing toolbar with an imageview. When extended, it looks like this:
When collapsed, it looks like this:
I know that according to the guidelines, the toolbar should be of the primary color while collapsed, but i like the way i've set it up, and would like to keep it like that.
However, obviously, the toolbar title won't be visible if the image has a lot of white in it. So, is there a way i can blur/dim the background so that the toolbar title is visible at all times?
Edit: The image in question, is loaded from the internet using Picasso.
If you are using Picasso to display the image, then you can leverage the Transformation object in Picasso's builder and create a Blur effect class with something like this:
public class BlurEffect implements Transformation {
private static final int UP_LIMIT = 25;
private static final int LOW_LIMIT = 1;
protected final Context context;
private final int blurRadius;
public BlurEffect(Context context, int radius) {
this.context = context;
if (radius < LOW_LIMIT) {
this.blurRadius = LOW_LIMIT;
} else if (radius > UP_LIMIT) {
this.blurRadius = UP_LIMIT;
} else {
this.blurRadius = radius;
}
}
#Override public Bitmap transform(Bitmap source) {
Bitmap blurredBitmap;
blurredBitmap = Bitmap.createBitmap(source);
RenderScript renderScript = RenderScript.create(context);
Allocation input =
Allocation.createFromBitmap(renderScript, source,
Allocation.MipmapControl.MIPMAP_FULL,
Allocation.USAGE_SCRIPT);
Allocation output = Allocation.createTyped(renderScript, input.getType());
ScriptIntrinsicBlur script =
ScriptIntrinsicBlur.create(renderScript,
Element.U8_4(renderScript));
script.setInput(input);
script.setRadius(blurRadius);
script.forEach(output);
output.copyTo(blurredBitmap);
return blurredBitmap;
}
#Override public String key() {
return "blurred";
}
}
Then you can use it from Picasso in this way, the more value in second parameter in the constructor, then blurer:
Picasso.with(appBarImage.getContext())
.load(track.getUrl())
.transform(new BlurEffect(this, 10))
.into(appBarImage);
My not optimal solution to this problem was to overlap two images, the normal and the blurred one, and change the alpha of the blurred one. When expanded, the alpha is 0. When collapsed its alpha is one. I used
AppBarLayout.addOnOffsetChangedListener
to get the scroll event. In my case
appBarLayout.addOnOffsetChangedListener((view, verticalOffset) -> {
if (mScrollRange == 0) {
mScrollRange = appBarLayout.getTotalScrollRange();
}
ViewCompat.setAlpha(mBlurredImage,
(float) (1 - Math.pow(((mScrollRange + verticalOffset) / (float) mScrollRange), 4)));
});
It is not ideal but it does its work
Ok, So, i just ended up using android:tint in the layout file in order to add a grayish tint to the image.For reference, the tint value i set is #55000000. This seems to make the title visible even on entirely white backgrounds.
You could use Blurry library
Blurry is an easy blur library for Android
// from Bitmap
Blurry.with(context).from(bitmap).into(imageView);
//use with Picasso
Blurry.with(MainActivity.this)
.radius(10)
.sampling(8)
.async()
.capture(findViewById(R.id.image_veiw))
.into((ImageView) findViewById(R.id.image_veiw));
You can use scrim for this. create gradient file in drawable folder.
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="-90"
android:startColor="#00000000"
android:centerColor="#00000000"
android:endColor="#4d000000"
android:type="linear" />
</shape>
After creating the above file add an extra view to your layout like this
<FrameLayout>
<ImageView>
<View
android:background="#drawable/nameOfAboveFile"/>
<TexTView>
</FrameLayout>

View with round corners not smooth

Have a look at my code below.
ShapeDrawable shapeDrawable = new ShapeDrawable(new RectShape());
shapeDrawable.getPaint().setColor(Color.parseColor("#5a2705"));
shapeDrawable.getPaint().setStyle(Style.STROKE);
shapeDrawable.getPaint().setAntiAlias(true);
shapeDrawable.getPaint().setStrokeWidth(2);
shapeDrawable.getPaint().setPathEffect(new CornerPathEffect(10));
I am applying this as background to my LinearLayout, but the edges are not smooth. How can I fix this?
Here is the screenshot of how it looks.
Using a programmatically-created shape drawable as a View background results in the outer half of your stroke width getting cropped off (for reasons I don't know). Look closely at your image and you'll see that your stroke is only 1 pixel wide, even though you requested 2. That is why the corners look ugly. This effect will be much more apparent if you try a bigger stroke and radius such as 10 and 40, respectively.
Either use an XML drawable, which doesn't seem to have this problem, like in Harshit Jain's answer, or do the following if you must (or prefer to) use a programmatic solution.
Solution: Use a layer list to inset the rectangle by the amount that is being clipped (half the stroke width), like this:
float strokeWidth = 2;
ShapeDrawable shapeDrawable = new ShapeDrawable(new RectShape());
shapeDrawable.getPaint().setColor(Color.parseColor("#5a2705"));
shapeDrawable.getPaint().setStyle(Style.STROKE);
shapeDrawable.getPaint().setAntiAlias(true);
shapeDrawable.getPaint().setStrokeWidth(strokeWidth);
shapeDrawable.getPaint().setPathEffect(new CornerPathEffect(10));
Drawable[] layers = {shapeDrawable};
LayerDrawable layerDrawable = new LayerDrawable(layers);
int halfStrokeWidth = (int)(strokeWidth/2);
layerDrawable.setLayerInset(0, halfStrokeWidth, halfStrokeWidth, halfStrokeWidth, halfStrokeWidth);
Then use the layerDrawable as your background. Here is a screen shot of the result of the above code:
You can try creating a separate xml file with a layout of the rounded rectangle. Such as:
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="color_here"/>
<stroke android:width="5dp" android:color="color_here"/>
<corners android:radius="2dp"/>
</shape>
You can tune this to your liking and use this XML file as a background in your main XML.
You can also try using 9Patch which should already come with your SDK
To cater such background requirement I prefer using 9-patch drawable.
Below are the pointer to get going with creating and using 9-patch drawable resources:
Developer Doc: http://developer.android.com/tools/help/draw9patch.html
Another explanation: http://radleymarx.com/blog/simple-guide-to-9-patch/
Online tool to create 9-patch images. (Note: Modify the file extension to *.9.png while saving the image)
You might want to consider checking this out.
Rounded Image Views by Vince
I have solved this issue in a very simple way,
I was looking for a code which can be done programmatically and not via xml,
Here is the code:
GradientDrawable shape = new GradientDrawable();
shape.setCornerRadius(10);
shape.setColor(Color.WHITE);
shape.setStroke(2, Color.parseColor("#996633"));
You can set this like this : view.setBackgroundDrawable(shape);
The answer posted by #Tenfour04 might also solve the issue but i havent tried as such.
Hope it helps someone.
You may try using selector like this . This solution is the best you just need to set this selector to the background of the layout.
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#color/white" />
<stroke android:width="1dp" android:color="#color/grey" />
<padding android:left="10dp" android:top="10dp" android:right="10dp" android:bottom="10dp" />
<corners android:radius="15dp" />
<gradient
android:angle="90"
android:startColor="#color/white"
android:centerColor="#color/grey"
android:endColor="#color/white"
android:type="linear" />
</shape>
Choose color of your own.
Cheers!
I've encountered this problem recently and came up with a different solution. IMHO the best solution is to create your own Shape implementation and use it to create a ShapeDrawable.
Below is a simple implementation of rounded rectangle, that will allow you to inset it's border.
class InsetRoundRectShape(
private var radiusArray: FloatArray = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f),
private var inset: RectF = RectF()
): RectShape() {
private var innerRect: RectF = RectF()
private var path: Path = Path()
constructor(radius: Float, inset: RectF): this(floatArrayOf(radius, radius, radius, radius, radius, radius, radius, radius), inset)
constructor(radius: Float, inset: Float): this(radius, RectF(inset, inset, inset, inset))
init {
if (radiusArray.size < 8) {
throw ArrayIndexOutOfBoundsException("radius array must have >= 8 values")
}
}
override fun draw(canvas: Canvas, paint: Paint) {
canvas.drawPath(path, paint)
}
override fun getOutline(outline: Outline) {
super.getOutline(outline)
val radius = radiusArray[0]
if(radiusArray.any { it != radius }) {
outline.setConvexPath(path)
return
}
val r = rect()
outline.setRoundRect(ceil(r.left).toInt(), ceil(r.top).toInt(), floor(r.right).toInt(), floor(r.bottom).toInt(), radius)
}
override fun onResize(w: Float, h: Float) {
super.onResize(w, h)
val r = rect()
path.reset()
innerRect.set(r.left + inset.left, r.top + inset.top, r.right - inset.right, r.bottom - inset.bottom)
if(innerRect.width() <= w && innerRect.height() <= h) {
path.addRoundRect(innerRect, radiusArray, Path.Direction.CCW)
}
}
override fun clone(): InsetRoundRectShape {
val shape = super.clone() as InsetRoundRectShape
shape.radiusArray = radiusArray.clone()
shape.inset = RectF(inset)
shape.path = Path(path)
return shape
}
}
Create ShapeDrawable like this
//Inset has to be half of strokeWidth
ShapeDrawable(InsetRoundRectShape(10f, 4f)).apply {
this.paint.color = Color.BLUE
this.paint.style = Paint.Style.STROKE
this.paint.strokeWidth = 8.dp
this.invalidateSelf()
}

Categories

Resources