bottom navigation tab page on android

This commit is contained in:
Kyle Spearrin 2017-12-28 17:31:44 -05:00
parent ea7290afab
commit fdc51f33ad
5 changed files with 665 additions and 309 deletions

View file

@ -73,6 +73,10 @@
<ItemGroup> <ItemGroup>
<Reference Include="Mono.Android" /> <Reference Include="Mono.Android" />
<Reference Include="Mono.Android.Export" /> <Reference Include="Mono.Android.Export" />
<Reference Include="Naxam.Ittianyu.BottomNavExtension, Version=1.2.2.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>.\Naxam.Ittianyu.BottomNavExtension.dll</HintPath>
</Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.IO.Compression" /> <Reference Include="System.IO.Compression" />
@ -924,5 +928,8 @@
<ItemGroup> <ItemGroup>
<AndroidResource Include="Resources\drawable-xxxhdpi\refresh_selected.png" /> <AndroidResource Include="Resources\drawable-xxxhdpi\refresh_selected.png" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable\bottom_nav_bg.xml" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" /> <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
</Project> </Project>

View file

@ -2,94 +2,427 @@
using Bit.Android.Controls; using Bit.Android.Controls;
using Bit.App.Controls; using Bit.App.Controls;
using Xamarin.Forms; using Xamarin.Forms;
using Com.Ittianyu.Bottomnavigationviewex;
using Xamarin.Forms.Platform.Android; using Xamarin.Forms.Platform.Android;
using Android.Support.Design.Widget; using RelativeLayout = Android.Widget.RelativeLayout;
using Xamarin.Forms.Platform.Android.AppCompat; using Platform = Xamarin.Forms.Platform.Android.Platform;
using System.Reflection;
using System.Linq;
using Android.Content; using Android.Content;
using Android.Views;
using Android.Widget;
using Android.Support.Design.Internal;
using System.IO;
using System.Linq;
using System.ComponentModel;
using Android.Support.Design.Widget;
[assembly: ExportRenderer(typeof(ExtendedTabbedPage), typeof(ExtendedTabbedPageRenderer))] [assembly: ExportRenderer(typeof(ExtendedTabbedPage), typeof(ExtendedTabbedPageRenderer))]
namespace Bit.Android.Controls namespace Bit.Android.Controls
{ {
public class ExtendedTabbedPageRenderer : TabbedPageRenderer, TabLayout.IOnTabSelectedListener public class ExtendedTabbedPageRenderer : VisualElementRenderer<ExtendedTabbedPage>,
BottomNavigationView.IOnNavigationItemSelectedListener
{ {
private TabLayout _tabLayout; public static bool ShouldUpdateSelectedIcon;
public static Action<IMenuItem, FileImageSource, bool> MenuItemIconSetter;
public static float? BottomBarHeight = 50;
private RelativeLayout _rootLayout;
private FrameLayout _pageContainer;
private BottomNavigationViewEx _bottomNav;
private readonly int _barId;
public static global::Android.Graphics.Color? BackgroundColor;
public ExtendedTabbedPageRenderer(Context context) public ExtendedTabbedPageRenderer(Context context)
: base(context) : base(context)
{ } {
AutoPackage = false;
_barId = GenerateViewId();
}
protected override void OnElementChanged(ElementChangedEventArgs<TabbedPage> e) IPageController TabbedController => Element as IPageController;
public int LastSelectedIndex { get; internal set; }
public bool OnNavigationItemSelected(IMenuItem item)
{
this.SwitchPage(item);
return true;
}
internal void SetupTabItems()
{
this.SetupTabItems(_bottomNav);
}
internal void SetupBottomBar()
{
_bottomNav = this.SetupBottomBar(_rootLayout, _bottomNav, _barId);
}
public static readonly Action<IMenuItem, FileImageSource, bool> DefaultMenuItemIconSetter = (menuItem, icon, selected) =>
{
var tabIconId = ResourceUtils.IdFromTitle(icon, ResourceManager.DrawableClass);
menuItem.SetIcon(tabIconId);
};
protected override void OnElementChanged(ElementChangedEventArgs<ExtendedTabbedPage> e)
{ {
base.OnElementChanged(e); base.OnElementChanged(e);
var view = (ExtendedTabbedPage)Element;
var field = typeof(ExtendedTabbedPageRenderer).BaseType.GetField("_tabLayout", if(e.OldElement != null)
BindingFlags.Instance | BindingFlags.NonPublic);
_tabLayout = field?.GetValue(this) as TabLayout;
if(_tabLayout != null)
{ {
var tab = _tabLayout.GetTabAt(0); e.OldElement.ChildAdded -= PagesChanged;
SetSelectedTabIcon(tab, Element.Children[0].Icon, true); e.OldElement.ChildRemoved -= PagesChanged;
} e.OldElement.ChildrenReordered -= PagesChanged;
} }
void TabLayout.IOnTabSelectedListener.OnTabSelected(TabLayout.Tab tab) if(e.NewElement == null)
{
if(Element == null)
{ {
return; return;
} }
var selectedIndex = tab.Position; UpdateIgnoreContainerAreas();
var child = Element.Children[selectedIndex];
if(Element.Children.Count > selectedIndex && selectedIndex >= 0) if(_rootLayout == null)
{ {
Element.CurrentPage = Element.Children[selectedIndex]; SetupNativeView();
} }
SetSelectedTabIcon(tab, child.Icon, true); this.HandlePagesChanged();
SwitchContent(Element.CurrentPage);
Element.ChildAdded += PagesChanged;
Element.ChildRemoved += PagesChanged;
Element.ChildrenReordered += PagesChanged;
} }
void TabLayout.IOnTabSelectedListener.OnTabUnselected(TabLayout.Tab tab) protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{ {
var child = Element.Children[tab.Position]; base.OnElementPropertyChanged(sender, e);
SetSelectedTabIcon(tab, child.Icon, false); if(e.PropertyName == nameof(TabbedPage.CurrentPage))
{
SwitchContent(Element.CurrentPage);
}
} }
private void SetSelectedTabIcon(TabLayout.Tab tab, string icon, bool selected) void PagesChanged(object sender, EventArgs e)
{ {
if(string.IsNullOrEmpty(icon)) this.HandlePagesChanged();
}
protected override void OnAttachedToWindow()
{
base.OnAttachedToWindow();
TabbedController?.SendAppearing();
}
protected override void OnDetachedFromWindow()
{
base.OnDetachedFromWindow();
TabbedController?.SendDisappearing();
}
protected override void OnLayout(bool changed, int left, int top, int right, int bottom)
{
var width = right - left;
var height = bottom - top;
base.OnLayout(changed, left, top, right, bottom);
if(width <= 0 || height <= 0)
{ {
return; return;
} }
if(selected) this.Layout(width, height);
}
protected override void Dispose(bool disposing)
{ {
var selectedIcon = string.Format("{0}_selected", icon.Replace(".png", string.Empty)); if(disposing)
var selectedResource = IdFromTitle(selectedIcon, ResourceManager.DrawableClass); {
if(selectedResource != 0) Element.ChildAdded -= PagesChanged;
Element.ChildRemoved -= PagesChanged;
Element.ChildrenReordered -= PagesChanged;
if(_rootLayout != null)
{
RemoveAllViews();
foreach(Page pageToRemove in Element.Children)
{
var pageRenderer = Platform.GetRenderer(pageToRemove);
if(pageRenderer != null)
{
pageRenderer.View.RemoveFromParent();
pageRenderer.Dispose();
}
}
if(_bottomNav != null)
{
_bottomNav.SetOnNavigationItemSelectedListener(null);
_bottomNav.Dispose();
_bottomNav = null;
}
_rootLayout.Dispose();
_rootLayout = null;
}
}
base.Dispose(disposing);
}
internal void SetupNativeView()
{
_rootLayout = this.CreateRoot(_barId, GenerateViewId(), out _pageContainer);
AddView(_rootLayout);
}
void SwitchContent(Page page)
{
this.ChangePage(_pageContainer, page);
}
void UpdateIgnoreContainerAreas()
{
foreach(var child in Element.Children)
{
child.IgnoresContainerArea = false;
}
}
}
internal static class TabExtensions
{
public static Rectangle CreateRect(this Context context, int width, int height)
{
return new Rectangle(0, 0, context.FromPixels(width), context.FromPixels(height));
}
public static void HandlePagesChanged(this ExtendedTabbedPageRenderer renderer)
{
renderer.SetupBottomBar();
renderer.SetupTabItems();
if(renderer.Element.Children.Count == 0)
{ {
tab.SetIcon(selectedResource);
return; return;
} }
EnsureTabIndex(renderer);
} }
var resource = IdFromTitle(icon, ResourceManager.DrawableClass); static void EnsureTabIndex(ExtendedTabbedPageRenderer renderer)
tab.SetIcon(resource);
}
private int IdFromTitle(string title, Type type)
{ {
var name = System.IO.Path.GetFileNameWithoutExtension(title); var rootLayout = (RelativeLayout)renderer.GetChildAt(0);
return GetId(type, name); var bottomNav = (BottomNavigationViewEx)rootLayout.GetChildAt(1);
var menu = (BottomNavigationMenu)bottomNav.Menu;
var itemIndex = menu.FindItemIndex(bottomNav.SelectedItemId);
var pageIndex = renderer.Element.Children.IndexOf(renderer.Element.CurrentPage);
if(pageIndex >= 0 && pageIndex != itemIndex && pageIndex < bottomNav.ItemCount)
{
var menuItem = menu.GetItem(pageIndex);
bottomNav.SelectedItemId = menuItem.ItemId;
if(ExtendedTabbedPageRenderer.ShouldUpdateSelectedIcon && ExtendedTabbedPageRenderer.MenuItemIconSetter != null)
{
ExtendedTabbedPageRenderer.MenuItemIconSetter?.Invoke(menuItem, renderer.Element.CurrentPage.Icon, true);
if(renderer.LastSelectedIndex != pageIndex)
{
var lastSelectedPage = renderer.Element.Children[renderer.LastSelectedIndex];
var lastSelectedMenuItem = menu.GetItem(renderer.LastSelectedIndex);
ExtendedTabbedPageRenderer.MenuItemIconSetter?.Invoke(lastSelectedMenuItem, lastSelectedPage.Icon, false);
renderer.LastSelectedIndex = pageIndex;
}
}
}
} }
private int GetId(Type type, string propertyName) public static void SwitchPage(this ExtendedTabbedPageRenderer renderer, IMenuItem item)
{
var rootLayout = (RelativeLayout)renderer.GetChildAt(0);
var bottomNav = (BottomNavigationViewEx)rootLayout.GetChildAt(1);
var menu = (BottomNavigationMenu)bottomNav.Menu;
var index = menu.FindItemIndex(item.ItemId);
var pageIndex = index % renderer.Element.Children.Count;
var currentPageIndex = renderer.Element.Children.IndexOf(renderer.Element.CurrentPage);
if(currentPageIndex != pageIndex)
{
renderer.Element.CurrentPage = renderer.Element.Children[pageIndex];
}
}
public static void Layout(this ExtendedTabbedPageRenderer renderer, int width, int height)
{
var rootLayout = (RelativeLayout)renderer.GetChildAt(0);
var bottomNav = (BottomNavigationViewEx)rootLayout.GetChildAt(1);
var Context = renderer.Context;
rootLayout.Measure(MakeMeasureSpec(width, MeasureSpecMode.Exactly),
MakeMeasureSpec(height, MeasureSpecMode.AtMost));
((IPageController)renderer.Element).ContainerArea =
Context.CreateRect(rootLayout.MeasuredWidth, rootLayout.GetChildAt(0).MeasuredHeight);
rootLayout.Measure(MakeMeasureSpec(width, MeasureSpecMode.Exactly),
MakeMeasureSpec(height, MeasureSpecMode.Exactly));
rootLayout.Layout(0, 0, rootLayout.MeasuredWidth, rootLayout.MeasuredHeight);
if(renderer.Element.Children.Count == 0)
{
return;
}
int tabsHeight = bottomNav.MeasuredHeight;
var item = (ViewGroup)bottomNav.GetChildAt(0);
item.Measure(MakeMeasureSpec(width, MeasureSpecMode.Exactly),
MakeMeasureSpec(tabsHeight, MeasureSpecMode.Exactly));
item.Layout(0, 0, width, tabsHeight);
int item_w = width / item.ChildCount;
for(int i = 0; i < item.ChildCount; i++)
{
var frame = (FrameLayout)item.GetChildAt(i);
frame.Measure(MakeMeasureSpec(item_w, MeasureSpecMode.Exactly),
MakeMeasureSpec(tabsHeight, MeasureSpecMode.Exactly));
frame.Layout(i * item_w, 0, i * item_w + item_w, tabsHeight);
}
}
public static void SetupTabItems(this ExtendedTabbedPageRenderer renderer, BottomNavigationViewEx bottomNav)
{
var element = renderer.Element;
var menu = (BottomNavigationMenu)bottomNav.Menu;
menu.ClearAll();
var tabsCount = Math.Min(element.Children.Count, bottomNav.MaxItemCount);
for(int i = 0; i < tabsCount; i++)
{
var page = element.Children[i];
var menuItem = menu.Add(0, i, 0, page.Title);
var setter = ExtendedTabbedPageRenderer.MenuItemIconSetter ?? ExtendedTabbedPageRenderer.DefaultMenuItemIconSetter;
setter.Invoke(menuItem, page.Icon, renderer.LastSelectedIndex == i);
}
if(element.Children.Count > 0)
{
bottomNav.EnableShiftingMode(false);
bottomNav.EnableItemShiftingMode(false);
bottomNav.EnableAnimation(false);
bottomNav.SetTextVisibility(false);
bottomNav.SetIconsMarginTop(30);
bottomNav.SetBackgroundResource(Resource.Drawable.bottom_nav_bg);
var stateList = new global::Android.Content.Res.ColorStateList(
new int[][] {
new int[] { global::Android.Resource.Attribute.StateChecked },
new int[] { global::Android.Resource.Attribute.StateEnabled}
},
new int[] {
element.TintColor.ToAndroid(), // Selected
Color.FromHex("A1A1A1").ToAndroid() // Normal
});
bottomNav.ItemIconTintList = stateList;
}
}
public static BottomNavigationViewEx SetupBottomBar(this ExtendedTabbedPageRenderer renderer,
global::Android.Widget.RelativeLayout rootLayout, BottomNavigationViewEx bottomNav, int barId)
{
if(bottomNav != null)
{
rootLayout.RemoveView(bottomNav);
bottomNav.SetOnNavigationItemSelectedListener(null);
}
var barParams = new global::Android.Widget.RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MatchParent,
ExtendedTabbedPageRenderer.BottomBarHeight.HasValue ?
(int)rootLayout.Context.ToPixels(ExtendedTabbedPageRenderer.BottomBarHeight.Value) :
ViewGroup.LayoutParams.WrapContent);
barParams.AddRule(LayoutRules.AlignParentBottom);
bottomNav = new BottomNavigationViewEx(rootLayout.Context)
{
LayoutParameters = barParams,
Id = barId
};
if(ExtendedTabbedPageRenderer.BackgroundColor.HasValue)
{
bottomNav.SetBackgroundColor(ExtendedTabbedPageRenderer.BackgroundColor.Value);
}
bottomNav.SetOnNavigationItemSelectedListener(renderer);
rootLayout.AddView(bottomNav, 1, barParams);
return bottomNav;
}
public static void ChangePage(this ExtendedTabbedPageRenderer renderer, FrameLayout pageContainer, Page page)
{
renderer.Context.HideKeyboard(renderer);
if(page == null)
{
return;
}
if(Platform.GetRenderer(page) == null)
{
Platform.SetRenderer(page, Platform.CreateRendererWithContext(page, renderer.Context));
}
var pageContent = Platform.GetRenderer(page).View;
pageContainer.AddView(pageContent);
if(pageContainer.ChildCount > 1)
{
pageContainer.RemoveViewAt(0);
}
EnsureTabIndex(renderer);
}
public static RelativeLayout CreateRoot(this ExtendedTabbedPageRenderer renderer, int barId, int pageContainerId, out FrameLayout pageContainer)
{
var rootLayout = new RelativeLayout(renderer.Context)
{
LayoutParameters = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.MatchParent),
};
var pageParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.MatchParent);
pageParams.AddRule(LayoutRules.Above, barId);
pageContainer = new FrameLayout(renderer.Context)
{
LayoutParameters = pageParams,
Id = pageContainerId
};
rootLayout.AddView(pageContainer, 0, pageParams);
return rootLayout;
}
private static int MakeMeasureSpec(int size, MeasureSpecMode mode)
{
return size + (int)mode;
}
}
public static class ResourceUtils
{
public static int IdFromTitle(string title, Type type)
{
var name = Path.GetFileNameWithoutExtension(title);
var id = GetId(type, name);
return id;
}
public static int GetId(Type type, string propertyName)
{ {
var props = type.GetFields(); var props = type.GetFields();
var prop = props.FirstOrDefault(p => p.Name == propertyName); var prop = props.Select(p => p).FirstOrDefault(p => p.Name == propertyName);
if(prop != null) if(prop != null)
{ {
return (int)prop.GetValue(type); return (int)prop.GetValue(type);

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="#D2D6DF" />
</shape>
</item>
<item android:top="1dp">
<shape android:shape="rectangle">
<solid android:color="#F5F5F7" />
</shape>
</item>
</layer-list>