LoginSignup
1
2

More than 5 years have passed since last update.

Xamarin.FormsでPinchGestureRecognizerのユーザー操作について

Posted at

この記事は「Xamarin Advent Calendar 2015」の16日目の記事になっている予定です。
新しく実装されたPinchGestureRecognizerに関して知りたくなったので、調査のまとめを記事にさせて頂きましたー。
これで好きな画像をズームイン・アウトできるアプリ開発ができると信じて記事を書き始めています。

早速脱線話

自己紹介

Qiitaでは初投稿なので軽く自己紹介を。

  • 仕事ではモバイルのネイティブアプリの開発で主にiOSの方を担当しています。
  • 最近ではXamarinやXamarin.Formsを使ったアプリ案件がメインです。

おめでたいお話

Xamarin4.0、Xamarin.Forms2.0 アップデートおめでとうございます!
アプリの起動が早くなったと感じますし、PageやListViewなどのパフォーマンスがあがっているので有り難いことでした!
(しかし、XamarinStudioのバグはいつ直るのでしょうか・・・)

そういえばRelativeLayoutを避ける必要があるらしいですが、よくわかっていないです。

本題

そしてXamarin Forms 2 から新しいジェスチャークラスとしてPinchGestureRecognizerが追加されました。
ピンチっていうのは、デバイスでズームイン・アウトするときに2本指で行う摘むような操作のことです。
つまむようにすればズームアウトで、広げるようにすればズームインってのが定番でしょうか。

Xamarin.Formsでは今まで拡張しようとする場合TapGestureRecognizerを使うタップしかありませんでした。
アプリ実装するうえでは、スワイプやフリックがなくとも
ListViewContextActionsPullToRefreshCarouselPageでギャラリーっぽいページなどあるのでまあ何とかなります。

UIGestureRecognizerとは全然違います

まあここを読んでいる方は知っている方が多いと思いますが、iOSであるUIGestureRecognizerとは全然違います。

TapGestureRecognizer

まずは、TapGestureRecognizerについて、軽く説明。GestureRecognizer実装はそこまで難しくないですし、Build INSIDERの記事でXamarin.Formsでタッチイベントを処理するには?(iOS/Androidの各種ジェスチャー対応)のわかりやすい解説を参考にしましょう。
ちなみに今回のサンプルが下です。

sampleTapEvent.cs
            var image = new Image
            {
                VerticalOptions = LayoutOptions.CenterAndExpand,
                HorizontalOptions = LayoutOptions.CenterAndExpand,
                Source = "yNormal" // 好きな画像のファイル名に
            };

            var gr = new TapGestureRecognizer();
            gr.Tapped += (sender, e) =>
            {
                System.Diagnostics.Debug.WriteLine("画像がダブルタップされました");
            };
            gr.NumberOfTapsRequired = 2; // AndroidでのNumberOfTapsRequiredのバグはもう直っています。
            image.GestureRecognizers.Add(gr);

それとViewModelを使いICommandを使うならば、

sampleTapCommand.cs
            var gr = new TapGestureRecognizer();
            gr.Command = _vm.ImageTapCommand; // var _vm = BindingContext as ViewModel
            gr.NumberOfTapsRequired = 2;
            image.GestureRecognizers.Add(gr);

とか良いのではないでしょうか。

PinchGestureRecognizer

