syncfusion-flutter-signature-pad について
Flutter画面で署名や描画を取得する開発者向けに、Syncfusion FlutterのSfSignaturePadを解説するスキルです。入力、検証、カスタマイズ、イベント処理、画像または生データへの出力を扱います。
主な機能
- 01タッチとスタイラスによる描画
- 02線と背景のカスタマイズ
- 03最小ストローク数の検証
- 04PNG、JPEG、生データへの出力
- 05描画コールバックと読み取り専用表示
活用シーン
- 01契約書やフォームでの署名取得
- 02承認ワークフローでの署名収集
- 03手書きメモやスケッチの追加
- 04保存済み署名の読み取り専用表示
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ syncfusion/ flutter-ui-components-skills --skill syncfusion-flutter-signature-padインストール後、Claude Code で/syncfusion-flutter-signature-padを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/accessibility.mdadvanced-features.mdcallbacks.mdcustomization.mdgetting-started.mdoverview.mdsaving-exporting.md詳細
Syncfusion Flutter Signature Pad
このスキルでは、Flutter アプリケーションで手書きの署名や描画をエレガントかつ直感的に取り込める、Syncfusion Flutter の SfSignaturePad コンポーネントを扱います。このコンポーネントは、タッチとスタイラスに対応した滑らかな描画体験に加え、高度なカスタマイズ機能とエクスポート機能を提供します。
このスキルを使用する場面
次のような場合に、このスキルを使用します。
- 書類、フォーム、契約書の電子署名を取り込む
- 承認ワークフローに電子署名機能を実装する
- PDF フォームやデジタル文書に署名欄を作成する
- 契約書、領収書、法的書類向けの文書署名インターフェイスを構築する
- メモ、スケッチ、注釈用の手書き入力を追加する
- 認証フローに署名検証画面を実装する
- 簡単なスケッチやマークアップ用の描画領域を作成する
- 署名ベースの認証でユーザーの同意を取り込む
- 手書き署名が必要な承認システムを構築する
- 登録、購入手続き、配達確認画面に署名パッドを追加する
コンポーネントの概要
SfSignaturePad は、タッチまたはスタイラス入力で滑らかで高品質な署名や描画を取り込む Flutter ウィジェットです。ストロークのカスタマイズ、背景画像、最小ストローク数の検証、複数のエクスポート形式などの機能を備えた、カスタマイズ可能な描画領域を提供します。
主な機能
- 滑らかな描画体験 - 滑らかな曲線を実現するため、タッチ入力とスタイラス入力に最適化されています
- ストロークのカスタマイズ - ストロークの色、幅、外観を調整できます
- 背景のサポート - 背景色や画像を追加できます
- 最小ストローク数の検証 - 意味のある署名が取り込まれるようにします
- 複数のエクスポート形式 - 画像(PNG、JPEG)に変換したり、生データとして保存したりできます
- クリアとリセット - プログラムから署名パッドをクリアできます
- 読み取り専用モード - 署名を表示し、変更を許可しません
- コールバック - 描画開始、描画中、描画終了のイベントを処理します
- 透明な背景 - 背景を透過した署名の取り込みに対応します
- 高品質な出力 - 希望する解像度で署名をエクスポートできます
ドキュメントとナビゲーションガイド
はじめに
📄 参照: references/getting-started.md
- インストールとパッケージのセットアップ
- SfSignaturePad の基本的な実装
- パッケージの依存関係とインポート
- 初めて署名を取り込む例
- クイックセットアップガイド
コンポーネントの概要
📄 参照: references/overview.md
- SfSignaturePad ウィジェットの概要と機能
- SignaturePad を使用する場面
- 主な機能とユースケース
- ウィジェットのアーキテクチャ
- 基本設定
カスタマイズとスタイル設定
📄 参照: references/customization.md
- ストロークの色のカスタマイズ
- ストロークの幅と最小/最大設定
- 背景色と画像
- 枠線とコンテナーのスタイル設定
- 透明な背景
- 外観のカスタマイズ
- テーマとの統合
保存とエクスポート
📄 参照: references/saving-exporting.md
- 署名を画像に変換する(toImage メソッド)
- 画像形式のオプション(PNG、JPEG)
- 解像度と品質の設定
- 署名をストレージに保存する
- Uint8List への変換
- 署名パッドのクリア
- 最小ストローク数の検証
- エクスポートのパターンとベストプラクティス
イベント処理
📄 参照: references/callbacks.md
- onDrawStart コールバック
- onDraw コールバック
- onDrawEnd コールバック
- 署名イベントの処理
- 描画中の検証
- リアルタイムのフィードバックパターン
- イベント引数の型
高度な機能
📄 参照: references/advanced-features.md
- 署名を表示するための読み取り専用モード
- 背景画像のサポート
- 署名の検証手法
- 描画のカスタム制約
- フォームとの統合
- 署名の比較
- 複数署名のワークフロー
- 元に戻す/やり直しの実装パターン
アクセシビリティ
📄 読む: references/accessibility.md
- スクリーンリーダーのサポート
- 署名フィールドのセマンティックラベル
- キーボード操作のアクセシビリティに関する考慮事項
- 代替入力方法
- WCAG への準拠
- アクセシブルな署名ワークフロー
クイックスタートの例
基本的な署名パッド
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_signaturepad/signaturepad.dart';
class BasicSignaturePad extends StatelessWidget {
final GlobalKey<SfSignaturePadState> _signaturePadKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Sign Here')),
body: Column(
children: [
Padding(
padding: EdgeInsets.all(16),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
),
child: SfSignaturePad(
key: _signaturePadKey,
backgroundColor: Colors.white,
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () => _signaturePadKey.currentState!.clear(),
child: Text('Clear'),
),
ElevatedButton(
onPressed: _saveSignature,
child: Text('Save'),
),
],
),
],
),
);
}
Future<void> _saveSignature() async {
final image = await _signaturePadKey.currentState!.toImage();
// Process the image (save to file, upload, etc.)
}
}
カスタマイズした署名パッド
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_signaturepad/signaturepad.dart';
class CustomSignaturePad extends StatelessWidget {
final GlobalKey<SfSignaturePadState> _signaturePadKey = GlobalKey();
@override
Widget build(BuildContext context) {
return SfSignaturePad(
key: _signaturePadKey,
backgroundColor: Colors.grey[100],
strokeColor: Colors.blue,
minimumStrokeWidth: 1.0,
maximumStrokeWidth: 4.0,
onDrawStart: () {
print('Started drawing');
return true;
},
onDraw: (offset, time) {
print('Drawing at: $offset');
},
onDrawEnd: () {
print('Finished drawing');
},
);
}
}
検証機能付き署名パッド
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_signaturepad/signaturepad.dart';
import 'dart:ui' as ui;
class ValidatedSignaturePad extends StatefulWidget {
@override
_ValidatedSignaturePadState createState() => _ValidatedSignaturePadState();
}
class _ValidatedSignaturePadState extends State<ValidatedSignaturePad> {
final GlobalKey<SfSignaturePadState> _signaturePadKey = GlobalKey();
String _validationMessage = '';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Sign Document')),
body: Column(
children: [
Expanded(
child: Padding(
padding: EdgeInsets.all(16),
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey, width: 2),
borderRadius: BorderRadius.circular(8),
),
child: SfSignaturePad(
key: _signaturePadKey,
backgroundColor: Colors.white,
strokeColor: Colors.black,
minimumStrokeWidth: 2.0,
maximumStrokeWidth: 4.0,
),
),
),
),
if (_validationMessage.isNotEmpty)
Padding(
padding: EdgeInsets.all(8),
child: Text(
_validationMessage,
style: TextStyle(color: Colors.red),
),
),
Padding(
padding: EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
OutlinedButton(
onPressed: _clearSignature,
child: Text('Clear'),
),
ElevatedButton(
onPressed: _saveSignature,
child: Text('Save Signature'),
),
],
),
),
],
),
);
}
void _clearSignature() {
setState(() {
_signaturePadKey.currentState!.clear();
_validationMessage = '';
});
}
Future<void> _saveSignature() async {
// Validate minimum stroke count
if (_signaturePadKey.currentState!.toImage == null) {
setState(() {
_validationMessage = 'Please provide a signature';
});
return;
}
try {
final ui.Image image = await _signaturePadKey.currentState!.toImage(
pixelRatio: 3.0,
);
// Convert to bytes
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
final bytes = byteData!.buffer.asUint8List();
// Save or upload the signature
print('Signature saved: ${bytes.length} bytes');
setState(() {
_validationMessage = 'Signature saved successfully!';
});
// Navigate or perform next action
} catch (e) {
setState(() {
_validationMessage = 'Error saving signature: $e';
});
}
}
}
背景画像付き署名パッド
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_signaturepad/signaturepad.dart';
class SignatureWithBackground extends StatelessWidget {
final GlobalKey<SfSignaturePadState> _signaturePadKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
image: DecorationImage(
image: AssetImage('assets/signature_background.png'),
fit: BoxFit.cover,
opacity: 0.3,
),
),
child: SfSignaturePad(
key: _signaturePadKey,
backgroundColor: Colors.transparent,
strokeColor: Colors.blue[900]!,
minimumStrokeWidth: 2.0,
maximumStrokeWidth: 5.0,
),
);
}
}
よくあるパターン
パターン 1:文書への署名フロー
class DocumentSigningFlow extends StatefulWidget {
@override
_DocumentSigningFlowState createState() => _DocumentSigningFlowState();
}
class _DocumentSigningFlowState extends State<DocumentSigningFlow> {
final GlobalKey<SfSignaturePadState> _signaturePadKey = GlobalKey();
bool _hasSignature = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Sign Agreement')),
body: Column(
children: [
// Document preview
Expanded(
flex: 2,
child: Container(
padding: EdgeInsets.all(16),
child: Text('Agreement Terms...'),
),
),
// Signature section
Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Your Signature',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Container(
height: 200,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: SfSignaturePad(
key: _signaturePadKey,
backgroundColor: Colors.white,
strokeColor: Colors.black,
minimumStrokeWidth: 2.0,
maximumStrokeWidth: 4.0,
onDrawEnd: () {
setState(() => _hasSignature = true);
},
),
),
],
),
),
// Action buttons
Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: _clearSignature,
child: Text('Clear'),
),
),
SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: _hasSignature ? _submitSignature : null,
child: Text('Submit'),
),
),
],
),
),
],
),
);
}
void _clearSignature() {
setState(() {
_signaturePadKey.currentState!.clear();
_hasSignature = false;
});
}
Future<void> _submitSignature() async {
final image = await _signaturePadKey.currentState!.toImage(pixelRatio: 3.0);
// Submit signature with document
Navigator.pop(context, image);
}
}
パターン 2:複数の署名の収集
class MultipleSignaturesForm extends StatefulWidget {
@override
_MultipleSignaturesFormState createState() => _MultipleSignaturesFormState();
}
class _MultipleSignaturesFormState extends State<MultipleSignaturesForm> {
final GlobalKey<SfSignaturePadState> _applicantKey = GlobalKey();
final GlobalKey<SfSignaturePadState> _witnessKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Signatures Required')),
body: SingleChildScrollView(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSignatureField(
'Applicant Signature',
_applicantKey,
),
SizedBox(height: 24),
_buildSignatureField(
'Witness Signature',
_witnessKey,
),
SizedBox(height: 24),
ElevatedButton(
onPressed: _submitAllSignatures,
child: Text('Submit All'),
),
],
),
),
);
}
Widget _buildSignatureField(String label, GlobalKey<SfSignaturePadState> key) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Container(
height: 150,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: SfSignaturePad(
key: key,
backgroundColor: Colors.white,
strokeColor: Colors.blue[900]!,
),
),
TextButton(
onPressed: () => key.currentState!.clear(),
child: Text('Clear'),
),
],
);
}
Future<void> _submitAllSignatures() async {
final applicantImage = await _applicantKey.currentState!.toImage();
final witnessImage = await _witnessKey.currentState!.toImage();
// Submit both signatures
}
}
パターン 3:読み取り専用の署名表示
class SignatureDisplay extends StatefulWidget {
final Uint8List signatureData;
SignatureDisplay({required this.signatureData});
@override
_SignatureDisplayState createState() => _SignatureDisplayState();
}
class _SignatureDisplayState extends State<SignatureDisplay> {
@override
Widget build(BuildContext context) {
return Container(
height: 200,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: Image.memory(
widget.signatureData,
fit: BoxFit.contain,
),
);
}
}
パターン 4:リアルタイムのフィードバック付き署名
class InteractiveSignaturePad extends StatefulWidget {
@override
_InteractiveSignaturePadState createState() => _InteractiveSignaturePadState();
}
class _InteractiveSignaturePadState extends State<InteractiveSignaturePad> {
final GlobalKey<SfSignaturePadState> _signaturePadKey = GlobalKey();
bool _isDrawing = false;
int _strokeCount = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
height: 300,
decoration: BoxDecoration(
border: Border.all(
color: _isDrawing ? Colors.blue : Colors.grey,
width: _isDrawing ? 2 : 1,
),
borderRadius: BorderRadius.circular(8),
),
child: SfSignaturePad(
key: _signaturePadKey,
backgroundColor: Colors.white,
strokeColor: Colors.black,
minimumStrokeWidth: 2.0,
maximumStrokeWidth: 4.0,
onDrawStart: () {
setState(() {
_isDrawing = true;
_strokeCount++;
});
return true;
},
onDrawEnd: () {
setState(() {
_isDrawing = false;
});
},
),
),
SizedBox(height: 8),
Text(
_isDrawing ? 'Drawing...' : 'Stroke count: $_strokeCount',
style: TextStyle(color: Colors.grey[600]),
),
],
);
}
}
主なプロパティ
基本プロパティ
backgroundColor- 署名パッドの背景色(Color)strokeColor- 署名のストロークの色(Color、デフォルト:Colors.black)minimumStrokeWidth- ストロークの最小幅(double、デフォルト:1.0)maximumStrokeWidth- ストロークの最大幅(double、デフォルト:4.0)
コールバックプロパティ
onDrawStart- 描画開始時のコールバック(SignatureOnDrawStartCallback → bool Function(); 描画を許可する場合は true、キャンセルする場合は false を返します)onDraw- オフセットとタイムスタンプを含む、描画中のコールバック(DrawCallback)onDrawEnd- 描画終了時のコールバック(VoidCallback)
状態メソッド(GlobalKey 経由)
clear()- 署名パッドのすべてのストロークを消去しますtoImage({double pixelRatio})- 署名を ui.Image に変換しますpixelRatio- 解像度の倍率(デフォルト:1.0。値を大きくすると品質が向上します)
よくあるユースケース
- 契約書への署名 - 法的文書や契約書用のデジタル署名の取得
- 配達確認 - 荷物の配達時に受取人の署名を取得
- 医療同意書 - 同意および承認のための患者の署名
- 登録フォーム - アカウント作成や登録時のユーザーの署名
- チェックアウトプロセス - 支払いまたは購入完了時の署名の取得
- 承認ワークフロー - 承認のためのマネージャーまたは権限者の署名
- 領収書の受領確認 - デジタル領収書への顧客の署名
- タイムシートへの署名 - 出勤またはタイムシート承認のための従業員の署名
- 訪問者管理 - チェックイン時の訪問者の署名
- 規約への同意 - 署名による利用規約への同意
ベストプラクティス
検証
// Always validate before saving
Future<bool> validateSignature() async {
try {
final image = await _signaturePadKey.currentState!.toImage();
// Check if image has meaningful content
return true;
} catch (e) {
return false; // No signature drawn
}
}
画像品質
// Use higher pixel ratio for better quality
final image = await _signaturePadKey.currentState!.toImage(
pixelRatio: 3.0, // 3x resolution for high-quality signatures
);
ユーザーエクスペリエンス
- わかりやすい「クリア」ボタンと「保存」ボタンを用意します
- 描画中に視覚的なフィードバック(枠線の強調表示)を示します
- 検証メッセージを明確に表示します
- 「ここに署名」インジケーターや背景の透かしの追加を検討します
- 間違えた場合にユーザーがやり直せるようにします
- 署名の取得に成功した後、確認メッセージを表示します
ストレージ
import 'dart:ui' as ui;
import 'dart:typed_data';
Future<Uint8List> getSignatureBytes() async {
final ui.Image image = await _signaturePadKey.currentState!.toImage(
pixelRatio: 3.0,
);
final ByteData? byteData = await image.toByteData(
format: ui.ImageByteFormat.png,
);
return byteData!.buffer.asUint8List();
}
ヒントと注意点
- 署名パッドのメソッドにアクセスするには、必ず
GlobalKey<SfSignaturePadState>を使用します - 署名を保持するため、画面遷移の前に
toImage()を呼び出します pixelRatioの値を大きくすると、ファイルサイズは大きくなりますが、品質が向上します- 検証に失敗した場合は、混乱を避けるためパッドをクリアします
- タッチ対応デバイスとスタイラス対応デバイスの両方でテストします
- 検証用にストローク数のチェックを追加することを検討します
- 視認性を確保するため、ストロークと背景にはコントラストの高い色を使用します
- 署名しやすいように、十分な高さ(150~300px)を確保します