Xamarin.Forms の タブをカスタマイズしてみた

Xamarin.Forms の TabbedRenderer(iOS)/TabbedPageRenderer(Android) をカスタマイズして、デザインを変えてみました.

タブの背景色が選択/非選択で切り替わるデザインです.Android, iOS どちらとも標準デザインとは外れるのでカスタマイズが必要です.デザインをO統一するために、Android は画面下端にタブを配置しています.

TabbedPage をカスタマイズするには Android では TabbedPageRenderer(FormsAppCompatActivityを使用時)、iOS では TabbedRenderer を用います.

なんで iOS には TabbedPageRenderer が無いのだろうかと思ったら、AppCompat 用に別途用意されたのが TabbedPageRenderer のようです.

PCLプロジェクト

using System;
using Xamarin.Forms;

namespace FinancialRecommend.Controls
{
	public class CustomTabbedPage : TabbedPage
	{
	}
}

CustomTabbedPage を定義します.

<?xml version="1.0" encoding="UTF-8"?>

<controls:CustomTabbedPage 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:views="clr-namespace:FinancialRecommend.Views;assembly=FinancialRecommend"
            xmlns:controls="clr-namespace:FinancialRecommend.Controls;assembly=FinancialRecommend"
            prism:ViewModelLocator.AutowireViewModel="True"
            x:Class="FinancialRecommend.Views.RootTabbedPage"
			Title="タブ">

	<views:DiagnosticTopPage />
	<views:MediaPage />

</controls:CustomTabbedPage>

XAML での使い方は TabbedPage と同じです.

Androidプロジェクト

Android の TabbedPage は internal でやばいらしいんですが、こちらに裏技的解決法がありました.

Bottom Tabs for Xamarin.Android (in Xamarin.forms app) – Stack Overflow

using System;
using Android.Support.Design.Widget;
using Android.Support.V4.View;
using FinancialRecommend.Controls;
using FinancialRecommend.Droid.Renderers;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android.AppCompat;

[assembly: ExportRenderer(typeof(CustomTabbedPage), typeof(CustomTabbedPageRenderer))]
namespace FinancialRecommend.Droid.Renderers
{
	public class CustomTabbedPageRenderer : TabbedPageRenderer
	{
	    protected override void OnLayout(bool changed, int l, int t, int r, int b)
	    {
	        InvertLayoutThroughScale();
	        base.OnLayout(changed, l, t, r, b);
	    }

	    private void InvertLayoutThroughScale()
	    {
	        ViewGroup.ScaleY = -1;

	        TabLayout tabLayout = null;
	        ViewPager viewPager = null;

	        for (var i = 0; i < ChildCount; ++i)
	        {
	            var view = (Android.Views.View)GetChildAt(i);
	            if (view is TabLayout) tabLayout = (TabLayout)view;
	            else if (view is ViewPager) viewPager = (ViewPager)view;
	        }

	        tabLayout.ScaleY = viewPager.ScaleY = -1;
	        tabLayout.GetTabAt(0).SetIcon(Resource.Drawable.image_diag_tab);
	        tabLayout.GetTabAt(1).SetIcon(Resource.Drawable.image_media_tab);
	        viewPager.SetPadding(0, -tabLayout.MeasuredHeight, 0, 0);
	    }
	}
}

参考ページをベースに、最後のアイコンをセットするところだけ追記しています.これでタブを下端に配置し、アイコンを設定できました.

細かい色の設定などは、xml でやっていきます.

Resource.layout.tabs.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.TabLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/sliding_tabs"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="?attr/colorPrimary"
    android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
    app:tabIndicatorColor="@android:color/transparent"
    app:tabBackground="@drawable/tab_color_selector"
    app:tabTextAppearance="@style/TabText"
    app:tabGravity="fill"
    app:tabMode="fixed"
    android:elevation="4dp" />

以下のことを設定しています.

  • tabIndicatorColor を透明色にして、タブ選択時に下線が付かないようにする
  • tabBackground に選択/非選択で可変な背景を設定
  • tabTextAppearance にスタイルを適用

