Using TCanvas in Delphi for Android
Drawing on TCanvas in Delphi XE5 for Android turned out to have some special aspects which at first left me in doubt and I want to share my experience.
Let’s draw some parallel lines.
Here I’d like to digress and notice that on Windows Stroke.Kind value is bkSolid by default, but on Android it’s bkNone. Thus, if you haven’t defined Stroke.Kind value, these lines will be visible on Windows, but will not on Android. I have no idea why they chose this approach.
procedure TForm2.FormPaint(Sender: TObject; Canvas: TCanvas;
const ARect: TRectF);
var
I: Integer;
begin
if Canvas.BeginScene then
try
Canvas.Stroke.Thickness := 1.5;
Canvas.Stroke.Kind := TBrushKind.bkSolid;
Canvas.Fill.Color := TAlphaColorRec.Black;
Canvas.Fill.Kind := TBrushKind.bkSolid;
for I := 1 to 9 do
Canvas.DrawLine(PointF(50 + I * 25, 0), PointF(50 + I * 25, ClientHeight), 1);
finally
Canvas.EndScene;
end;
end;
That’s it:

Obviously, some lines are thicker than others. On Windows the same code works just perfectly.
The reason is that unlike it does on Windows, the logical pixel on Android is not always equal to physical pixel. And if a line appears to be “between” physical pixels, it has to be blurred on neighboring pixels. It is a trade-off between accuracy and quality of rendering.
If we still want to draw equal lines, we could move them by the half of their thikness to ensure getting the appropriate physical pixels.
That’s how TLine and its ancestor TShape solve the problem:
function TShape.GetShapeRect: TRectF;
begin
Result := LocalRect;
if FStroke.Kind <> TBrushKind.bkNone then
InflateRect(Result, -(FStroke.Thickness / 2), -(FStroke.Thickness / 2));
end;
procedure TLine.Paint;
begin
case FLineType of
TLineType.ltTop:
Canvas.DrawLine(GetShapeRect.TopLeft, PointF(GetShapeRect.Right, GetShapeRect.Top),
AbsoluteOpacity, FStroke);
TLineType.ltLeft:
Canvas.DrawLine(GetShapeRect.TopLeft, PointF(GetShapeRect.Left, GetShapeRect.Bottom),
AbsoluteOpacity, FStroke);
else
Canvas.DrawLine(GetShapeRect.TopLeft, GetShapeRect.BottomRight, AbsoluteOpacity, FStroke);
end;
end;
Making the appropriate changes we can draw equal lines too:
procedure TForm2.FormPaint(Sender: TObject; Canvas: TCanvas;
const ARect: TRectF);
var
I: Integer;
begin
if Canvas.BeginScene then
try
Canvas.Stroke.Thickness := 1.5;
Canvas.Stroke.Kind := TBrushKind.bkSolid;
Canvas.Fill.Color := TAlphaColorRec.Black;
Canvas.Fill.Kind := TBrushKind.bkSolid;
for I := 1 to 9 do
begin
Canvas.DrawLine(PointF(50 + I * 25 - (Canvas.Stroke.Thickness / 2), 0),
PointF(50 + I * 25 - (Canvas.Stroke.Thickness / 2), ClientHeight), 1);
end;
finally
Canvas.EndScene;
end;
end;
The result is:

Much better :)
This can’t be done automatically: in this case it will “jump” during an animation. But anyways I’d like to have some flag to choose between accuracy and quality. It’s quite boring to do this calculation manually.
update
Alysson Cunha suggests one more approach:
function TForm2.RoundLogicPointsToMatchPixel(const LogicPoints: Single;
const AtLeastOnePixel: Boolean = False): Single;
var
ws: IFMXWindowService;
ScreenScale, Pixels: Single;
begin
ws := TPlatformServices.Current.GetPlatformService(IFMXWindowService) as IFMXWindowService;
ScreenScale := ws.GetWindowScale(Self);
// Maybe you will want to use Ceil or Trunc instead of Round
Pixels := Round(LogicPoints * ScreenScale);
if (Pixels < 1) and (AtLeastOnePixel) then
Pixels := 1.0;
Result := Pixels / ScreenScale;
end;
procedure TForm2.FormPaint(Sender: TObject; Canvas: TCanvas;
const ARect: TRectF);
var
I: Integer;
begin
if Canvas.BeginScene then
try
Canvas.Stroke.Thickness := RoundLogicPointsToMatchPixel(1.0, True);
Canvas.Stroke.Kind := TBrushKind.bkSolid;
Canvas.Fill.Color := TAlphaColorRec.Black;
Canvas.Fill.Kind := TBrushKind.bkSolid;
for I := 1 to 9 do
Canvas.DrawLine(PointF(RoundLogicPointsToMatchPixel(50 + I * 25), 0),
PointF(RoundLogicPointsToMatchPixel(50 + I * 25), ClientHeight), 1);
finally
Canvas.EndScene;
end;
end;
