読者です 読者をやめる 読者になる 読者になる

ぴよぴよエンジニアの日記

クラウドベンダーに勤める見習いSEの日記です。発言は私自身の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

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

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

developer.android.com

単純なスライドやフェードではない、よりコンテキストを反映したリッチな画面遷移を実現できます.
@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 で取得しています.



遷移元ページ

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

github.com

今回はGrid状の View として FlowListView を用いました.
2016年12月22日現在、iOS 10.1 では正常に動作しないようです.


GridPage.xaml

<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:controls="clr-namespace:DLToolkit.Forms.Controls;assembly=DLToolkit.Forms.Controls.FlowListView" x:Class="ActivityTransitionSample.GridPage" Title="GridPage">
	<ContentPage.Content>
		<controls:FlowListView VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand" FlowColumnCount="2" SeparatorVisibility="None" HasUnevenRows="true" FlowLastTappedItem="{Binding TappedImageModel}" FlowItemsSource="{Binding ImageModels}">
			<controls:FlowListView.FlowColumnTemplate>
				<DataTemplate>
					<Image x:Name="ImageView" HeightRequest="{Binding Source={x:Reference ImageView}, Path=Width}" Aspect="AspectFill" Source="{Binding ImageUrl}" Margin="3">
						<Image.GestureRecognizers>
        					        <TapGestureRecognizer Tapped="OnImageClicked"/>
    					        </Image.GestureRecognizers>
						<Image.Effects>
        					        <Effect x:FactoryMethod="Resolve" >
            					                <x:Arguments>
                					                <x:String>Santea.ViewAbsoluteBoundsEffect</x:String>
            					                </x:Arguments>
        					        </Effect>
    					        </Image.Effects>
					</Image>
				</DataTemplate>
			</controls:FlowListView.FlowColumnTemplate>
		</controls:FlowListView>
	</ContentPage.Content>
</ContentPage>


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<ImageModel> 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

<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="ActivityTransitionSample.DetailPage" Title="DetailPage">
	<ContentPage.Content>
		<StackLayout Orientation="Vertical" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
			<Image x:Name="Image" Aspect="AspectFill" HorizontalOptions="FillAndExpand" VerticalOptions="Start" Opacity="0.3"/>
			<StackLayout x:Name="StackLayout" Opacity="0.3">
				<Label Text="Re:ゼロから始める異世界生活"/>
				<Label Text="原作:長月達平"/>
				<Label Text="キャラクター原案:大塚真一郎"/>
				<Label Text="監督:渡邊政治"/>
			</StackLayout>
		</StackLayout>
	</ContentPage.Content>
</ContentPage>

アニメーションの対象は 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だけ遅くすることで、それぞれのアニメーションが際立つようにしています.



実装結果

f:id:Santea:20161222165251g:plain:w360

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



以上です.




参考

Xamarin.Forms × IBM MobileFirst Foundation プロジェクトの作成

f:id:Santea:20161203154823j:plain

本エントリーは連載エントリー Xamarin.Forms × IBM MobileFirst Foundation の一部となっております.


本エントリーでは IBM MobileFirst Foundation プロジェクトの作成を行います.



プロジェクトの作成

f:id:Santea:20161203222541j:plain

Bluemix のログイン後の画面で、右上の"カタログ"を選択します.



f:id:Santea:20161203222553j:plain:w360

左側にカタログ一覧が表示されるので、その中の"モバイル"を選択します.



f:id:Santea:20161203223040j:plain

バイルアプリケーション一覧が表示されるので、"Mobile Foundation" を選択しましょう.



f:id:Santea:20161203225301j:plain

Mobile Foundationサービスを作成します. 今回は、Mobile Foundation-Sample という名前にしました.
価格プランはデフォルトの開発者のままで作成します.



f:id:Santea:20161203224101j:plain

サービス作成が成功したら、基本サーバを起動を行います.



f:id:Santea:20161203025654p:plain

サーバの起動には少し時間が掛かります(私の環境では3分程度掛かりました).



f:id:Santea:20161203224652j:plain

サーバが起動するとこのような画面が表示されていると思います.
次にコンソールの起動を行います. 目のマークをクリックするとパスワードを表示できるのでコピーしましょう.



f:id:Santea:20161203224913j:plain

ユーザ名: admin, パスワード: 先程コピーしたパスワードでログインしてください.



f:id:Santea:20161203225050j:plain:w360

ログイン後の画面で、アプリケーションの"新規"を選択してください.



f:id:Santea:20161203230209j:plain

Android 用のアプリケーションを作成する場合は Android を選択します.

パッケージ名は Android のパッケージ名と一致させる必要があります.


f:id:Santea:20161211233739j:plain

iOS 用のアプリケーションを作成する場合は iOS を選択します.

バンドルID は iOS のバンドルIDと一致させる必要があります.




以上がプロジェクトの作成になります.