欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Flutter實現(xiàn)給圖片添加涂鴉功能

 更新時間:2024年01月25日 08:23:15   作者:如此風景  
這篇文章主要介紹了利用Flutter實現(xiàn)給圖片添加涂鴉功能,文中通過代碼示例給大家講解的非常詳細,對大家的學習或工作有一定的幫助,需要的朋友可以參考下

簡介

先來張圖,看一下最終效果

關閉和確定

  • 關閉和確定功能對應的是界面左下角叉號,和右下角對鉤,關閉按鈕僅做取消當次涂鴉,讀者可自行設置點擊后功能,也可自行更改相應UI。選擇功能點擊后會執(zhí)行一段把當前涂鴉后的圖片合成并保存到本地的操作。具體請看示例代碼。

顏色選擇

  • 顏色選擇功能可選擇和標識當前涂鴉顏色,和指示當前涂鴉顏色的選中狀態(tài)(以白色外圈標識)。切換顏色后下一次涂鴉即會使用新的顏色。

撤銷功能

  • 撤銷功能可撤銷最近的一次涂鴉。如沒有涂鴉時顯示置灰的撤銷按鈕。

清除功能

  • 清除功能可清除所有涂鴉,如當前沒有任何涂鴉時顯示置灰的清除按鈕。

涂鴉圖片的放大和縮小

  • 可雙指滑動切換涂鴉放大縮小的效果。

放大縮小后按照新的線條粗細繼續(xù)涂鴉

  • 涂鴉放大或縮小后,涂鴉線條會隨之放大和縮小,此時如果繼續(xù)涂鴉,則新涂鴉顯示的粗細程度與放大或縮小后的線條粗細程度保持一致。

保存涂鴉圖片到本地。

  • flutter涂鴉后的圖片可合成新圖片并保存到本地路徑。

代碼介紹

涂鴉顏色選擇組件。

主要是顯示為可配置的圓點和外圈

circle_ring_widget.dart

import 'package:flutter/material.dart';

class CircleRingWidget extends StatelessWidget {
  late bool isShowRing;
  late Color dotColor;
  CircleRingWidget(this.isShowRing,this.dotColor, {super.key});
  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: CircleAndRingPainter(isShowRing,dotColor),
      size: const Size(56.0, 81.0), // 調整尺寸大小
    );
  }
}

