บทที่ 20advanced|9 KB
บทที่ 20: Export & Share Features ขั้นสูง
สารบัญเนื้อหา
บทที่ 20: Export & Share Features ขั้นสูง
🎯 เป้าหมายการเรียนรู้
หลังจากเรียนบทนี้แล้ว คุณจะสามารถ:
- สร้างระบบ Export รูปภาพที่รองรับหลายขนาด (Instagram Story 9:16, Facebook Post 1:1)
- วาดลวดลายกราฟิกที่ซับซ้อนด้วย
CustomPainter(Canvas API) - เพิ่มลายน้ำ (Watermark) ลงในภาพที่ Export
- จัดการ Clipboard (Copy Text/Image)
- สร้าง UI สำหรับเลือก Theme แบบ Real-time Preview
📚 ทฤษฎีและแนวคิด
1. Canvas API
ถ้า Container และ Row/Column คือการต่อเลโก้สำเร็จรูป Canvas ก็คือการ "วาดภาพระบายสี" ลงบนกระดาษเปล่า
เราสามารถวาด:
- เส้น (Line), วงกลม (Circle), สี่เหลี่ยม (Rect)
- เส้นโค้ง (Bezier Curve)
- ไล่สี (Gradient)
2. Aspect Ratio (สัดส่วนภาพ)
การทำแอพคำคม สิ่งสำคัญคือรูปที่ออกไปต้องสวยในทุก Platform:
- Square (1:1): 1080x1080px (IG Post)
- Portrait (4:5): 1080x1350px (FB/IG Post)
- Story (9:16): 1080x1920px (TikTok/IG Story)
💻 การเขียนโค้ด (Implementation)
1. Advanced Custom Painter
ไฟล์: mobile/lib/widgets/pattern_painter.dart
เราจะสร้าง Painter ที่วาดลวดลาย Background แบบสุ่มหรือมี Pattern
import 'package:flutter/material.dart';
import 'dart:math';
**อธิบายทีละบรรทัด (Imports):**
- `import 'dart:math'`
- Library คณิตศาสตร์ (Random, Sin, Cos) ใช้สำหรับการสุ่มตำแหน่งจุด หรือคำนวณกราฟิก
**อธิบาย:**
enum PatternType { dots, lines, cross }
class PatternPainter extends CustomPainter {
final Color color;
final PatternType type;
PatternPainter({
required this.color,
this.type = PatternType.lines
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color.withOpacity(0.1)
..strokeWidth = 2
..style = PaintingStyle.stroke;
switch (type) {
case PatternType.lines:
_drawLines(canvas, size, paint);
break;
case PatternType.dots:
_drawDots(canvas, size, paint);
break;
case PatternType.cross:
_drawCross(canvas, size, paint);
break;
}
}
void _drawLines(Canvas canvas, Size size, Paint paint) {
// วาดเส้นทแยงมุมห่างกันทีละ 20px
for (double i = -size.height; i < size.width; i += 20) {
canvas.drawLine(
Offset(i, 0),
Offset(i + size.height, size.height),
paint,
);
}
}
void _drawDots(Canvas canvas, Size size, Paint paint) {
paint.style = PaintingStyle.fill;
final step = 30.0;
for (double x = 0; x < size.width; x += step) {
for (double y = 0; y < size.height; y += step) {
canvas.drawCircle(Offset(x, y), 2, paint);
}
}
}
void _drawCross(Canvas canvas, Size size, Paint paint) {
// ... logic วาดกากบาท
}
@override
bool shouldRepaint(covariant PatternPainter oldDelegate) {
// repaint เมื่อสีเปลี่ยน หรือแบบเปลี่ยน
return color != oldDelegate.color || type != oldDelegate.type;
}
}
2. Aspect Ratio Selector
เราต้องให้ User เลือกได้ว่าจะเอารูปขนาดไหน
class AspectRatioSelector extends StatelessWidget {
final Function(double) onSelected;
final double currentRatio;
const AspectRatioSelector({
required this.onSelected,
required this.currentRatio
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_RatioButton(label: "IG Post", ratio: 1.0, current: currentRatio, onTap: onSelected),
_RatioButton(label: "Portrait", ratio: 4/5, current: currentRatio, onTap: onSelected),
_RatioButton(label: "Story", ratio: 9/16, current: currentRatio, onTap: onSelected),
],
);
}
}
class _RatioButton extends StatelessWidget {
// ... implementation ปุ่มกดง่ายๆ
}
3. ใส่ลายน้ำ (Watermark)
เทคนิคคือการใช้ Stack ซ้อนข้อความไว้ล่างสุด หรือวาดลงไปใน Canvas เลยก็ได้ แต่ Stack ง่ายกว่า
RepaintBoundary(
key: globalKey,
child: AspectRatio(
aspectRatio: currentRatio,
child: Stack(
children: [
// 1. Background (สี/รูป/Pattern)
Container(color: bgColor),
CustomPaint(painter: PatternPainter(...)),
// 2. Quote (ตรงกลาง)
Center(child: Text(quote)),
// 3. Watermark (มุมขวาล่าง)
Positioned(
bottom: 16,
right: 16,
child: Row(
children: [
Icon(Icons.auto_awesome, size: 16, color: Colors.white54),
SizedBox(width: 4),
Text(
"Via QUOM App",
style: TextStyle(
color: Colors.white54,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
),
),
],
),
),
)
4. Clipboard Integration
บางที User ไม่อยากเซฟรูป แต่อยากก๊อปแค่ข้อความไปลง Twitter
import 'package:flutter/services.dart';
void _copyToClipboard(BuildContext context, String text) {
// ก๊อปปี้ลง Clipboard
Clipboard.setData(ClipboardData(text: text));
// แจ้งเตือน
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('คัดลอกข้อความแล้ว'),
duration: Duration(seconds: 1),
),
);
}
📝 แบบฝึกหัด
โจทย์ที่ 1: Image Background
สิ่งที่ต้องทำ:
- ให้ User เลือกรูปจาก Gallery (
image_picker) (ทบทวนบท 15) - นำรูปมาวางเป็น Background ของ Card
- ใส่
ColorFilter(สีดำโปร่งแสง) ทับรูป เพื่อให้ตัวหนังสืออ่านออก
โจทย์ที่ 2: Drag & Drop Watermark
สิ่งที่ต้องทำ:
- ทำให้ลายน้ำขยับได้ด้วยนิ้ว (
DraggableหรือPositioned+GestureDetector) - User สามารถลากลายน้ำไปวางตรงไหนก็ได้ที่ชอบ
โจทย์ที่ 3: Save as Preset
สิ่งที่ต้องทำ:
- เมื่อ User แต่งรูปสวยแล้ว ให้มีปุ่ม "Save Preset"
- เก็บค่าสี, ฟอนต์, pattern ลง
SharedPreferences - ครั้งหน้าเปิดมา เลือก Preset เดิมได้เลย
📌 สรุป
ระบบ Export ที่ดีต้องมีความ "ยืดหยุ่น":
- Canvas: ช่วยให้เราสร้างกราฟิกที่ไม่ซ้ำใคร
- Aspect Ratio: สำคัญมากในยุค Social Media
- Watermark: ช่วยโปรโมทแอปของเราไปในตัว
ไฟล์ที่เกี่ยวข้อง
🔜 บทถัดไป
บทที่ 21: Dynamic Theme System เจาะลึกการทำ Dark Mode แบบ Advance และการเปลี่ยนสีแอปทั้งตัว