Flutter: Transform.rotateで回転したウィジェットを、正しい方向にドラッグさせる方法

Flutter

Transform.rotate回転したウィジェットをドラッグしたら、全然違う方向に動いてしまう。

なぜ?どうしたら正しい方向に動かせるの?

という方向けの記事です。

 

「GestureDetector」でドラッグ可能にしていたウィジェットを回転させたら、この状況に陥りました。

 

ネット上で調べても情報が見つからず、解決に苦労したので、その経験を共有したいと思います。

 

結論は、以下のとおりです。

 

ドラッグした横・縦の移動量(移動方向)を、ウィジェットを回転させた角度分だけ補正する(高校数学で習う「1次変換」の式を使用)。

 

以下、詳細になります。

 


 

40代からプログラミング(Flutter)を始めて、GooglePlayAppStoreにアプリを公開しているhalzo appdevです。

 

作成したアプリはこちら↓ 全てFlutterで開発したアプリです。

 

「BasicMemo」については、Macのデスクトップ版もリリースしました。

 

超即ToDo –最短2タップで通知登録できるタスク管理アプリ

Google Play で手に入れよう
Download on the App Store

 

かんたんプリント管理:アラート・OCR文字認識・検索機能を搭載

Google Play で手に入れよう
Download on the App Store

 

シンプルメモ帳「BasicMemo」 – 文字カウント、ワンタッチ入力、タグ管理等の機能を搭載
New! Macデスクトップアプリ版もリリースしました。

Google Play で手に入れよう
Download on the App Store

 

Transform.rotateで回転させからドラッグするとどうなるか?

「GestureDetector」「onPanUpdate:」プロパティでドラッグ可能にしておいた「Container」を、「Transform.rotate」でラップして45度回転させました。

 

回転の中心は、「origin:」プロパティを使い、「Container」の左上端になるよう設定しました(ウィジェット中心からの相対位置で設定するため、「Container」の横幅の1/2、縦幅の1/2をマイナス値で設定)。

 

結果、ドラッグすると下記動画のような状況になりました。

 

ドラッグの方向とウィジェットが動く方向が完全にズレている。。

 

右にドラッグしたのに、右上の方に動き上にドラッグしたのに左斜め上の方に動いてしまいます。。

 

この状態の「Transform.rotate」部分のコードは以下のとおりです。

 

// pi を使用するのに必要
import 'dart:math';

import 'package:flutter/material.dart';

・・・(略)・・・

// ウィジェットの初期の配置位置
Offset offsetO = Offset(100, 100);

// 回転角度(45度)
double angleO = pi / 4;

・・・(略)・・・

body: Transform.translate(

        // ウィジェットの配置位置(オフセット)を設定
        offset: offsetO,

        child: Transform.rotate(
          // 45度右回りに回転
          angle: angleO,

          // Containerの左上端を軸に回転
          origin: Offset(- 100 / 2, - 50 / 2),

          child: GestureDetector(
            onPanUpdate: (DragUpdateDetails detailsO) {

              // 配置位置に横方向と縦方向の移動量を加算
              double dxO = offsetO.dx + detailsO.delta.dx;
              double dyO = offsetO.dy + detailsO.delta.dy;

              // 配置位置を更新
              offsetO = Offset(dxO, dyO);

              // 移動した位置で再描画
              setState(() {});
            },
            child: Container(
              width: 100,
              height: 50,
              color: Colors.green,
            ),
          ),
        ),
      ),

 

なぜズレた方向に動いてしまうのか?

上記コード内の変数

 

  • detailsO.delta.dx(横方向の移動量)
  • detailsO.delta.dy(縦方向の移動量)

 

をprint関数でコンソールに表示させ、数値を確認すると、下図のように、ちょうど「Container」を回転させた角度分だけズレた方向に移動量が発生していることがわかりました。

 

 