class CircleAndRingPainter extends CustomPainter {
  late bool isShowRing;
  late Color dotColor;
  CircleAndRingPainter(this.isShowRing,this.dotColor);
  @override
  void paint(Canvas canvas, Size size) {
    Paint circlePaint = Paint()
      ..color = dotColor // 設置圓點的顏色
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 1.0;

    Paint ringPaint = Paint()
      ..color = Colors.white // 設置圓環(huán)的顏色
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 1.0
      ..style = PaintingStyle.stroke;

    Offset center = size.center(Offset.zero);

    // 畫一個半徑為10的圓點
    canvas.drawCircle(center, 13.0, circlePaint);

    if(isShowRing){
      // 畫一個半徑為20的圓環(huán)
      canvas.drawCircle(center, 18.0, ringPaint);
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

存儲顏色和食指劃過點的數(shù)據(jù)類

color_offset.dart

import 'dart:ui';

class ColorOffset{
  late Color color;
  late List<Offset> offsets=[];
  ColorOffset(Color color,List<Offset> offsets){
    this.color=color;
    this.offsets.addAll(offsets);
  }
}

具體涂鴉點的繪制

此處是具體繪制涂鴉點的自定義view。大家是不是覺得哇,好簡單呢。兩個循環(huán)一嵌套,瞬間所有涂鴉就都出來了。其實做這個功能時,我參考了其他各種涂鴉控件,但是總覺得流程非常復雜。難以理解。原因是他們的顏色和點在數(shù)據(jù)層面都是混合到一起的,而且還得判斷哪里是新畫的涂鴉線條,來回控制。用這個demo的結構,相信各位讀者一看就能知道里面的思路

doodle_painter.dart

import 'package:flutter/cupertino.dart';

import 'color_offset.dart';

class DoodleImagePainter extends CustomPainter {
  late Map<int,ColorOffset> newPoints;
  DoodleImagePainter(this.newPoints);

  @override
  void paint(Canvas canvas, Size size) {
    newPoints.forEach((key, value) {
      Paint paint = _getPaint(value.color);
      for(int i=0;i<value.offsets.length - 1;i++){
        //最后一個畫點,其他畫線
        if(i==value.offsets.length-1){
          canvas.drawCircle(value.offsets[i], 2.0, paint);
        }else{
          canvas.drawLine(value.offsets[i], value.offsets[i + 1], paint);
        }

      }
    });
  }
  Paint _getPaint(Color color){
    return Paint()
      ..color = color
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 5.0;
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}

涂鴉主要界面代碼

  • 包含整體涂鴉數(shù)據(jù)的構建
  • 包含涂鴉圖片的合成和本地存儲
  • 包含涂鴉顏色列表的自定義
  • 包含涂鴉原圖片的放大縮小
  • 包含撤銷一步和清屏功能

下面這些就是整體涂鴉相關功能代碼,其中一些資源圖片未提供,請根據(jù)需要自己去設計處獲取。

import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

import '../player_base_control.dart';
import 'circle_ring_widget.dart';
import 'color_offset.dart';
import 'doodle_painter.dart';

class DoodleWidget extends PlayerBaseControllableWidget {
  final String snapShotPath;
  final ValueChanged<bool>? completed;
  const DoodleWidget(super.controller,
      {super.key, required this.snapShotPath, this.completed});

  @override
  State<StatefulWidget> createState() => _DoodleWidgetState();
}

class _DoodleWidgetState extends State<DoodleWidget> {
  Map<int, ColorOffset> newPoints = {};
  List<Offset> points = [];
  int lineIndex = 0;
  GlobalKey globalKey = GlobalKey();
  int currentSelect = 0;
  final double maxScale = 3.0;
  final double minScale = 1.0;
  List<Color> colors = const [
    Color(0xffff0000),
    Color(0xfffae03d),
    Color(0xff6f52ff),
    Color(0xffffffff),
    Color(0xff000000)
  ];
  TransformationController controller = TransformationController();
  double realScale = 1.0;
  Offset realTransLocation = Offset.zero;
  late Image currentImg;

  bool isSaved = false;

  @override
  void initState() {
    currentImg = Image.memory(File(widget.snapShotPath).readAsBytesSync());
    controller.addListener(() {
      ///獲取矩陣里面的縮放具體值
      realScale = controller.value.entry(0, 0);

      ///獲取矩陣里面的位置偏移量
      realTransLocation = Offset(controller.value.getTranslation().x,
          controller.value.getTranslation().y);
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Positioned.fill(child: LayoutBuilder(
            builder: (BuildContext context, BoxConstraints constraint) {
          return InteractiveViewer(
            panEnabled: false,
            scaleEnabled: true,
            maxScale: maxScale,
            minScale: minScale,
            transformationController: controller,
            onInteractionStart: (ScaleStartDetails details) {
              // print("--------------onInteractionStart執(zhí)行了  dx=${details.focalPoint.dx} dy=${details.focalPoint.dy}");
            },
            onInteractionUpdate: (ScaleUpdateDetails details) {
              if (details.pointerCount == 1) {
                /// 獲取 x,y 拿到值后進行縮放偏移等換算
                var x = details.focalPoint.dx;
                var y = details.focalPoint.dy;
                var point = Offset(
                    _getScaleTranslateValue(x, realScale, realTransLocation.dx),
                    _getScaleTranslateValue(
                        y, realScale, realTransLocation.dy));
                setState(() {
                  points.add(point);
                  newPoints[lineIndex] =
                      ColorOffset(colors[currentSelect], points);
                });
              }
            },
            onInteractionEnd: (ScaleEndDetails details) {
              // print("onInteractionEnd執(zhí)行了");
              if (points.length > 5) {
                newPoints[lineIndex] =
                    ColorOffset(colors[currentSelect], points);
                lineIndex++;
              }
              // newPoints.addAll({lineIndex:ColorOffset(colors[currentSelect],points)});
              //清空原數(shù)組
              points.clear();
            },
            child: RepaintBoundary(
              key: globalKey,
              child: Stack(
                alignment: AlignmentDirectional.center,
                children: [
                  Positioned.fill(child: currentImg),
                  Positioned.fill(
                      child:
                          CustomPaint(painter: DoodleImagePainter(newPoints))),
                ],
              ),
            ),
          );
        })),
        Positioned(
          bottom: 0,
          left: 0,
          right: 0,
          child: _bottomActions(),
        )
      ],
    );
  }

  double _getScaleTranslateValue(
      double current, double scale, double translate) {
    return current / scale - translate / scale;
  }

  Widget _bottomActions() {
    return Container(
      height: 81,
      color: const Color(0xaa17161f),
      child: Row(
        children: [
          /// 關閉按鈕
          SizedBox(
            width: 95,
            height: 81,
            child: Center(
              child: GestureDetector(
                onTap: () {
                  widget.completed?.call(false);
                },
                child: Image.asset(
                  "images/icon_close_white.webp",
                  width: 30,
                  height: 30,
                  scale: 3,
                  package: "koo_daxue_record_player",
                ),
              ),
            ),
          ),
          const VerticalDivider(
            thickness: 1,
            indent: 15,
            endIndent: 15,
            color: Colors.white,
          ),
          Row(
            children: _colorListWidget(),
          ),
          Expanded(child: Container()),

          /// 退一步按鈕
          SizedBox(
            width: 66,
            height: 81,
            child: GestureDetector(
              onTap: () {
                setState(() {
                  if (lineIndex > 0) {
                    lineIndex--;
                    newPoints.remove(lineIndex);
                  }
                });
              },
              child: Center(
                  child: Image.asset(
                lineIndex == 0
                    ? "images/icon_undo.webp"
                    : "images/icon_undo_white.webp",
                width: 30,
                height: 30,
                scale: 3,
                package: "koo_daxue_record_player",
              )),
            ),
          ),

          /// 清除按鈕
          SizedBox(
            width: 66,
            height: 81,
            child: Center(
                child: GestureDetector(
              onTap: () {
                setState(() {
                  lineIndex = 0;
                  newPoints.clear();
                });
              },
              child: Image.asset(
                lineIndex == 0
                    ? "images/icon_clear_doodle.webp"
                    : "images/icon_clear_doodle_white.webp",
                width: 30,
                height: 30,
                scale: 3,
                package: "koo_daxue_record_player",
              ),
            )),
          ),
          const VerticalDivider(
            thickness: 1,
            indent: 15,
            endIndent: 15,
            color: Colors.white,
          ),

          /// 確定按鈕
          SizedBox(
            width: 85,
            height: 81,
            child: Center(
                child: GestureDetector(
              onTap: () {
                if (isSaved) return;
                isSaved = true;
                if (newPoints.isEmpty) {
                  widget.completed?.call(false);
                  return;
                }
                saveDoodle(widget.snapShotPath).then((value) {
                  if (value) {
                    widget.completed?.call(true);
                  } else {
                    widget.completed?.call(false);
                  }
                });
              },
              child: Image.asset(
                "images/icon_finish_white.webp",
                width: 30,
                height: 30,
                scale: 3,
                package: "koo_daxue_record_player",
              ),
            )),
          )
        ],
      ),
    );
  }

  List<Widget> _colorListWidget() {
    List<Widget> widgetList = [];
    for (int i = 0; i < colors.length; i++) {
      Color color = colors[i];
      widgetList.add(GestureDetector(
        onTap: () {
          setState(() {
            currentSelect = i;
          });
        },
        child: CircleRingWidget(i == currentSelect, color),
      ));
    }
    return widgetList;
  }

  Future<bool> saveDoodle(String imgPath) async {
    try {
      RenderRepaintBoundary boundary =
          globalKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
      ui.Image image = await boundary.toImage(pixelRatio: 3.0);
      ByteData? byteData =
          await image.toByteData(format: ui.ImageByteFormat.png);
      Uint8List pngBytes = byteData!.buffer.asUint8List();
      // 保存圖片到文件
      File imgFile = File(imgPath);
      await imgFile.writeAsBytes(pngBytes);
      return true;
    } catch (e) {
      return false;
    }
  }
}

以上就是Flutter實現(xiàn)給圖片添加涂鴉功能的詳細內容,更多關于Flutter給圖片添加涂鴉的資料請關注腳本之家其它相關文章!

相關文章

  • AndroidStudio工程打包aab文件

    AndroidStudio工程打包aab文件

    本文主要介紹了AndroidStudio工程打包aab文件,文中通過圖文的非常詳細,需要的朋友們下面隨著小編來一起學習學習吧
    2021-07-07
  • Kotlin如何使用類似C#的yield功能詳解

    Kotlin如何使用類似C#的yield功能詳解

    在語句中使用 yield 關鍵字,則指示在的方案、運算符或 get 訪問器是迭代器。下面這篇文章主要給大家介紹了關于Kotlin如何使用類似C#的yield功能的相關資料,需要的朋友可以參考借鑒,下面來一起看看吧
    2018-06-06
  • Android異步消息機制詳解

    Android異步消息機制詳解

    這篇文章主要為大家詳細介紹了Android異步消息機制的相關資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2017-12-12
  • Android實現(xiàn)文件按時間先后順序排列顯示的示例代碼

    Android實現(xiàn)文件按時間先后順序排列顯示的示例代碼

    在很多Android應用中,需要管理和展示本地文件,對文件按最后修改時間排序展示,能讓用戶直觀地了解文件的創(chuàng)建或修改順序,從而更方便地查找最新或最舊的文件,本文將介紹如何在Android平臺上獲取指定目錄下的文件列表,并按照時間先后排序,需要的朋友可以參考下
    2025-04-04
  • Flutter開發(fā)技巧RadialGradient中radius計算詳解

    Flutter開發(fā)技巧RadialGradient中radius計算詳解

    這篇文章主要為大家介紹了Flutter小技巧RadialGradient?中?radius?的計算詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-01-01
  • Android自定義 WebView瀏覽器

    Android自定義 WebView瀏覽器

    WebView是Android中一個非常實用的組件,它和Safai、Chrome一樣都是基于Webkit網頁渲染引擎。接下來通過本文給大家介紹android自定義webview瀏覽器,感興趣的朋友一起學習吧
    2016-05-05
  • 深入分析Android ViewStub的應用詳解

    深入分析Android ViewStub的應用詳解

    本篇文章是對Android ViewStub的應用進行了詳細的分析介紹,需要的朋友參考下
    2013-05-05
  • android實現(xiàn)session保持簡要概述及實現(xiàn)

    android實現(xiàn)session保持簡要概述及實現(xiàn)

    其實sesion在瀏覽器和web服務器直接是通過一個叫做name為sessionid的cookie來傳遞的,所以只要在每次數(shù)據(jù)請求時保持sessionid是同一個不變就可以用到web的session了,感興趣的你可以參考下本文或許對你有所幫助
    2013-03-03
  • Android Handler多線程詳解

    Android Handler多線程詳解

    本文主要介紹Android Handler的知識,這里整理了詳細的資料及簡單示例代碼和實現(xiàn)效果圖,有興趣的小伙伴可以參考下
    2016-09-09
  • Android中DrawerLayout+ViewPager滑動沖突的解決方法

    Android中DrawerLayout+ViewPager滑動沖突的解決方法

    這篇文章主要為大家詳細介紹了Android中DrawerLayout+ViewPager滑動沖突的解決方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2017-06-06

最新評論