フラッターチュートリアル:イメージ拡大鏡


特に画像を横切ってマーカーをドラッグすると、それは指で覆われているため、領域を拡大拡大鏡を表示するにはかなり一般的です.それをフラッターで実装する方法を見てみましょう.

ゴール


上記の説明は非常に広い.拡大鏡の主要な特徴を定義することによって、さらに詳細に行こう
  • 画像の向こう側にドラッグすることができるよりも半透明の円があります
  • 一度ドラッグされると、拡大鏡が左上に表示されます
  • 円が拡大鏡に干渉するたび、右上にジャンプする
  • 拡大鏡は、定義可能な因子によって下にある画像の領域を拡大する
  • 一旦ドラッグが終わると、拡大鏡は消えます
  • これは、すべてのウィジェットのためだけでなく、画像のために動作しません
  • 実装


    我々は、ズームする可能性を追加したいイメージを含むサンプル画面を実装することから始めます.このため、最も人気のあるテストイメージを使用します.Lena Forsén, better known as Lenna .
      @override
      Widget build(BuildContext context) {
        return Stack(
          children: [
            Image(
              image: AssetImage('assets/lenna.png')
            )
          ],
        );
      }
    
    私たちはStack ウィジェットは、我々は後にタッチバブルと拡大鏡を置くしたいので.

    タッチバブル


    今、我々はユーザーが拡大されるべきイメージの一部に位置することができるタッチバブルを必要とします.
    class _SampleImageState extends State<SampleImage> {
      static const double touchBubbleSize = 20;
    
      Offset position;
      double currentBubbleSize;
      bool magnifierVisible = false;
    
      @override
      void initState() {
        currentBubbleSize = touchBubbleSize;
        SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return Stack(
          children: [
            Magnifier(
              position: position,
              visible: magnifierVisible,
              child: Image(
                image: AssetImage('assets/lenna.png')
              ),
            ),
            _getTouchBubble()
          ],
        );
      }
    
      Positioned _getTouchBubble() {
        return Positioned(
            top: position == null ? 0 : position.dy - currentBubbleSize / 2,
            left: position == null ? 0 : position.dx - currentBubbleSize / 2,
            child: GestureDetector(
              child: Container(
                width: currentBubbleSize,
                height: currentBubbleSize,
                decoration: BoxDecoration(
                  shape: BoxShape.circle,
                  color: Theme.of(context).accentColor.withOpacity(0.5)
                ),
              )
            )
        );
      }
    
    我々は、我々は拡大鏡にその情報を提供できるようにタッチバブルの位置を格納する必要があります.これは、拡大鏡がタッチバブルによって決定される特定の領域を拡大できるだけのために必要である.
    また、バブルのときに相互作用が起こっているときに戻るベースサイズを決定タッチバブルの静的なサイズを持っている.また、ユーザーがバブルをドラッグするときに異なる実際の現在のサイズが必要です.ドラッグイベントをハイライトするには、バブルが50 %成長するようにします.
    我々は上記のように実装することができるだけで、サンプル画像ウィジェットに存在する独自のメソッドにタッチバブルの視覚的な説明を抽出します.しかし、それは良い考えだput it into its own widget .
    これにより次のようになります.
    import 'package:flutter/material.dart';
    
    class TouchBubble extends StatelessWidget {
      TouchBubble({
        @required this.position,
        this.bubbleSize
      });
    
      final Offset position;
      final double bubbleSize;
    
      @override
      Widget build(BuildContext context) {
        return Positioned(
          top: position == null ? 0 : position.dy - bubbleSize / 2,
          left: position == null ? 0 : position.dx - bubbleSize / 2,
          child: GestureDetector(
            child: Container(
              width: bubbleSize,
              height: bubbleSize,
              decoration: BoxDecoration(
                shape: BoxShape.circle,
                color: Theme.of(context).accentColor.withOpacity(0.5)
              ),
            )
          )
        );
      }
    }
    
    これまでのところ良い.しかし、まだ相互作用はありません.

    タッチバブルは左上に表示されます.これはPositioned ウィジェット内TouchBubble クラス.
    我々の拡大鏡に双方向性を加えるためにTouchBubble コンストラクタでいくつかのコールバックを期待してください.
    import 'package:flutter/material.dart';
    
    class TouchBubble extends StatelessWidget {
      TouchBubble({
        this.position,
        @required this.onStartDragging,
        @required this.onDrag,
        @required this.onEndDragging,
        @required this.bubbleSize,
      }) : assert(onStartDragging != null),
           assert(onDrag != null),
           assert(onEndDragging != null),
           assert(bubbleSize != null && bubbleSize > 0);
    
      final Offset position;
      final double bubbleSize;
      final Function onStartDragging;
      final Function onDrag;
      final Function onEndDragging;
    
      @override
      Widget build(BuildContext context) {
        return Positioned(
          ...
          child: GestureDetector(
            onPanStart: (details) => onStartDragging(details.globalPosition),
            onPanUpdate: (details) => onDrag(details.globalPosition),
            onPanEnd: (_) => onEndDragging(),
            ...
          )
        );
      }
    }
    
    ここで三つの新たな追加引数があります.onStartDragging , onDrag and onEndDragging . これらの関数によって引き起こされる動作は、親ウィジェットで定義されます.この引数を省略した場合には、私たちは、それらのすべて(私たちがフォールバックを起こしていることを期待しています)に注釈を付けます.また、IDEの提案を無視した場合、ランタイムエラーが正しい場所で発生するというアサートを提供します.onStartDragging and onDrag は、新しい絶対的な位置を提供することが期待されます.onEndDragging は引数を期待しません.
    我々の新しいSampleImage ウィジェットは以下のようになります:
    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    import 'package:flutter_magnifier/touch_bubble.dart';
    
    class SampleImage extends StatefulWidget {
      @override
      _SampleImageState createState() => _SampleImageState();
    }
    
    class _SampleImageState extends State<SampleImage> {
      static const double touchBubbleSize = 20;
    
      Offset position;
      double currentBubbleSize;
      bool magnifierVisible = false;
    
      @override
      void initState() {
        currentBubbleSize = touchBubbleSize;
        SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return Stack(
          children: [
            Image(
              image: AssetImage('assets/lenna.png')
            ),
            TouchBubble(
              position: position,
              bubbleSize: currentBubbleSize,
              onStartDragging: _startDragging,
              onDrag: _drag,
              onEndDragging: _endDragging,
            )
          ],
        );
      }
    
      void _startDragging(Offset newPosition) {
        setState(() {
          magnifierVisible = true;
          position = newPosition;
          currentBubbleSize = touchBubbleSize * 1.5;
        });
      }
    
      void _drag(Offset newPosition) {
        setState(() {
          position = newPosition;
        });
      }
    
      void _endDragging() {
        setState(() {
          currentBubbleSize = touchBubbleSize;
          magnifierVisible = false;
        });
      }
    }
    
    位置はドラッグスタートとドラッグアップデートで更新されます.ドラッグが開始すると、MAGが可視化され、そのサイズが50 %増加する.ドラッグが終了すると、サイズが正常に戻って、拡大鏡は目に見えなくなっている.
    このように動作するUIにつながります.

    拡大鏡


    私たちが実装に飛び込む前に、2番目のことを考えましょう.私たちはイメージを取り、それをスケールアップし、マグの中心がどこにあるかにイメージを動かして、それから、それを作ろうとして、マグの内側だけが翻訳されたイメージを示します.
    翻訳もスケール要因の影響を受ける.視覚的にこれは次のように説明できます.

    最初に、私たちの拡大鏡を表す新しいウィジェットを作成しましょう
    class Magnifier extends StatefulWidget {
      const Magnifier({
        @required this.child,
        @required this.position,
        this.visible = true,
        this.scale = 1.5,
        this.size = const Size(160, 160)
      }) : assert(child != null);
    
      final Widget child;
      final Offset position;
      final bool visible;
      final double scale;
      final Size size;
    
      @override
      _MagnifierState createState() => _MagnifierState();
    }
    
    class _MagnifierState extends State<Magnifier> {
      Size _magnifierSize;
      double _scale;
      Matrix4 _matrix;
    
      @override
      void initState() {
        _magnifierSize = widget.size;
        _scale = widget.scale;
        _calculateMatrix();
    
        super.initState();
      }
    
      @override
      void didUpdateWidget(Magnifier oldWidget) {
        super.didUpdateWidget(oldWidget);
    
        _calculateMatrix();
      }
    
      @override
      Widget build(BuildContext context) {
        return Stack(
          children: [
            widget.child,
            if (widget.visible && widget.position != null)
              _getMagnifier(context)
          ],
        );
      }
    
      void _calculateMatrix() {
        if (widget.position == null) {
          return;
        }
    
        setState(() {
          double newX = widget.position.dx - (_magnifierSize.width / 2 / _scale);
          double newY = widget.position.dy - (_magnifierSize.height / 2 / _scale);
    
          final Matrix4 updatedMatrix = Matrix4.identity()
            ..scale(_scale, _scale)
            ..translate(-newX, -newY);
    
          _matrix = updatedMatrix;
        });
      }
    }
    
    コンストラクタの引数はいくつかあります.最も関連性が高いchild and position . この機能をすべてのウィジェットで動作させたいので、ウィジェットを提供する必要があります.下にあるウィジェットのどの部分を拡大するかを決定するために、我々はまたそれを必要とするposition パラメータvisible ユーザーがバブルをドラッグしたときにのみ表示されます.scale and size 自己説明する必要がありますし、デフォルト値があります.
    ここで重要なビジネスロジックは_calculateMatrix() メソッドは、タッチバブルの位置に基づいてスケーリングと翻訳を決定するため.我々は、アイデンティティのマトリックスを取り、それを更新するスケールと翻訳方法を使用します.重要:タッチバブルの中心をアンカーとして使う必要があります.そのため、高さと幅の半分を減算して位置を決定します.
    さて、今、実際の拡大鏡を描くの一部はまだ行方不明です.コール_getMagnifier() しかし、まだ実装がありません.
    Widget _getMagnifier(BuildContext context) {
      return Align(
        alignment: Alignment.topLeft,
        child: ClipOval(
          child: BackdropFilter(
            filter: ImageFilter.matrix(_matrix.storage),
            child: Container(
                width: _magnifierSize.width,
                height: _magnifierSize.height,
                decoration: BoxDecoration(
                  color: Colors.transparent,
                  shape: BoxShape.circle,
                  border: Border.all(color: Colors.blue, width: 2)
            ),
          ),
        ),
      );
    }
    
    これはいくつかの説明に値する.さて、ここで重要なウィジェットはBackdropFilter . このフィルタはフィルタと子供を取ります.指定されたフィルタを子に適用します.使用するフィルタはマトリックスである.これにより、計算された行列を子供に適用することができる.それを包むのは非常に重要ですClipOval 我々は唯一のサイズの円を見て、全体のスケールとは、元のイメージの翻訳重複をしたい.
    バブルを正確にどこに配置する可能性を改善するために、我々は拡大鏡に十字を追加することができます.これは、私たちが自己描画ウィジェットでコンテナを置き換える必要があります.
    import 'package:flutter/material.dart';
    
    class MagnifierPainter extends CustomPainter {
      const MagnifierPainter({
        @required this.color,
        this.strokeWidth = 5
      });
    
      final double strokeWidth;
      final Color color;
    
      @override
      void paint(Canvas canvas, Size size) {
        _drawCircle(canvas, size);
        _drawCrosshair(canvas, size);
      }
    
      void _drawCircle(Canvas canvas, Size size) {
        Paint paintObject = Paint()
          ..style = PaintingStyle.stroke
          ..strokeWidth = strokeWidth
          ..color = color;
    
        canvas.drawCircle(
          size.center(
            Offset(0, 0)
          ),
          size.longestSide / 2, paintObject
        );
      }
    
      void _drawCrosshair(Canvas canvas, Size size) {
        Paint crossPaint = Paint()
          ..strokeWidth = strokeWidth / 2
          ..color = color;
    
        double crossSize = size.longestSide * 0.04;
    
        canvas.drawLine(
          size.center(Offset(-crossSize, -crossSize)),
          size.center(Offset(crossSize, crossSize)),
          crossPaint
        );
    
        canvas.drawLine(
          size.center(Offset(crossSize, -crossSize)),
          size.center(Offset(-crossSize, crossSize)),
          crossPaint
        );
      }
    
      @override
      bool shouldRepaint(CustomPainter oldDelegate) {
        return true;
      }
    }
    
    私たちは、与えられたサイズの円を描くことから始めますが、ストロークモードでは輪郭を描くだけです.その後、中央を使用して拡大鏡サイズの4 %で左上と右下に移動してクロスを描画します.
    現在_getMagnifier() メソッドMagnifier クラスは以下のようになります:
      Widget _getMagnifier(BuildContext context) {
        return Align(
          alignment: Alignment.topLeft,
          child: ClipOval(
            child: BackdropFilter(
              filter: ImageFilter.matrix(_matrix.storage),
              child: CustomPaint(
                painter: MagnifierPainter(
                  color: Theme.of(context).accentColor
                ),
                size: _magnifierSize,
              ),
            ),
          ),
        );
      }
    
    我々が今の世話をしたいことは、拡大鏡が我々のタッチバブルの位置が拡大鏡と干渉するとき、右上にジャンプするということです.
      Alignment _getAlignment() {
        if (_bubbleCrossesMagnifier()) {
          return Alignment.topRight;
        }
    
        return Alignment.topLeft;
      }
    
      bool _bubbleCrossesMagnifier() => widget.position.dx < widget.size.width &&
        widget.position.dy < widget.size.height;
    
    代入する代わりにAlignment.topLeft 静的に拡大鏡には、使用するかどうかを決定するtopLeft or topRight その位置に基づきます.
    今私たちは余分なブロックを追加する必要がありますcalculateMatrix() メソッド:
    if (_bubbleCrossesMagnifier()) {
      final box = context.findRenderObject() as RenderBox;
      newX -= ((box.size.width - _magnifierSize.width) / _scale);
    }
    
    条件が満たされるときはいつでも、私たちはnewX MAG幅によって差し引かれる全体の幅によって、それは正確にアライメントが左から右へ変わるとき、拡大鏡が動く量です.

    クロージングコメント


    高価だが便利なウィジェットを使うBackdropFilter そして、行列計算の少しは、我々は位置を与えられたすべての子ウィジェットを拡大することができる拡大鏡を実装することができた.拡大鏡は左上に位置していますが、我々のタッチが拡大鏡の位置と衝突するとき、途中から飛び出します.
    アクションを見ることができますmy edge detection project . 拡大鏡は、動的な画像の周りに検出されたエッジを模倣タッチパネルに使用されます.