新しく使えるようになったPinchです。
まずは、Xamarinが上げている説明を読まず実装します。(おい

何も見ずにチャレンジ

コーディング
同じGestureRecognizerだから実装も似た感じと思い、とりあえず書いたのが下のコードです。

samplePinchEvent.cs
            var image = new Image
            {
                VerticalOptions = LayoutOptions.CenterAndExpand,
                HorizontalOptions = LayoutOptions.CenterAndExpand,
                Source = "yNormal"
            };

            var gr = new PinchGestureRecognizer();
            gr.PinchUpdated += (sender, e) => {
                System.Diagnostics.Debug.WriteLine("画像がピンチされました");
            };
            image.GestureRecognizers.Add(gr);

ここまではTapのコードと同じに実装しました。
Android側、iOS側のプロジェクトではなにも弄っていません。
結果
iOSでは動かず、Androidではアプリが落ちます。
理由
はっきりわからないのですが。。。PinchはTapとは違い IPinchGestureController を継承しています。
PinchGestureRecognizer.png
(WebでのAPIsでは GestureRecognizer だけの表記だったんだけど何でだろ)
このあたりが絡んでるのかと予測しますが、詳しい方がいれば教えて下さい。
まあ悩んでも仕方ないので サンプルコードにあったとおりにContextViewをかませます。

サンプル見ながらチャレンジ

samplePinchEvent.cs
            var image = new Image
            {
                VerticalOptions = LayoutOptions.CenterAndExpand,
                HorizontalOptions = LayoutOptions.CenterAndExpand,
                Source = "yNormal"
            };

            var gr = new PinchGestureRecognizer();
            gr.PinchUpdated += (sender, e) => {
                System.Diagnostics.Debug.WriteLine("画像がピンチされました");
            };
            var cv = new ContentView{ Content = image };           
            cv.GestureRecognizers.Add(gr);

結果
シミュレータではiOSでもAndroidでも動きました!
ContentViewでなくてもFrameStackLayoutでも大丈夫でした。

PinchGestureUpdatedEventArgs

では、PinchUpdatedの拡張をします。
引数のeventArgsは、PinchGestureUpdatedEventArgsであり、
属性の double型のScale、Point型のScaleOrigin、EnumであるGestureStatus型のStatusからユーザーがどんな操作をしているかをキャッチします。

スクリーンショット 2015-12-15 15.19.58.png

Scaleは指の間の広さが狭くなると小さく、広くなると大きくなります。Statusはこれ以外にCancelがあります。
(break pointで急な操作をしているのでScaleの値が変に!本来なら0.98やら1.01とかです。)

実行中1、実行中2というのは指を動かすたびに呼ばれます。Scaleが1の時は指の間隔を変えずに指を動かしているときです。
Originは2本指の中心点だと思っていますが、あまり良くわかっていません。。。

取れる情報はこれぐらいです。以上です!

サンプルのズームインとズームアウトを実装

この辺りもコピペです。(iOSのUIGestureRecognizerのズーム実装とは違ってすごくややこしい。。。)

アルゴリズムはちゃんと読めば何しているかわかりますね。
始まった値を保存して、Scale係数からXとYの相対座標を計算、画像の座標を計算、サイズ変更を行っています。
自分の理解だとこんなアルゴリズムです。

貼り付けて修正した結果こんなコードになりました。

SamplePinchPage.cs
    public class SamplePinchPage : ContentPage
    {
        double currentScale = 1;
        double startScale = 1;
        double xOffset = 0;
        double yOffset = 0;

        public SamplePinchPage()
        {
            var image = new Image
            {
                VerticalOptions = LayoutOptions.CenterAndExpand,
                HorizontalOptions = LayoutOptions.CenterAndExpand,
                Source = "yNormal"
            };

            var pinchGesture = new PinchGestureRecognizer();
            pinchGesture.PinchUpdated += (sender, e) =>
            {
                if (e.Status == GestureStatus.Started)
                {
                    startScale = Content.Scale;
                    Content.AnchorX = 0;
                    Content.AnchorY = 0;
                }
                if (e.Status == GestureStatus.Running)
                {
                    System.Diagnostics.Debug.WriteLine(string.Format("scale:{0} x:{1}", e.Scale, e.ScaleOrigin.X));
                    System.Diagnostics.Debug.WriteLine(string.Format("scale:{0} y:{1}", e.Scale, e.ScaleOrigin.Y));

                    currentScale += (e.Scale - 1) * startScale;
                    currentScale = Math.Max(1, currentScale);

                    var renderedX = Content.X + xOffset;
                    var deltaX = renderedX / Width;
                    var deltaWidth = Width / (Content.Width * startScale);
                    var originX = (e.ScaleOrigin.X - deltaX) * deltaWidth;

                    var renderedY = Content.Y + yOffset;
                    var deltaY = renderedY / Height;
                    var deltaHeight = Height / (Content.Height * startScale);
                    var originY = (e.ScaleOrigin.Y - deltaY) * deltaHeight;

                    var targetX = xOffset - (originX * Content.Width) * (currentScale - startScale);
                    var targetY = yOffset - (originY * Content.Height) * (currentScale - startScale);

                    Content.TranslationX = targetX.Clamp(-Content.Width * (currentScale - 1), 0);
                    Content.TranslationY = targetY.Clamp(-Content.Height * (currentScale - 1), 0);

                    Content.Scale = currentScale;
                }
                if (e.Status == GestureStatus.Completed)
                {
                    xOffset = Content.TranslationX;
                    yOffset = Content.TranslationY;
                }
            };
            var content = new ContentView{ Children = { image } };           
            content.GestureRecognizers.Add(pinchGesture);

            Content = new StackLayout
            { 
                Children =
                {
                    content
                }
            };
        }
    }

    public static class DoubleExtensions
    {
        public static double Clamp(this double self, double min, double max)
        {
            return Math.Min(max, Math.Max(self, min));
        }
    }

これで好きなようにズームイン、ズームアウト出来る!Xamarin.Formsだけで画像アプリができますよ!!
終わり

そんな訳ありませんでした。

なんで

ズームイン、ズームアウト出来るアプリにはなっています。
ですが、指定した場所をズームイン・アウトが出来ても、ドラッグで画像の移動が出来るわけではありません。(そしてデバイスの隅などは指が広げられないのでズームイン出来ない)
使いにくくなっています。

じゃあどうすればいいの?

解決策としては、十字のボタンをつくって押した方に画像を動かすぐらいが限界かなと思います。

なのでドラッグが使いたかったらぜひ、CustomRendererを使いましょう!

 まとめ

と、この記事の意味があるのかないのかよくわからない結論しか出せませんでした。
せっかくのXamarin Advent Calendarに、そしてQiitaへの最初の投稿でこの内容で大変申し訳無いです。。。

言い訳をさせていただくと、記事を書き始めるときは本当に出来ると思っていました。

そして、Pinchの機能としては問題はなかったです。例えばピンチして値の大きさや図形の大きさを調整するようなアプリなら使えるとは思います。ですがやっぱりドラッグは欲しいですよね。。。

以上で終わりになります。良ければ、次の記事(?)をお楽しみに。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2