บทที่ 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

สิ่งที่ต้องทำ:

  1. ให้ User เลือกรูปจาก Gallery (image_picker) (ทบทวนบท 15)
  2. นำรูปมาวางเป็น Background ของ Card
  3. ใส่ ColorFilter (สีดำโปร่งแสง) ทับรูป เพื่อให้ตัวหนังสืออ่านออก

โจทย์ที่ 2: Drag & Drop Watermark

สิ่งที่ต้องทำ:

  1. ทำให้ลายน้ำขยับได้ด้วยนิ้ว (Draggable หรือ Positioned + GestureDetector)
  2. User สามารถลากลายน้ำไปวางตรงไหนก็ได้ที่ชอบ

โจทย์ที่ 3: Save as Preset

สิ่งที่ต้องทำ:

  1. เมื่อ User แต่งรูปสวยแล้ว ให้มีปุ่ม "Save Preset"
  2. เก็บค่าสี, ฟอนต์, pattern ลง SharedPreferences
  3. ครั้งหน้าเปิดมา เลือก Preset เดิมได้เลย

📌 สรุป

ระบบ Export ที่ดีต้องมีความ "ยืดหยุ่น":

  • Canvas: ช่วยให้เราสร้างกราฟิกที่ไม่ซ้ำใคร
  • Aspect Ratio: สำคัญมากในยุค Social Media
  • Watermark: ช่วยโปรโมทแอปของเราไปในตัว

ไฟล์ที่เกี่ยวข้อง


🔜 บทถัดไป

บทที่ 21: Dynamic Theme System เจาะลึกการทำ Dark Mode แบบ Advance และการเปลี่ยนสีแอปทั้งตัว