mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 23:25:45 +03:00
bottom navigation tab page on android
This commit is contained in:
parent
ea7290afab
commit
fdc51f33ad
5 changed files with 665 additions and 309 deletions
|
@ -73,6 +73,10 @@
|
|||
<ItemGroup>
|
||||
<Reference Include="Mono.Android" />
|
||||
<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.Core" />
|
||||
<Reference Include="System.IO.Compression" />
|
||||
|
@ -924,5 +928,8 @@
|
|||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable-xxxhdpi\refresh_selected.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidResource Include="Resources\drawable\bottom_nav_bg.xml" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
|
||||
</Project>
|
|
@ -2,94 +2,427 @@
|
|||
using Bit.Android.Controls;
|
||||
using Bit.App.Controls;
|
||||
using Xamarin.Forms;
|
||||
using Com.Ittianyu.Bottomnavigationviewex;
|
||||
using Xamarin.Forms.Platform.Android;
|
||||
using Android.Support.Design.Widget;
|
||||
using Xamarin.Forms.Platform.Android.AppCompat;
|
||||
using System.Reflection;
|
||||
using System.Linq;
|
||||
using RelativeLayout = Android.Widget.RelativeLayout;
|
||||
using Platform = Xamarin.Forms.Platform.Android.Platform;
|
||||
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))]
|
||||
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)
|
||||
: 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);
|
||||
var view = (ExtendedTabbedPage)Element;
|
||||
|
||||
var field = typeof(ExtendedTabbedPageRenderer).BaseType.GetField("_tabLayout",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
_tabLayout = field?.GetValue(this) as TabLayout;
|
||||
if(_tabLayout != null)
|
||||
if(e.OldElement != null)
|
||||
{
|
||||
var tab = _tabLayout.GetTabAt(0);
|
||||
SetSelectedTabIcon(tab, Element.Children[0].Icon, true);
|
||||
}
|
||||
e.OldElement.ChildAdded -= PagesChanged;
|
||||
e.OldElement.ChildRemoved -= PagesChanged;
|
||||
e.OldElement.ChildrenReordered -= PagesChanged;
|
||||
}
|
||||
|
||||
void TabLayout.IOnTabSelectedListener.OnTabSelected(TabLayout.Tab tab)
|
||||
{
|
||||
if(Element == null)
|
||||
if(e.NewElement == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedIndex = tab.Position;
|
||||
var child = Element.Children[selectedIndex];
|
||||
if(Element.Children.Count > selectedIndex && selectedIndex >= 0)
|
||||
UpdateIgnoreContainerAreas();
|
||||
|
||||
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];
|
||||
SetSelectedTabIcon(tab, child.Icon, false);
|
||||
base.OnElementPropertyChanged(sender, e);
|
||||
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;
|
||||
}
|
||||
|
||||
if(selected)
|
||||
this.Layout(width, height);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
var selectedIcon = string.Format("{0}_selected", icon.Replace(".png", string.Empty));
|
||||
var selectedResource = IdFromTitle(selectedIcon, ResourceManager.DrawableClass);
|
||||
if(selectedResource != 0)
|
||||
if(disposing)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
EnsureTabIndex(renderer);
|
||||
}
|
||||
|
||||
var resource = IdFromTitle(icon, ResourceManager.DrawableClass);
|
||||
tab.SetIcon(resource);
|
||||
}
|
||||
|
||||
private int IdFromTitle(string title, Type type)
|
||||
static void EnsureTabIndex(ExtendedTabbedPageRenderer renderer)
|
||||
{
|
||||
var name = System.IO.Path.GetFileNameWithoutExtension(title);
|
||||
return GetId(type, name);
|
||||
var rootLayout = (RelativeLayout)renderer.GetChildAt(0);
|
||||
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 prop = props.FirstOrDefault(p => p.Name == propertyName);
|
||||
var prop = props.Select(p => p).FirstOrDefault(p => p.Name == propertyName);
|
||||
if(prop != null)
|
||||
{
|
||||
return (int)prop.GetValue(type);
|
||||
|
|
BIN
src/Android/Naxam.Ittianyu.BottomNavExtension.dll
Normal file
BIN
src/Android/Naxam.Ittianyu.BottomNavExtension.dll
Normal file
Binary file not shown.
537
src/Android/Resources/Resource.Designer.cs
generated
537
src/Android/Resources/Resource.Designer.cs
generated
File diff suppressed because it is too large
Load diff
13
src/Android/Resources/drawable/bottom_nav_bg.xml
Normal file
13
src/Android/Resources/drawable/bottom_nav_bg.xml
Normal 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>
|
Loading…
Reference in a new issue