Reputation: 103
I want to cradle floating action button. Button must be dynamic (it will be animated and cradle must adjust to it like in videos taken from material.io). It must works with as iOS as Android. XAML examples if you could. Maybe custom renderers.
I know this functionality exist in Android Studio and Flutter by default.
Write me if something in my question is wrong and sorry for my English.
Upvotes: 3
Views: 1743
Reputation: 366
You'll have to get your hands a little dirty with codes. My solution is for Android and I would also love to know how it is done in iOS. I'm relying on the nuget package Xamarin.Forms.Visual.Material. A great article that got me started can be found here and also here. Also checkout Google's Material design here.
In your Android Project, create a BottomTabLayout.xml file with the following content:
<?xml version="1.0" encoding="utf-8" ?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!-- Note: A RecyclerView can also be used -->
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="100dp"
android:clipToPadding="false">
<!-- Scrollable content -->
<FrameLayout
android:id="@+id/bottomtab.navarea"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="fill"
android:layout_weight="1" />
</androidx.core.widget.NestedScrollView>
<com.google.android.material.bottomappbar.BottomAppBar
android:id="@+id/bottomAppBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:contentInsetLeft="0dp"
app:contentInsetStart="0dp"
app:contentInsetRight="0dp"
app:contentInsetEnd="0dp"
app:fabCradleMargin="10dp">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomtab.tabbar"
android:theme="@style/Widget.Design.BottomNavigationView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginLeft="0dp"
android:layout_marginRight="0dp"
android:layout_marginEnd="0dp"
android:background="@android:color/transparent"/>
</com.google.android.material.bottomappbar.BottomAppBar>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/fab"
app:layout_anchor="@id/bottomAppBar" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
And in the style.xml file, modify its content as follow to use any of the Material Themes:
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<style name="MainTheme" parent="Theme.MaterialComponents.Light">
<!-- As of Xamarin.Forms 4.6 the theme has moved into the Forms binary -->
<!-- If you want to override anything you can do that here. -->
<!-- Underneath are a couple of entries to get you started. -->
<!-- Set theme colors from https://aka.ms/material-colors -->
<!-- colorPrimary is used for the default action bar background -->
<!--<item name="colorPrimary">#2196F3</item>-->
<!-- colorPrimaryDark is used for the status bar -->
<!--<item name="colorPrimaryDark">#1976D2</item>-->
<!-- colorAccent is used as the default value for colorControlActivated
which is used to tint widgets -->
<!--<item name="colorAccent">#FF4081</item>-->
<item name="android:windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
</resources>
Create a folder called "Renderers" and add the following classes:
BottomNavigationViewUtils2.cs (To handle dynamic menu generation)
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using FabTabBarDemo.Droid.Extensions;
using Google.Android.Material.BottomNavigation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
namespace FabTabBarDemo.Droid.Renderers
{
internal class BottomNavigationViewUtils2
{
internal const int MoreTabId = 99;
internal static void UpdateEnabled(bool tabEnabled, IMenuItem menuItem)
{
if (menuItem.IsEnabled != tabEnabled)
menuItem.SetEnabled(tabEnabled);
}
internal static async void SetupMenu(IMenu menu, int maxBottomItems,
List<(string title, ImageSource icon, bool tabEnabled)> items,
int currentIndex, BottomNavigationView bottomView, Context context, bool hasFab = false)
{
menu.Clear();
int numberOfMenuItems = items.Count;
bool showMore = numberOfMenuItems > maxBottomItems;
int end = showMore ? maxBottomItems - 1 : numberOfMenuItems;
List<IMenuItem> menuItems = new List<IMenuItem>();
List<Task> loadTasks = new List<Task>();
for (int i = 0; i < end; i++)
{
var item = items[i];
using var title = new Java.Lang.String(item.title);
var menuItem = menu.Add(0, i, 0, title);
menuItems.Add(menuItem);
loadTasks.Add(SetMenuItemIcon(menuItem, item.icon, context));
UpdateEnabled(item.tabEnabled, menuItem);
if (i == currentIndex)
{
menuItem.SetChecked(true);
bottomView.SelectedItemId = i;
}
if (hasFab)
{
int redundantMenus = CanAddRedundantMenu(numberOfMenuItems, i);
for (int x = 0; x < redundantMenus; x++)
{
menuItem = menu.Add(0, x + 20, 0, "");
UpdateEnabled(false, menuItem);
}
}
}
if (showMore)
{
var moreString = context.Resources.GetText(Resource.String.overflow_tab_title);
var menuItem = menu.Add(0, MoreTabId, 0, moreString);
menuItems.Add(menuItem);
menuItem.SetIcon(Resource.Drawable.abc_ic_menu_overflow_material);
if (currentIndex >= maxBottomItems - 1)
menuItem.SetChecked(true);
}
bottomView.SetShiftMode(false, false);
if (loadTasks.Count > 0)
await Task.WhenAll(loadTasks);
foreach (var menuItem in menuItems)
menuItem.Dispose();
}
/// <summary>
/// Gets a value that determines whether a redundant menu can be added to
/// the <see cref="BottomNavigationView"/>.
/// </summary>
/// <param name="menuCount">The total menu entries in the view.</param>
/// <param name="currentIndex">The zero-based menu index.</param>
/// <returns>The total number of redundant menus to add or -1.</returns>
internal static int CanAddRedundantMenu(int menuCount, int currentIndex)
{
return menuCount switch
{
1 => currentIndex == 0 ? 2 : -1,
3 => currentIndex == 1 ? 2 : -1,
2 => currentIndex == 0 ? 1 : -1,
4 => currentIndex == 1 ? 1 : -1,
_ => -1,
};
}
static async Task SetMenuItemIcon(IMenuItem menuItem, ImageSource source, Context context)
{
if (source == null)
return;
var drawable = await source.GetDrawableAsync(context);
//var drawable = await context.GetFormsDrawableAsync(source);
menuItem.SetIcon(drawable);
drawable?.Dispose();
}
}
}
FabBottomNavViewAppearanceTracker.cs (To manage BottomNavigationView rendering)
using Google.Android.Material.BottomNavigation;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
namespace FabTabBarDemo.Droid.Renderers
{
public class FabBottomNavViewAppearanceTracker : ShellBottomNavViewAppearanceTracker
{
public FabBottomNavViewAppearanceTracker(IShellContext shellContext, ShellItem shellItem) : base(shellContext, shellItem)
{
}
protected override void SetBackgroundColor(BottomNavigationView bottomView, Color color)
{
// Prevent the default xamarin forms background rendering of BottomNavigationView
}
}
}
FabShellItemRenderer.cs (To manage the shell tabs and handle rendering)
using Android.Content.Res;
using Android.OS;
using Android.Views;
using Android.Widget;
using Google.Android.Material.BottomAppBar;
using Google.Android.Material.BottomNavigation;
using Google.Android.Material.FloatingActionButton;
using System.Collections.Generic;
using System.Reflection;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
namespace FabTabBarDemo.Droid.Renderers
{
public class FabShellItemRenderer : ShellItemRenderer
{
private readonly FieldInfo navigationAreaField;
private readonly FieldInfo bottomViewField;
BottomAppBar _bottomAppBar;
FloatingActionButton _fab;
BottomNavigationView _bottomView;
IShellBottomNavViewAppearanceTracker appearanceTracker;
public FabShellItemRenderer(IShellContext shellContext) : base(shellContext)
{
var baseType = typeof(ShellItemRenderer);
navigationAreaField = baseType.GetField("_navigationArea", BindingFlags.Instance | BindingFlags.NonPublic);
bottomViewField = baseType.GetField("_bottomView", BindingFlags.Instance | BindingFlags.NonPublic);
}
protected BottomNavigationView BottomView => (BottomNavigationView)bottomViewField.GetValue(this);
public override Android.Views.View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
var layout = inflater.Inflate(Resource.Layout.BottomTabLayout, null);
var _navigationArea = layout.FindViewById<FrameLayout>(Resource.Id.bottomtab_navarea);
_bottomAppBar = layout.FindViewById<BottomAppBar>(Resource.Id.bottomAppBar);
_fab = layout.FindViewById<FloatingActionButton>(Resource.Id.fab);
_bottomView = layout.FindViewById<BottomNavigationView>(Resource.Id.bottomtab_tabbar);
_bottomView.Background = null;
_bottomView.SetOnNavigationItemSelectedListener(this);
navigationAreaField.SetValue(this, _navigationArea);
bottomViewField.SetValue(this, _bottomView);
HookEvents(ShellItem);
SetupMenu();
appearanceTracker = ShellContext.CreateBottomNavViewAppearanceTracker(ShellItem);
((IShellController)ShellContext.Shell).AddAppearanceObserver(this, ShellItem);
return layout;
}
void SetupMenu()
{
using var menu = _bottomView.Menu;
SetupMenu(menu, _bottomView.MaxItemCount, ShellItem);
}
protected override void ResetAppearance() => appearanceTracker?.ResetAppearance(BottomView);
protected override void SetAppearance(ShellAppearance appearance)
{
IShellAppearanceElement controller = appearance;
var backgroundColor = controller.EffectiveTabBarBackgroundColor.ToAndroid();
_bottomAppBar.BackgroundTintList = ColorStateList.ValueOf(backgroundColor);
_fab.BackgroundTintList = ColorStateList.ValueOf(backgroundColor);
appearanceTracker?.SetAppearance(BottomView, appearance);
}
protected override void SetupMenu(IMenu menu, int maxBottomItems, ShellItem shellItem)
{
var _bottomView = BottomView;
_bottomView.SetBackgroundColor(Android.Graphics.Color.Transparent);
_bottomView.Background = null;
var currentIndex = ((IShellItemController)ShellItem).GetItems().IndexOf(ShellSection);
var items = CreateTabList(shellItem);
BottomNavigationViewUtils2.SetupMenu(menu, maxBottomItems, items, currentIndex, _bottomView, Context, true);
UpdateTabBarVisibility();
}
List<(string title, ImageSource icon, bool tabEnabled)> CreateTabList(ShellItem shellItem)
{
var items = new List<(string title, ImageSource icon, bool tabEnabled)>();
var shellItems = ((IShellItemController)shellItem).GetItems();
for (int i = 0; i < shellItems.Count; i++)
{
var item = shellItems[i];
items.Add((item.Title, item.Icon, item.IsEnabled));
}
return items;
}
}
}
FabShellRenderer.cs (Shell renderer for the project)
using Android.Content;
using FabTabBarDemo.Droid.Renderers;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(Shell), typeof(FabShellRenderer))]
namespace FabTabBarDemo.Droid.Renderers
{
public class FabShellRenderer : ShellRenderer
{
public FabShellRenderer(Context context) : base(context)
{
}
protected override IShellItemRenderer CreateShellItemRenderer(ShellItem shellItem)
{
return new FabShellItemRenderer(this);
}
protected override IShellBottomNavViewAppearanceTracker CreateBottomNavViewAppearanceTracker(ShellItem shellItem)
{
return new FabBottomNavViewAppearanceTracker(this, shellItem);
}
}
}
And finally... ImageSourceExtensions.cs (some basic ImageSource logics)
using Android.Content;
using Android.Graphics.Drawables;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
namespace FabTabBarDemo.Droid.Extensions
{
internal static class ImageSourceExtensions
{
public static IImageSourceHandler GetHandler(this ImageSource imageSource)
{
if (imageSource is UriImageSource)
return new ImageLoaderSourceHandler();
else if (imageSource is FileImageSource)
return new FileImageSourceHandler();
else if (imageSource is StreamImageSource)
return new StreamImagesourceHandler();
else if (imageSource is FontImageSource)
return new FontImageSourceHandler();
return null;
}
public static async Task<Drawable> GetDrawableAsync(this ImageSource imageSource, Context context)
{
var imageHandler = imageSource.GetHandler();
if (imageHandler == null)
return null;
var bitmap = await imageHandler.LoadImageAsync(imageSource, context);
return new BitmapDrawable(context.Resources, bitmap);
}
}
}
In your Xamarin Shared Project (.Net Standard with Shell) use your TabBar as you wish. An example is Below:
<TabBar>
<ShellContent Title="Home" Route="HomePage" ContentTemplate="{DataTemplate local:HomePage}">
<ShellContent.Icon>
<FontImageSource FontFamily="{StaticResource FontAwesomeSolid}"
Glyph="{x:Static fonts:FaSolidIcons.Home}" />
</ShellContent.Icon>
</ShellContent>
<ShellContent Title="Search" Route="SearchPage" ContentTemplate="{DataTemplate local:SearchPage}">
<ShellContent.Icon>
<FontImageSource FontFamily="{StaticResource FontAwesomeSolid}"
Glyph="{x:Static fonts:FaSolidIcons.Search}" />
</ShellContent.Icon>
</ShellContent>
<ShellContent Title="Profile" Route="ProfilePage" ContentTemplate="{DataTemplate local:ProfilePage}">
<ShellContent.Icon>
<FontImageSource FontFamily="{StaticResource FontAwesomeSolid}"
Glyph="{x:Static fonts:FaSolidIcons.User}" />
</ShellContent.Icon>
</ShellContent>
<ShellContent Title="Settings" Route="SettingsPage" ContentTemplate="{DataTemplate local:SettingsPage}">
<ShellContent.Icon>
<FontImageSource FontFamily="{StaticResource FontAwesomeSolid}"
Glyph="{x:Static fonts:FaSolidIcons.Cog}" />
</ShellContent.Icon>
</ShellContent>
</TabBar>
And that's it. The result is shown below:
Note: Thememing and Icon for the FAB can easily be done and it's up to anyone interested. Happy coding!
Upvotes: 3