【Xamarin.Forms】アプリの背景を好き放題にデコる

## アプリの動作にマッチした、いい感じの背景エフェクトを付加したい
アプリがどのような動作をしているかを視覚的に分かりやすくするために、背景に何かしらのエフェクトを付加したいことがあります。
例えば、[ビーコン発信アプリ](http://qiita.com/microwavePC/items/689fd81affe641c60449)で発信中にいかにも『発信中です』って感じのエフェクトを発生させたい、等々…。
そんなとき、**WebViewをアプリの背景として使う**と、世の中に出回っているJavaScriptのエフェクトを利用できたり、エフェクト発生部分のソースを全プラットフォームで共通化できたりして便利です。

#### エフェクトを付加した例

以下のアニメーションは、ビーコン発信中っぽいエフェクトを追加してみた例です（[参考](http://qiita.com/nekoneko-wanwan/items/c9f26ce049bd422e555c)）。
~~実機で動かすともっと綺麗にヌルヌル動くんですけどね。。。~~

|iOS|Android|
|:-:|:-:|
|![iOS_Transmission.gif](https://qiita-image-store.s3.amazonaws.com/0/140394/e62b33b7-d4b5-8041-593e-49a186ceecf7.gif)|![Android_Transmission.gif](https://qiita-image-store.s3.amazonaws.com/0/140394/502a326e-4163-f6ea-2345-164f3f402416.gif)|

JavaScriptやCSSの扱いに慣れている人なら、この他にも色々なエフェクトを載せ放題です。

#### サンプルアプリ

以下にUPしたアプリを例にして、この後の説明を記載しています。
https://github.com/microwavePC/WebViewBackgroundSample
Prism for Xamarin.Forms (Prism.Forms) で作成しています。

サンプルアプリには、以下の背景エフェクトを実装しています。

* マトリックス風エフェクト（[参考](http://gigazine.net/news/20130321-matrix-javascript/)）
* ビーコン発信中っぽいエフェクト
* ブランク（エフェクトなし）

マトリックス風エフェクトは、以下のような感じです。

|iOS|Android|
|:-:|:-:|
|![iOS_Matrix.gif](https://qiita-image-store.s3.amazonaws.com/0/140394/75d89ab1-2912-31d9-7dc2-984b2772320e.gif)|![Android_Matrix.gif](https://qiita-image-store.s3.amazonaws.com/0/140394/d9141826-b8f1-8c8c-c290-74a98d314886.gif)|

#### 実装手順

背景実装の手順はざっくり以下の通りです。
　1. WebViewを継承した、背景用のカスタムコントロールを作成する(※)。
　2. カスタムコントロールが背景になるよう、Xamlを修正する。
　3. 背景にするWeb系ファイル（HTML、CSS、JavaScript）を用意し、共通プロジェクトに追加する。
　4. プラットフォームごとのプロジェクトに、Web系ファイルへのリンクを作成する。
　5. ローカルのHTMLファイルを実際に読み込む。
　　※WebViewの背景を透明化するために必要

## 1. WebViewを継承した、背景用カスタムコントロールの作成

Xamarin.Forms標準のWebViewは背景を透明にすることができません。マトリックス風エフェクトのように背景を塗りつぶすなら問題ありませんが、そうでないエフェクト用にWebViewをそのまま扱ってしまうと、背景が真っ白になってしまいます。
WebViewには『BackgroundColor』というプロパティがありますが、値を"Transparent"にしても効きません（Xamarin.Forms 2.3.4.231で確認）。その他の色を指定しても適用されません（バグ？）。
また、『Opacity』というプロパティがありますが、この値をいじるとWebViewそのものがエフェクトごとまるまる透明化してしまいます。

iOSなら真っ白の背景で特に問題ないとしても、Androidで背景が白塗りになると大分違和感が出てしまいます。
ということで、**背景を透明にしたWebView**を自作してこの点を解決します。

#### 1.1. 共通プロジェクトにCustomWebView.xamlを作成する。

今後さらに機能を追加していくかもしれないので、このような名前です。
具体的には、以下の機能（というか性質）を追加しています。

* 背景の透過
* iOSで、画面上部の電池残量やらの部分とWebViewが被らないような調整

```xml:CustomWebView.xaml
<?xml version="1.0" encoding="UTF-8"?>
<WebView xmlns="http://xamarin.com/schemas/2014/forms"
		 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
		 x:Class="WebViewBackgroundSample.CustomWebView">
	<WebView.Margin>
		<!-- iOSだけ、上に20pxのMarginを設定する -->
		<OnPlatform x:TypeArguments="Thickness"
					iOS="0,20,0,0"
					Android="0,0,0,0" 
					WinPhone="0,0,0,0" />
	</WebView.Margin>
</WebView>
```

```csharp:CustomWebView.xaml.cs
using Xamarin.Forms;
namespace WebViewBackgroundSample
{
// WebViewを継承しているだけ
	public partial class CustomWebView : WebView
	{
		public CustomWebView()
		{
			InitializeComponent();
		}
	}
}
```

#### 1.2. 各プラットフォームのプロジェクトにCustomWebViewのRendererを作成する。

背景の透過はこの中に実装します。
[2017/04/13追記] iOS側の透過に必要な1行（Opaque = false;）が抜けていたので追加しました。

```csharp:CustomWebViewRenderer.cs（Android側）
using WebViewBackgroundSample;
using WebViewBackgroundSample.Droid;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

[assembly: ExportRenderer(typeof(CustomWebView), typeof(CustomWebViewRenderer))]
namespace WebViewBackgroundSample.Droid
{
	public class CustomWebViewRenderer : WebViewRenderer
	{
		protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
		{
			base.OnElementChanged(e);
			
			// 背景を透過
			this.Control.SetBackgroundColor(Android.Graphics.Color.Transparent);
		}
	}
}
```

```csharp:CustomWebViewRenderer.cs（iOS側）
using UIKit;
using WebViewBackgroundSample;
using WebViewBackgroundSample.iOS;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly: ExportRenderer(typeof(CustomWebView), typeof(CustomWebViewRenderer))]
namespace WebViewBackgroundSample.iOS
{
	public class CustomWebViewRenderer : WebViewRenderer
	{
		protected override void OnElementChanged(VisualElementChangedEventArgs e)
		{
			base.OnElementChanged(e);
			
			// 背景を透過
			this.Opaque = false;
			this.BackgroundColor = UIColor.Clear;
		}
	}
}
```

## 2. カスタムコントロールが背景として張り付くようXamlを修正

今までのXamlを以下のように改造すると、CustomWebViewを背景に使えるようになります。
MainPage.xamlの背景にCustomWebViewを貼り付けます。

#### 2.1. ContentPageに、カスタムコントロールを利用するためのプロパティを追加する。

```xml:MainPage.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:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
             xmlns:controls="clr-namespace:WebViewBackgroundSample;assembly=WebViewBackgroundSample"　←追加部分
			 prism:ViewModelLocator.AutowireViewModel="True"
			 x:Class="WebViewBackgroundSample.Views.MainPage"
			 Title="MainPage">
（以下省略）
```

#### 2.2. 元々ContentPageの直下にあったStackLayoutを、Gridの中に移動させてCustomWebViewと並べる。

```xml:MainPage.xaml（修正前）
<?xml version="1.0" encoding="utf-8"?>
<ContentPage （プロパティ省略）>
	<StackLayout （プロパティ省略）>
		（元々配置していた諸々のコントロール）
	</StackLayout>
</ContentPage>
```

```xml:MainPage.xaml（修正後）
<?xml version="1.0" encoding="utf-8"?>
<ContentPage （プロパティ省略）>
	<StackLayout HorizontalOptions="Center" VerticalOptions="Center">
		<Grid>
			<controls:CustomWebView HorizontalOptions="FillAndExpand"
									VerticalOptions="FillAndExpand"
									WidthRequest="5000"
									HeightRequest="5000"
									Source="{Binding Url}">
			</controls:CustomWebView>
			<StackLayout （プロパティ省略）>
				（元々配置していた諸々のコントロール）
			</StackLayout>
		</Grid>
	</StackLayout>
</ContentPage>
```

WidthRequestとHeightRequestをとりあえず適当にでかくしておけば、CustomWebViewが全画面サイズで張り付いてくれます。
参照するHTMLファイルのパスをViewModelから制御できるようにするため、CustomWebViewのSourceは変数にバインディングするようにします。

## 3. 背景にするWeb系ファイルの用意・共通プロジェクトへの追加

用意したHTML、CSS、JavaScriptの各ファイルを、以下のように共通プロジェクトに追加します。

![スクリーンショット 2017-02-24 23.28.16.png](https://qiita-image-store.s3.amazonaws.com/0/140394/d6af8f20-c31f-0e33-20cc-ec908e8e31cf.png)

各ファイルを設置する階層はお好みです。
共通プロジェクトに置いた上でプラットフォーム固有のプロジェクトがそれを参照するようにすることで、背景部分のソースを共通化できます。

#### 注意
HTMLからCSSやJavaScriptを参照させる場合は、作った階層に対応した相対パスでの指定ではなく、ファイル名のみの指定にしてください。
これらのファイルは各プラットフォームのリソースフォルダに配置されることになりますが、そのときには全て同じ階層に置かれます。
HTMLからのCSS・JavaScriptの参照は、例えば以下のような具合です。

```html:Background_Transmission.html
<html>
	<head>
		<meta charset="UTF-8">
		<title>Transmission Background</title>
		<link rel="stylesheet" href="Background_Transmission.css">
		<script src="jquery-2.1.4.min.js"></script>
		<script src="Background_Transmission.js"></script>
	</head>
	<body>
		（省略）
	</body>
</html>
```

## 4. ［プラットフォーム別］Web系ファイルへのリンクの作成

#### 4.1. Android側のリンクの作成

Androidのプロジェクト内に初めから置かれているAssetsフォルダ内にリンクを作成します。

##### 4.1.1. Assetsフォルダを右クリックし、「追加」→「ファイルを追加」を選択する。

![Assetsフォルダ内にリンクを作成.png](https://qiita-image-store.s3.amazonaws.com/0/140394/076d3a42-dbfc-89b5-74f9-6af3e8bcf0db.png)

##### 4.1.2. 共通プロジェクトに追加しておいたWeb系ファイルを選択する。

##### 4.1.3. 「どう追加するか」を聞かれるので、「ファイルに対するリンクを追加する」を選択する。

![ファイルに対するリンクを追加する.png](https://qiita-image-store.s3.amazonaws.com/0/140394/905a9e20-ad26-8ab4-9e6f-bd0e5e29df5b.png)

この手順を、必要なファイル全てに対して行います。
最終的には、以下のようにAssetsフォルダ直下にWeb系リソースへのリンクが並ぶ形になります。

<img width="276" alt="Assetsフォルダ内.png" src="https://qiita-image-store.s3.amazonaws.com/0/140394/219e441e-52b1-ec55-aa51-3ce86866fab9.png">

#### 4.2. iOS側のリンクの作成

同様の手順を、iOSのプロジェクト内にあるResourcesフォルダに対して行います。

<img width="279" alt="スクリーンショット 2017-02-25 0.16.57.png" src="https://qiita-image-store.s3.amazonaws.com/0/140394/07cbeeb9-466d-9260-9f13-12aac15ec9f1.png">

#### なぜリソースフォルダ内に共通プロジェクトと同じようなフォルダ階層を作らないか？

理由はよく分かりませんが、そうしてしまうとビルド時にリンク対象のファイルを取得することができなくなり、ビルドが通らなくなります。

## 5. ローカルのHTMLファイルを読み込む

サンプルアプリでは、タップしたボタンに応じて背景のエフェクトが変わるようにしています。
「ボタンのタップに紐付いたCommandで、変数Url（この変数がCustomWebViewのSourceにバインドされている）を書き換える」ということをしています。
この部分は、以下のような実装になっています。

```csharp:MainPageViewModel.csのコンストラクタ内から抜粋
			_localHtmlFileRootPath = _resourceUtility.GetHtmlFileRootPath();

			// マトリックス風の背景に変更するボタンに紐付いているコマンド
			ChangeBackgroundMatrixCommand = new DelegateCommand(() => {
				Url = _localHtmlFileRootPath + "Background_Matrix.html";
			});

			// 何かを発信している風の背景に変更するボタンに紐付いているコマンド
			ChangeBackgroundTransmissionCommand = new DelegateCommand(() =>
			{
				Url = _localHtmlFileRootPath + "Background_Transmission.html";
			});

			//　何もない背景に変更するボタンに紐付いているコマンド
			ChangeBackgroundBlankCommand = new DelegateCommand(() =>
			{
				Url = _localHtmlFileRootPath + "Background_Blank.html";
			});
```

この中にある_resourceUtility.GetHtmlFileRootPath()は、プラットフォーム固有のリソースフォルダのパスを返すように作ったdependency serviceです。
プラットフォームごとの実装内容は以下のようになっています。

* Android

```csharp:ResourceUtility.cs内
		public string GetHtmlFileRootPath()
		{
			return "file:///android_asset/";
		}
```

* iOS

```csharp:ResourceUtility.cs内
		public string GetHtmlFileRootPath()
		{
			return NSBundle.MainBundle.BundleUrl.ToString();
		}
```