Pull up to Close を実装してみる、Xamarin.iOS で

イマドキのスマホアプリでは Pull to Refresh(引っ張って更新)を実装してるアプリをよく目にするのですが、RSS Reader の Feedly では Pull up to Close(上に引っぱって閉じる)も採用しています。

この操作性がなかなか使いやすかったので、自分でも実装してみました。

デモ

こんな感じ。 WebView なんですが、一番下までスクロールして、さらに上に引っ張ると "Pull up to Close" → "Release to Close" とラベルが変わり、そこで離すとコールバックします。

実装してみたコード

Xamarin.iOS ですから、C# です。

UIWebView でやってますが、ScrollView なコントロールならだいたい同じ感じでいけるんじゃないかと思います。

//PullUpToCloseSampleViewController.cs
public partial class PullUpToCloseSampleViewController : UIViewController
{
    public PullUpToCloseSampleViewController() : base ("PullUpToCloseSampleViewController", null)
    {
    }

    public override void ViewDidLoad()
    {
        base.ViewDidLoad();

        // WebView が持ってる ScrollView、よく使うので変数化しておく
        // webView は Interface Builder で UIWebView を Outlet にしたもの。
        var scrollView = webView.ScrollView;

        // Bounces の影を消す via http://stackoverflow.com/questions/8480571/removing-shadows-from-uiwebview
        scrollView.Subviews.Where(v => v is UIImageView)
            .ToList().ForEach(v => v.Hidden = true);

        // 上に引っ張った時に見える背景とラベル
        var bounceBackground = new UIView(
            new RectangleF(0f, 0f, webView.Frame.Width, webView.Frame.Height));
        bounceBackground.BackgroundColor = UIColor.LightGray;
        var bounceLabel = new UILabel(
            new RectangleF(0f, webView.Frame.Height - 30f, webView.Frame.Width, 30f));
        bounceLabel.Text = "Pull up to Close";
        bounceLabel.TextAlignment = UITextAlignment.Center;
        bounceLabel.BackgroundColor = UIColor.Clear;
        bounceLabel.Opaque = false;

        // 背景とラベルを WebView の一番奥に追加する
        webView.InsertSubview(bounceLabel, 0);
        webView.InsertSubview(bounceBackground, 0);

        // 適当な URL を読み込み
        webView.LoadRequest(NSUrlRequest.FromUrl(new NSUrl("http://yahoo.co.jp/")));
		
        // 閉じるのに必要な分だけ上に引っ張ったら true になる
        var canClose = false;

        // ドラッグ開始時にフラグOFF(一応)
        scrollView.DraggingStarted += (sender, e) => 
        {
            canClose = false;
        };

        // ドラッグ終了時、必要量引っ張っていたら OnCloseByPullUp を呼ぶ
        scrollView.DraggingEnded += (sender, e) => 
        {
            if (canClose)
            {
                OnCloseByPullUp();
            }
        };

        // スクロールした時にいろいろやる
        scrollView.Scrolled += (sender, e) => 
        {
            var labelFrame = bounceLabel.Frame;

            // コンテンツの一番下まで表示してさらに引っ張ったサイズ
            var offsetY = (scrollView.Frame.Height + scrollView.ContentOffset.Y) 
                - scrollView.ContentSize.Height;

            // 50px 上に引っ張ったら閉じるものとする
            canClose = offsetY > 50f;
            bounceLabel.Text = canClose ? "Release to Close" : "Pull up to Close";

            // ラベルがいつまでも移動しないように
            if (offsetY > labelFrame.Height)
            {
                offsetY = labelFrame.Height;
            }

            // ラベルがドラッグと共に下からせり出してくるように
            labelFrame.Y = scrollView.Frame.Height - offsetY;
            bounceLabel.Frame = labelFrame;
        };
    }
    
    // "Release to Close" で離すと呼ばれる
    void OnCloseByPullUp()
    {
        var v = new UIAlertView("", "Close this view",  null, "Close");
        v.Show ();
    }
}

やってる事

  1. ScrollView の「引っ張った時に見える場所(= Bounce というらしい)」の影を消す。 via http://stackoverflow.com/questions/8480571/removing-shadows-from-uiwebview
  2. 背景と、ラベルを WebView 内の一番奥に挿入する(引っ張った時にのみ見えるように)
  3. あとはイベントハンドラでの処理。スクロール中に、「最下部で引っ張り中」だったら "Pull up to Close" ラベルをアニメーションさせながら表示する。50px 以上引っ張ってたら "Release to Close" にラベルを変える。「閉じられるよ」フラグも ON にしとく。
  4. ドラッグ終了イベントで、「閉じられるよ」フラグが立ってたら、コールバックする。

今後

もうちょっとライブラリっぽくしたいですね。あと引っ張り中にアイコンとか表示させたい。