Xamarin.Forms で Activity Transitions もどきを作ってみた

Android L から Activity Transitions という概念が導入されました.

単純なスライドやフェードではない、よりコンテキストを反映したリッチな画面遷移を実現できます.

@konifarさんのMaterial Catに Activity Transitions の実例がありますので、ぜひインストールしてご覧ください.

Xamarin.Forms には Activity Transitions のような機能は API レベルでは提供されていません.私が探した限りではサードパーティー製のライブラリも見つかりませんでした.

そこで今回は Xamarin.Forms で Activity Transitions もどきの表現を実装してみます.ページ間で View を共有する Shared Elements のパターンを実装します.

実装結果がしょっぱい感じになっていますがご容赦ください.

絶対座標を取得する Effect

画面遷移の起点となる View のスクリーン上での絶対座標を取得する必要があリます.

しかしながら Xamarin.Forms.ViewElement.Bounds で取得できるものは親 View との相対座標なので不十分です.

そこでスクリーン上の絶対座標を取得する Effect を作成します.

using System;
using Xamarin.Forms;

namespace ActivityTransitionSample
{
	public static class AbsoluteBoundsEffect
	{
		public static readonly BindableProperty AbsoluteBoundsProperty =
			BindableProperty.CreateAttached("AbsoluteBounds", typeof(Rectangle), typeof(AbsoluteBoundsEffect), new Rectangle(0, 0, 0, 0));

		public static Rectangle GetAbsoluteBounds(BindableObject view)
		{
			return (Rectangle)view.GetValue(AbsoluteBoundsProperty);
		}

		public static void SetAbsoluteBounds(BindableObject view, Rectangle value)
		{
			view.SetValue(AbsoluteBoundsProperty, value);
		}
	}

	class ViewAbsoluteBoundsEffect : RoutingEffect
	{
		public ViewAbsoluteBoundsEffect() : base("Santea.ViewAbsoluteBoundsEffect")
		{
			
		}
	}
}

PCLプロジェクトに AbsoluteBoundsEffect を作ります.

using System;
using ActivityTransitionSample.Droid;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

[assembly: ResolutionGroupName("Santea")]
[assembly: ExportEffect(typeof(ViewAbsoluteBoundsEffect), "ViewAbsoluteBoundsEffect")]
namespace ActivityTransitionSample.Droid
{
	public class ViewAbsoluteBoundsEffect : PlatformEffect
	{
		protected override void OnAttached()
		{
			try
			{
				Control.LayoutChange += (sender, e) =>
				{
					UpdateAbsoluteBounds();
				};
				UpdateAbsoluteBounds();
			}
			catch (Exception ex)
			{
				Console.WriteLine("Cannot set property on attached control. Error: ", ex.Message);
			}
		}

		protected override void OnDetached()
		{
			
		}

		private void UpdateAbsoluteBounds()
		{
			int[] position = new int[2];
			Control.GetLocationInWindow(position);
			AbsoluteBoundsEffect.SetAbsoluteBounds(Element,
                            new Rectangle(position[0], position[1], Control.Width, Control.Height));
		}
	}
}

レイアウトの変更に合わせてプロパティを変更するために LayoutChange にイベントを付与しています.

絶対座標は Android.Views.View.GetLocationInWindow で取得しています.

遷移元ページ

まずは遷移元のページを作ります.

今回はGrid状の View として FlowListView を用いました.

2016年12月22日現在、iOS 10.1 では正常に動作しないようです.

GridPage.xaml

GridPage.xaml.cs

using System;
using System.Collections.Generic;
using System.Windows.Input;
using Xamarin.Forms;

namespace ActivityTransitionSample
{
	public partial class GridPage : ContentPage
	{
		public IList ImageModels { get; set; }

	        public GridPage()
		{
			InitializeComponent();
			ImageModels = ImageModel.GenerateList();
			BindingContext = this;
		}

		public void OnImageClicked(object sender, EventArgs e)
		{
			var image = sender as Image;
			Navigation.PushAsync(new DetailPage(image));
		}
	}
}

Grid 中の Image をクリックしたとき、Image の参照を遷移先の Page に渡します.

遷移先ページ

次に遷移先ページです.

DetailPage.xaml

アニメーションの対象は Image と StackLayout になります.それぞれフェードアニメーション用の Opacity=”0.3″ の初期値を与えています.

DetailPage.xaml.cs

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace ActivityTransitionSample
{
	public partial class DetailPage : ContentPage
	{
		private Image _image;

	        public DetailPage(Image image)
		{
			InitializeComponent();
			_image = image;
			Image.Source = _image.Source;
		}

		protected async override void OnAppearing()
		{
			base.OnAppearing();

			await Task.WhenAll(
  				Image.LayoutTo((Rectangle)_image.GetValue(AbsoluteBoundsEffect.AbsoluteBoundsProperty), 0),
				StackLayout.LayoutTo(new Rectangle(0, Height, 0, 0), 0)
			);
			Image.FadeTo(1.0, 250);
			Image.LayoutTo(new Rectangle(0, 0, Width, 290), 250);
			StackLayout.FadeTo(1.0, 300);
			StackLayout.LayoutTo(new Rectangle(0, 290, Width, Height - 290), 300);
		}
	}
}

ここからが本エントリーの中心です.Page.OnAppearing にアニメーションを実装します.

await Task.WhenAll(
	Image.LayoutTo((Rectangle)_image.GetValue(AbsoluteBoundsEffect.AbsoluteBoundsProperty), 0),
	StackLayout.LayoutTo(new Rectangle(0, Height, 0, 0), 0)
);

まず、アニメーション前の View の座標を指定します.Image には遷移元ページの Image の絶対座標を指定します.StackLayout にはページの画面下端の座標を指定します.

Image.FadeTo(1.0, 250);
Image.LayoutTo(new Rectangle(0, 0, Width, 290), 250);
StackLayout.FadeTo(1.0, 300);
StackLayout.LayoutTo(new Rectangle(0, 290, Width, Height - 290), 300);

フェードとレイアウトのアニメーションを非同期に実行します.StackLayout を Image より50msだけ遅くすることで、それぞれのアニメーションが際立つようにしています.

実装結果

画面遷移前のページの Image を起点にして 画面遷移後のページの Image がアニメーションしている様子がご覧いただけると思います.

以上です.

参考