これは、「GestureDetector」クラスを「Transform.rotate」クラスでラップしているため、移動方向自体も「Transform.rotate」により回転させられてしまうためと思われます。

 

ただ、直感的には、右下方向に移動するのであれば、移動方向も右回りに回転角度分だけズレたことになるので、理解しやすいのですが、なぜか逆方向(左回り)にズレるようです。。

 

この点は、後述する「Transform」が、変形前(回転前)のウィジェット領域を保持している点と関係しているようです。

 

右へのドラッグは、回転後のウィジェットから見ると、右上方向へのドラッグになるので、それを回転前のウィジェット領域に対して設定すると、画面上で右上方向への移動になる、ということだと推察します(違っているかもしれませんが、、、)。

 

回転後のウィジェットにGestureDetectorをつけたらどうなるか?

考えられる対策として、移動方向が回転の影響を受けないよう、回転後の状態に対して、ドラッグ機能を設定すれば良さそうです。

 

具体的には、「Container」を先に「Transform.rotate」クラスでラップし、それを「GestureDetector」クラスでラップする順番に変更する、という方法です。

 

これでやると、確かにドラッグした方向に移動できるようになります。

 

しかし、この方法だと、下記動画のように、回転後のウィジェット(この例では緑の「Container」)内の一部分しかドラッグに反応しない、という問題が生じます。

 

思った方向に動くものの、ウィジェットの左側や右下のゾーンはドラッグに反応しない。。

 

右上部分はドラッグのために掴むことができますが、左側や右下のゾーンは掴むことができず、ドラッグに反応しません。

 

理由は、「Transform」クラスの公式説明ページにある、下記内容が影響しています。

 

 

このオブジェクトは描画の直前に変形を適用します。つまり、このウィジェットの子ウィジェット(つまりこのウィジェット)が消費する空間の大きさを計算するときに、変形は考慮されないということです。

https://api.flutter.dev/flutter/widgets/Transform-class.html

 

つまり、「Transform.rotate」で回転しても、ウィジェットの領域はあくまで回転前の状態なので、下図のように、回転後と回転前の領域の重複した部分でないと「GestureDetector」が反応しない、ということです。

 

 

回転軸をウィジェットの中心にし、わずかに回転させる程度であれば、この方法でも大きな問題は生じないですが、今回のように回転軸をずらし、かつ大きく回転させる場合は、不都合が大きいです。

 

三角関数を使って、正しい移動量(移動方向)に修正する(1次変換)

今回、たどり着いた結論になります。

 

高校の数学で習う「1次変換」の考え方を使い、右上に移動してしまう移動量を、右に移動する移動量に変換します。

 

イメージは以下のとおりです。

 

 

一見難しそうですが、単純な公式がありますので、移動量を下記公式に従って再計算するだけになります。

 

  • 補正後方向の移動量
    =補正前の方向移動量 × cos(回転角度) 補正前の方向移動量 × sin(回転角度)
  • 補正後方向の移動量
    =補正前の方向移動量 × sin(回転角度) + 補正前の方向移動量 × cos(回転角度)

 

※移動量の符号は、横方向は右向きがプラス・左向きがマイナス、縦方向は下向きがプラス・上向きがマイナスになります。

 

※回転角度の符号は、右回りがプラス・左回りがマイナスになります。

 

※sinやcosの三角関数を使うには、「math」をインポートする必要がありますが、標準でDartに入っているライブラリなので、Android Studioの場合は、sinやcosを書いたときに出てくる赤い波線のところで、「option+Enter」を押して、項目を選択すれば、簡単にインポートできます(「pi」を使っていれば、既にインポート済のはずです)。

 

なお、1次変換については、下記サイトのご説明が大変わかりやすかったです(ありがとうございます!)。

 

 

上記でご紹介した公式のわかりやすい証明も書かれています。

 

スマホの画面上は、数学上の座標とは、Y軸の正負、回転角度の正負の方向が逆になりますが、全く同じ公式を適用できます。

 