Resource.drawable.tab_color_selector.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@color/appBlue" android:state_selected="true"/>
    <item android:drawable="@color/appBlueWhite"/>
</selector>

タブの背景色は selector で変えてあげます.

Resources.values.styles.xml

<resources>
    <style name="TabText" parent="TextAppearance.Design.Tab">
        <item name="android:textSize">12sp</item>
    </style>
</resources>

テキストサイズをここで設定します.

以上が Android の実装です.

iOSプロジェクト

iOS は Android と違い Renderer だけで実装しています.(xib に追い出せるならそれもそれでいいんですが、やり方あるんですかね?)

using System;
using System.Linq;
using CoreGraphics;
using FinancialRecommend.Controls;
using FinancialRecommend.iOS.Renderers;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly: ExportRenderer(typeof(CustomTabbedPage), typeof(CustomTabbedPageRenderer))]

namespace FinancialRecommend.iOS.Renderers
{
    public class CustomTabbedPageRenderer : TabbedRenderer
    {
        public override void ViewWillAppear(bool animated)
        {
            base.ViewWillAppear(animated);

            var numberOfItems = TabBar.Items.Count();
            var tabBarItemSize = new CGSize()
            {
                Width = TabBar.Frame.Width / numberOfItems,
                Height = TabBar.Frame.Height
            };

            TabBar.SelectionIndicatorImage
                = ImageWithColor(
                        new UIColor(red: (nfloat) 0.02, green: (nfloat) 0.18, blue: (nfloat) 0.45, alpha: (nfloat) 1.0),
                        tabBarItemSize)
                    .CreateResizableImage(UIEdgeInsets.Zero);

            TabBar.Frame = new CGRect()
            {
                X = TabBar.Frame.X - 2,
                Y = TabBar.Frame.Y,
                Width = View.Frame.Width + 4,
                Height = TabBar.Frame.Height
            };

            TabBar.TintColor = UIColor.White;
            TabBar.BarTintColor = new UIColor(red: (nfloat) 0.58, green: (nfloat) 0.69, blue: (nfloat) 0.77,
                alpha: (nfloat) 1.0);

            var fontFamily = UIFont.SystemFontOfSize(10);
            var attributes = new UITextAttributes() {Font = fontFamily, TextColor = UIColor.White};
            UITabBarItem.Appearance.SetTitleTextAttributes(attributes, UIControlState.Normal);
            var assets = new string[] {"image_diag_tab", "image_media_tab.png"};

            TabBar.Items[0].Image = UIImage.FromBundle(assets[0])
                .ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);
            TabBar.Items[1].Image = UIImage.FromBundle(assets[1])
                .ImageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal);
        }

        private UIImage ImageWithColor(UIColor color, CGSize size)
        {
            var rect = new CGRect {X = 0, Y = 0, Width = size.Width, Height = size.Height};
            UIGraphics.BeginImageContextWithOptions(size, false, 0);
            color.SetFill();
            UIGraphics.RectFill(rect);
            var image = UIGraphics.GetImageFromCurrentImageContext();
            UIGraphics.EndImageContext();
            return image;
        }
    }
}

iOS はタブ選択時の背景色を設定する項目が無いので、色を付けた UIImage を TabBar.SelectionIndicatorImage に設定することで背景色を変えています.

この辺はネイティブのお作法だと思いますが一つハマったのが、これらの一連の処理を ViewWillLoad に書くとクラッシュします.

xamarin.ios - How do I override the Xamarin Forms TabbedPage item fonts for iOS? - Stack Overflow

ネイティブと Xamarin.Forms のライフサイクルの微妙な違いのせいだと思います.気付くのにだいぶ掛かりました(スタックトレース読んでもよくわからなかった;).

まとめ

今回は Android/iOS の TabbedPage をカスタマイズしてみました.Android は上手く xml を使ってあげると楽かなと思います.

Renderer を使うと細かいところまでカスタマイズできますが、ネイティブの微妙に違う場合に注意ですね.

以上です.

参考