Search code examples
paintcode

PaintCode drawing code android is using pixels instead of points/DP


Note: Yes I know there are other ways of doing buttons in Android, but this is just an example to demonstrate my issue (the actuall buttons are far far more complex). So please don't reply in offering other solutions for buttons in Android, I am looking for a solution with PaintCode...

I have been using PaintCode for drawing custom buttons for years in iOS, it works brilliantly. I want to do the same for android and have the following issue:

  1. In PaintCode I draw a button which is basically a rounded rectangle with a radius of 20 points.
  2. I draw a frame around and then setting the correct resizing behaviour using the springs (see screenshot).
  3. The result is that whatever the size of the button is going to be (= the frame) the corners will always be nicely rounded with 20 points. Basically a nicely resizable button.

This works very well on iOS but on android, the radius is 20 pixels not points, resulting in a far to small radius (now with the high res devices).

Or in general all drawings that I make in PaintCode when drawn using the draw method generated by PaintCode are to small.

It seams that the generated drawing code does not take into account the scale of the device (as it does on iOS).

enter image description here

Looking at https://www.paintcodeapp.com/documentation/android section "scale" PaintCode suggest to play with the density metric in android to perform scaling. This does work, but makes the generated drawing fuzzy, I guess this is because we are drawing in lower resolution due to the scaling. So its not a viable solution.

class Button1 @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : Button(context, attrs, defStyleAttr) {

    var frame = RectF()

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        val displayDensity = resources.displayMetrics.density
        canvas?.scale(displayDensity, displayDensity)
        frame.set(0f,0f,width.toFloat()/displayDensity,height.toFloat()/displayDensity)
        StyleKitName.drawButton1(canvas, frame)
    }
}

Any suggestions to solve this? Is this a bug in PaintCode?


Solution

  • I'm the developer. Sorry about the long answer.

    TLDR: handle the scaling yourself, for example the way you do. Switch layerType of your View to software to avoid blurry results of scale.

    First I totally understand the confusion, for iOS it just works and in Android you have to fiddle around with some scales. It would make much more sense if it just worked the same I would love that and also would other PaintCode users. Yet it’s not a bug. The problem is difference between UIKit and android.graphic.

    In UIKit the distances are measured in points. That means if you draw a circle with diameter 40 points, it should be more-less the same size on various iOS devices. PaintCode adopted this convention and all the numbers you see in PaintCode's user interface like position of shapes, stroke width or radius - everything is in points. The drawing code generated by PaintCode is not only resolution independent (i.e. you can resize/scale it and it keeps the sharpness), but also display-density independent (renders about the same size on retina display, regular display and retina HD display). And there isn’t anything special about the code. It looks like this:

    NSBezierPath* rectanglePath = [NSBezierPath bezierPathWithRect: NSMakeRect(0, 0, 100, 50)];
    [NSColor.grayColor setFill];
    [rectanglePath fill];
    

    So the display scaling is handled by UIKit. Also the implicit scale depends on the context. If you call the drawing code within drawRect: of some UIView subclass, it takes the display-density, but if you are drawing inside a custom UIImage, it takes the density of that image. Magic.

    Then we added support for Android. All the measures in android.graphic are represented in pixels. Android doesn’t do any of UIKit's “display density” magic. Also there isn’t a good way to find out what the density is in the scope of drawing code. You need access to resources for that. So we could add that as a parameter to all the drawing methods. But what if you are not going to publish the drawing to the display but you are rather creating an image (that you are going to send to your friend or whatever)? Then you don’t want display density, but image density. OK so if adding a parameter, we shouldn’t add resources, but the density itself as a float and generate the scaling inside every drawing method. Now what if you don’t really care about the density? What if all you care about is that your drawing fills some rectangle and have the best resolution possible? Actually I think that that is usually the case. Having so many different display resolutions and display densities makes the “element of one physical size fits all” approach pretty minor in my opinion. So in most cases the density parameter would be extraneous. We decided to leave the decision of how the scale should be handled to user.

    Now for the fuzziness of the scaled drawing. That’s another difference between UIKit and android.graphics. All developers should understand that CoreGraphics isn’t very fast when it comes to rendering large scenes with multiple objects. If you are programming performance sensitive apps, you should probably consider using SpriteKit or Metal. The benefit of this is that you are not restricted in what you can do in CoreGraphics and you will almost always get very accurate results. Scaling is one such example. You can apply enormous scale and the result is still crisp. If you want more HW acceleration, use a different API and handle the restrictions yourself (like how large textures you can fit in your GPU).

    Android took other path. Their android.graphic api can work in two modes - without HW acceleration (they call it software) or with HW acceleration (they call it hardware). It’s still the same API, but one of the hardware modes has some significant restrictions. This includes scale, blur (hence shadows), some blend modes and more. https://developer.android.com/guide/topics/graphics/hardware-accel.html#unsupported

    And they decided that every view will use the hardware mode by default if target API level >= 14. You can of course turn it off and magically your scaled button will be nice and sharp.

    We mention that you need to turn off hardware acceleration in our documentation page section “Type of Layer” https://www.paintcodeapp.com/documentation/android And it’s also in Android documentation https://developer.android.com/guide/topics/graphics/hardware-accel.html#controlling