結果、この方法で移動量(移動方向)を修正すると、下記動画のように、違和感のないドラッグができるようになります。

 

思ったとおりの方向にドラッグできるようになった

 

サンプルコード

ご参考に、今回作成したサンプルコードを以下に掲載します。

 

コピペすれば、そのまま挙動を確認いただけます。

 

移動量の修正部分のイメージは以下のとおりです。

 

 

注意点としては、補正するのはオフセット値(ウィジェットのX、Y座標)ではなく、あくまで移動量(X、Yの変化量)である、という点です。

 

間違って、オフセット値を補正してしまうと、ドラッグした途端、とんでもない方向に動いてしまいますので、ご注意ください(自分はこれでハマりました。。。)。

 

// クラス名、メソッド名、プロパティ名(変数名)について、筆者が作成したもの(名前変更可のもの)
// の名前の末尾には、大文字のオー「O」をつけています
// ※ライブラリ(パッケージ)で予め決められているもの(名前の変更不可のもの)と、
//  自分で作成したもの(名前の変更可のもの)の区別をしやすくするため

import 'dart:math';

import 'package:flutter/material.dart';

void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Test",
      theme: ThemeData.light(),
      home: SampleScreenO(),
    );
  }
}
class SampleScreenO extends StatefulWidget {
  @override
  _SampleScreenOState createState() => _SampleScreenOState();
}
class _SampleScreenOState extends State<SampleScreenO> {

  Offset offsetO = Offset(100, 100);
  double angleO = pi / 4;

  @override
  Widget build(BuildContext context) {
    
    return Scaffold(
      appBar: AppBar(),
      body: Transform.translate(
        offset: offsetO,
        child: Transform.rotate(
          angle: angleO,
          origin: Offset(- 100 / 2, - 50 / 2),
          child: GestureDetector(
            onPanUpdate: (DragUpdateDetails detailsO) {

              // 一次変換の公式で、横方向・縦方向の移動量を修正
              double convertedDeltaDxO = detailsO.delta.dx * cos(angleO) - detailsO.delta.dy * sin(angleO);
              double convertedDeltaDyO = detailsO.delta.dx * sin(angleO) + detailsO.delta.dy * cos(angleO);

              // 修正後の移動量をオフセット値(表示位置)に加算
              double dxO = offsetO.dx + convertedDeltaDxO;
              double dyO = offsetO.dy + convertedDeltaDyO;
              offsetO = Offset(dxO, dyO);
              
              setState(() {});
            },
            child: Container(
              width: 100,
              height: 50,
              color: Colors.green,
            ),
          ),
        ),
      ),
    );
  }
}

 

補足:「RotatedBox」を使う方法

ウィジェットの回転に、「Transform.rotate」ではなく、「RotatedBox」を用いるという方法もあります。

 

「RotatedBox」は、「Transform.rotate」と異なり、回転後の領域がウィジェットの領域になります

 

 

Transform とは異なり、このオブジェクトはレイアウトの前に回転を適用します。つまり、回転したボックス全体が、回転した子に必要なだけのスペースしか消費しません。

https://api.flutter.dev/flutter/widgets/RotatedBox-class.html

 

そのため、「Container」を先に「RotatedBox」クラスでラップし、それを「GestureDetector」クラスでラップする順番にすれば、回転後のウィジェット内で掴めない部分は発生せず、違和感のないドラッグが可能になります。

 

しかし、「RotatedBox」は90度単位での回転しかできないため、任意の角度で回転させたい場合には不都合になります。

 

そのため、回転角度が90度単位で問題ない場合に限っては、「RotatedBox」を用いた方が、コードがシンプルになると思います。

 


以上、ご参考になれば幸いです。

 

最後までお読みいただき、ありがとうございました。

 

\ Flutterの学習で役立ったコンテンツ・書籍 /

 

 

 


Dart入門 – Dartの要点をつかむためのクイックツアー

コメント

タイトルとURLをコピーしました