Firemonkey使用TPathData类来存储一系列相连的曲线和直线。查看TPathData的曲线方法源码可知,SmoothCurveTo和QuadCurveTo方法最终还是转化为调用CurveTo方法。而CurveTo是绘制三阶贝塞尔曲线,也就是说TPataData不支持直接绘制二阶贝塞尔曲线。
Firemonkey自带有一个PaintBox控件,但是这个控件仅仅是公布了一个OnPaint事件,真正绘制内容代码还需自己实现。TMS里有个继承自TShape的SignatureCapture控件,查看源码发现其是在Mouse Down/Move/Up 三个事件中记录点并使用的是DrawLine方法进行连线,所以其绘制签名笔迹不够圆滑。OrangeUI里也有一个DrawPanel控件可以手绘笔迹,虽没有源码,但其测试效果也不圆滑,而且在iOS上测试还有一个能绘制到控件区域外的Bug。
这里给出一个使用原生手势识别和BezierPath绘制签名的控件实现源码:
unit FMX.TU2Signature;
interface
uses
System.Classes, System.Types, System.UITypes,
FMX.Types, FMX.Graphics, FMX.Controls, FMX.Objects, FMX.TU2;
type
IBezierPath = interface
procedure MoveTo(const P: TPointF);
procedure LineTo(const P: TPointF); //一阶
procedure QuadTo(const ControlPoint, EndPoint: TPointF); //二阶
procedure CurveTo(const Control1, Control2, EndPoint: TPointF); //三阶
procedure DrawToBitmap(const ACanvas: TCanvas);
procedure Clear;
function IsEmpty: Boolean;
{Update}
procedure Resize(const AWidth, AHeight: Single);
procedure SetPenColor(const Value: TAlphaColor);
procedure SetPenThickness(const Value: Single);
end;
[ComponentPlatformsAttribute(TU2FMXPlatforms)]
TSignature = class(TRectangle)
private
FPrevPoint: TPointF;
FPath: IBezierPath;
FIndex: Integer;
FPenColor: TAlphaColor;
FPenThickness: Single;
private
procedure SetPenColor(const Value: TAlphaColor);
procedure SetPenThickness(const Value: Single);
function GetEmpty: Boolean;
protected
procedure CMGesture(var EventInfo: TGestureEventInfo); override;
procedure Paint; override;
procedure DoResized; override;
public
constructor Create(AOwner: TComponent); override;
procedure Clear;
property Empty: Boolean read GetEmpty;
published
property PenColor: TAlphaColor read FPenColor write SetPenColor;
property PenThickness: Single read FPenThickness write SetPenThickness;
end;
implementation
uses
{$IFDEF IOS}
FMX.TU2Signature.iOS,
{$ENDIF IOS}
{$IFDEF ANDROID}
FMX.TU2Signature.Android,
{$ENDIF}
System.SysUtils;
{ TSignatureControl }
constructor TSignature.Create(AOwner: TComponent);
begin
inherited;
{$IFDEF IOS}
FPath := TiOSBezierPath.Create;
{$ENDIF}
{$IFDEF ANDROID}
FPath := TAndroidBezierPath.Create;
{$ENDIF}
FPenColor := TAlphaColorRec.Black;
FPenThickness := 2;
Touch.InteractiveGestures := [TInteractiveGesture.Pan];
end;
procedure TSignature.DoResized;
begin
inherited;
{$IF Defined(IOS) OR Defined(ANDROID)}
FPath.Resize(Width, Height);
{$ENDIF}
end;
function TSignature.GetEmpty: Boolean;
begin
{$IF Defined(IOS) OR Defined(ANDROID)}
Result := FPath.IsEmpty;
{$ELSE}
Result := True;
{$ENDIF}
end;
procedure TSignature.Paint;
begin
inherited;
{$IF Defined(IOS) OR Defined(ANDROID)}
if not FPath.IsEmpty then
FPath.DrawToBitmap(Canvas);
{$ENDIF}
end;
procedure TSignature.SetPenColor(const Value: TAlphaColor);
begin
{$IF Defined(IOS) OR Defined(ANDROID)}
FPath.SetPenColor(Value);
{$ENDIF}
FPenColor := Value;
end;
procedure TSignature.SetPenThickness(const Value: Single);
begin
{$IF Defined(IOS) OR Defined(ANDROID)}
FPath.SetPenThickness(Value);
{$ENDIF}
FPenThickness := Value;
end;
procedure TSignature.Clear;
begin
FPath.Clear;
Repaint;
end;
procedure TSignature.CMGesture(var EventInfo: TGestureEventInfo);
var
LP: TPointF;
begin
if EventInfo.GestureID=igiPan then
begin
LP := AbsoluteToLocal(EventInfo.Location);
if PointInObjectLocal(LP.X,LP.Y) then
begin
if TInteractiveGestureFlag.gfBegin in EventInfo.Flags then
begin
FPath.MoveTo(LP);
FIndex := 0;
FPrevPoint := LP;
end else if EventInfo.Flags=[] then
begin
if FIndex=0 then
Inc(FIndex)
else begin
FPath.QuadTo(FPrevPoint, LP.MidPoint(FPrevPoint));
Repaint;
end;
FPrevPoint := LP;
end else if TInteractiveGestureFlag.gfEnd in EventInfo.Flags then
begin
FPath.QuadTo(FPrevPoint, LP.MidPoint(FPrevPoint));
Repaint;
end;
end;
end else
inherited;
end;
end.
iOS平台实现代码:
unit FMX.TU2Signature.iOS;
interface
uses System.Types, System.UITypes, FMX.Graphics, FMX.TU2Signature, iOSapi.UIKit;
type
TiOSBezierPath = class(TInterfacedObject, IBezierPath)
private
FData: UIBezierPath;
FPenColor: TAlphaColor;
FSize: TSize;
protected
procedure MoveTo(const P: TPointF);
procedure LineTo(const P: TPointF); //一阶
procedure QuadTo(const ControlPoint, EndPoint: TPointF); //二阶
procedure CurveTo(const Control1, Control2, EndPoint: TPointF); //三阶
procedure DrawToBitmap(const ACanvas: TCanvas);
procedure Clear;
function IsEmpty: Boolean;
{Update}
procedure Resize(const AWidth, AHeight: Single);
procedure SetPenColor(const Value: TAlphaColor);
procedure SetPenThickness(const Value: Single);
public
constructor Create;
destructor Destroy; override;
end;
implementation
uses iOSapi.Foundation, iOSapi.CoreGraphics, iOSapi.CocoaTypes, FMX.Helpers.iOS;
{ TiOSBezierPath }
constructor TiOSBezierPath.Create;
begin
FData := TUIBezierPath.Wrap(TUIBezierPath.OCClass.bezierPath);
FData.setLineWidth(2);
FData.setLineCapStyle(kCGLineCapRound);
FData.setLineJoinStyle(kCGLineJoinRound);
FData.retain;
FPenColor := TAlphaColorRec.Black;
end;
destructor TiOSBezierPath.Destroy;
begin
FData.release;
inherited;
end;
procedure TiOSBezierPath.Clear;
begin
FData.removeAllPoints;
end;
function TiOSBezierPath.IsEmpty: Boolean;
begin
Result := FData.isEmpty;
end;
procedure TiOSBezierPath.MoveTo(const P: TPointF);
begin
FData.moveToPoint(NSPoint.Create(P));
end;
procedure TiOSBezierPath.LineTo(const P: TPointF);
begin
FData.addLineToPoint(NSPoint.Create(P));
end;
procedure TiOSBezierPath.QuadTo(const ControlPoint, EndPoint: TPointF);
begin
FData.addQuadCurveToPoint(NSPoint.Create(EndPoint),NSPoint.Create(ControlPoint));
end;
procedure TiOSBezierPath.CurveTo(const Control1, Control2, EndPoint: TPointF);
begin
FData.addCurveToPoint(NSPoint.Create(EndPoint),NSPoint.Create(Control1),NSPoint.Create(Control2));
end;
procedure TiOSBezierPath.DrawToBitmap(const ACanvas: TCanvas);
var
img: UIImage;
bm: TBitmap;
begin
UIGraphicsBeginImageContextWithOptions(CGSizeMake(FSize.Width, FSize.Height), False, 0);
//绘制曲线
AlphaColorToUIColor(FPenColor).setStroke;
FData.stroke;
//GPU绘制缓存
img := TUIImage.Wrap(UIGraphicsGetImageFromCurrentImageContext);
bm := UIImageToBitmap(img, 0, FSize);
UIGraphicsEndImageContext;
ACanvas.DrawBitmap(bm, bm.BoundsF, TRectF.Create(0,0,FSize.cx, FSize.cy), 1);
end;
procedure TiOSBezierPath.SetPenColor(const Value: TAlphaColor);
begin
FPenColor := Value;
end;
procedure TiOSBezierPath.SetPenThickness(const Value: Single);
begin
FData.setLineWidth(Value);
end;
procedure TiOSBezierPath.Resize(const AWidth, AHeight: Single);
begin
FSize.cx := Round(AWidth);
FSize.cy := Round(AHeight);
end;
end.
unit FMX.TU2Signature.Android;
interface
uses System.Types, System.UITypes, FMX.Graphics, FMX.TU2Signature,
Androidapi.JNI.GraphicsContentViewText;
type
TAndroidBezierPath = class(TInterfacedObject, IBezierPath)
private
FData: JPath;
FCanvas: JCanvas;
FBitmap: JBitmap;
FPaint: JPaint;
protected
procedure MoveTo(const P: TPointF);
procedure LineTo(const P: TPointF); //一阶
procedure QuadTo(const ControlPoint, EndPoint: TPointF); //二阶
procedure CurveTo(const Control1, Control2, EndPoint: TPointF); //三阶
procedure DrawToBitmap(const ACanvas: TCanvas);
procedure Clear;
function IsEmpty: Boolean;
{Update}
procedure Resize(const AWidth, AHeight: Single);
procedure SetPenColor(const Value: TAlphaColor);
procedure SetPenThickness(const Value: Single);
public
constructor Create;
destructor Destroy; override;
end;
implementation
uses System.SysUtils, FMX.Surfaces, FMX.Helpers.Android;
{ TAndroidBezierPath }
constructor TAndroidBezierPath.Create;
begin
FData := TJPath.JavaClass.Init;
FPaint := TJPaint.Wrap(TJPaint.JavaClass.init(TJPaint.JavaClass.ANTI_ALIAS_FLAG));
FPaint.setStyle(TJPaint_Style.Wrap(TJPaint_Style.JavaClass.STROKE));
FPaint.setStrokeCap(TJPaint_Cap.JavaClass.ROUND);
FPaint.setStrokeJoin(TJPaint_Join.JavaClass.ROUND);
FPaint.setStrokeWidth(2);
FPaint.setColor(TAlphaColorRec.Black);
end;
destructor TAndroidBezierPath.Destroy;
begin
FData := nil;
FPaint := nil;
FCanvas := nil;
FBitmap := nil;
inherited;
end;
procedure TAndroidBezierPath.Clear;
begin
FData.reset;
end;
function TAndroidBezierPath.IsEmpty: Boolean;
begin
Result := FData.isEmpty;
end;
procedure TAndroidBezierPath.MoveTo(const P: TPointF);
begin
FData.moveTo(P.X, P.Y);
end;
procedure TAndroidBezierPath.LineTo(const P: TPointF);
begin
FData.lineTo(P.X, P.Y);
end;
procedure TAndroidBezierPath.QuadTo(const ControlPoint, EndPoint: TPointF);
begin
FData.quadTo(ControlPoint.X, ControlPoint.Y, EndPoint.X, EndPoint.Y);
end;
procedure TAndroidBezierPath.CurveTo(const Control1, Control2, EndPoint: TPointF);
begin
FData.cubicTo(Control1.X, Control1.Y, Control2.X, Control2.Y, EndPoint.X, EndPoint.Y);
end;
procedure TAndroidBezierPath.DrawToBitmap(const ACanvas: TCanvas);
var
Surface: TBitmapSurface;
bm: TBitmap;
begin
FCanvas.drawColor(0,TJPorterDuff_Mode.Wrap(TJPorterDuff_Mode.JavaClass.CLEAR));
FCanvas.drawPath(FData, FPaint);
//获取结果
Surface := TBitmapSurface.Create;
try
if JBitmapToSurface(FBitmap, Surface) then
begin
bm := TBitmap.Create;
bm.Assign(Surface);
ACanvas.DrawBitmap(bm, bm.BoundsF, TRectF.Create(0,0,FBitmap.getWidth, FBitmap.getHeight), 1);
end;
finally
Surface.Free;
end;
end;
procedure TAndroidBezierPath.SetPenColor(const Value: TAlphaColor);
begin
FPaint.setColor(Value);
end;
procedure TAndroidBezierPath.SetPenThickness(const Value: Single);
begin
FPaint.setStrokeWidth(Value);
end;
procedure TAndroidBezierPath.Resize(const AWidth, AHeight: Single);
begin
if FBitmap<>nil then
begin
FCanvas := nil;
FBitmap.recycle;
FBitmap := nil;
end;
FBitmap := TJBitmap.JavaClass.createBitmap(Round(AWidth), Round(AHeight),
TJBitmap_Config.JavaClass.ARGB_8888);
FCanvas := TJCanvas.JavaClass.init(FBitmap);
end;
end.
测试效果:
最终版效果: