Firemonkey扩展增强:iOS/Android使用贝塞尔曲线绘制签名(笔迹)

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.

Android平台实现:

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.

完整源码

测试效果:

Firemonkey扩展增强:iOS/Android使用贝塞尔曲线绘制签名(笔迹)_第1张图片

Firemonkey扩展增强:iOS/Android使用贝塞尔曲线绘制签名(笔迹)_第2张图片Firemonkey扩展增强:iOS/Android使用贝塞尔曲线绘制签名(笔迹)_第3张图片Firemonkey扩展增强:iOS/Android使用贝塞尔曲线绘制签名(笔迹)_第4张图片

最终版效果:

Firemonkey扩展增强:iOS/Android使用贝塞尔曲线绘制签名(笔迹)_第5张图片

你可能感兴趣的:(Delphi,iOS,Firemonkey扩展增强,Android)