From 61e95e03c8b5fab2e8a4d6e798578b9cf8a8942b Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 4 Apr 2019 22:28:03 -0400 Subject: [PATCH] BoxedView with LabelCell --- src/Android/Android.csproj | 19 + .../BoxedView/BoxedViewRecyclerAdapter.cs | 532 ++++++++++++++++ .../Renderers/BoxedView/BoxedViewRenderer.cs | 195 ++++++ .../BoxedView/BoxedViewSimpleCallback.cs | 104 +++ .../BoxedView/Cells/BaseCellRenderer.cs | 76 +++ .../Renderers/BoxedView/Cells/BaseCellView.cs | 353 ++++++++++ .../BoxedView/Cells/LabelCellRenderer.cs | 133 ++++ src/Android/Renderers/RendererUtils.cs | 79 +++ src/Android/Resources/Resource.designer.cs | 592 +++++++++-------- .../Resources/layout/CellBaseView.axml | 50 ++ src/Android/Resources/layout/ContentCell.axml | 21 + src/Android/Resources/layout/FooterCell.axml | 15 + src/Android/Resources/layout/HeaderCell.axml | 22 + src/App/Controls/BoxedView/BoxedModel.cs | 85 +++ src/App/Controls/BoxedView/BoxedRoot.cs | 61 ++ src/App/Controls/BoxedView/BoxedSection.cs | 147 +++++ src/App/Controls/BoxedView/BoxedView.cs | 602 ++++++++++++++++++ src/App/Controls/BoxedView/Cells/BaseCell.cs | 87 +++ src/App/Controls/BoxedView/Cells/LabelCell.cs | 38 ++ src/App/Pages/BaseViewModel.cs | 3 + src/App/Pages/SettingsPage.xaml | 16 +- 21 files changed, 2945 insertions(+), 285 deletions(-) create mode 100644 src/Android/Renderers/BoxedView/BoxedViewRecyclerAdapter.cs create mode 100644 src/Android/Renderers/BoxedView/BoxedViewRenderer.cs create mode 100644 src/Android/Renderers/BoxedView/BoxedViewSimpleCallback.cs create mode 100644 src/Android/Renderers/BoxedView/Cells/BaseCellRenderer.cs create mode 100644 src/Android/Renderers/BoxedView/Cells/BaseCellView.cs create mode 100644 src/Android/Renderers/BoxedView/Cells/LabelCellRenderer.cs create mode 100644 src/Android/Renderers/RendererUtils.cs create mode 100644 src/Android/Resources/layout/CellBaseView.axml create mode 100644 src/Android/Resources/layout/ContentCell.axml create mode 100644 src/Android/Resources/layout/FooterCell.axml create mode 100644 src/Android/Resources/layout/HeaderCell.axml create mode 100644 src/App/Controls/BoxedView/BoxedModel.cs create mode 100644 src/App/Controls/BoxedView/BoxedRoot.cs create mode 100644 src/App/Controls/BoxedView/BoxedSection.cs create mode 100644 src/App/Controls/BoxedView/BoxedView.cs create mode 100644 src/App/Controls/BoxedView/Cells/BaseCell.cs create mode 100644 src/App/Controls/BoxedView/Cells/LabelCell.cs diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj index 43800d681..f883a241e 100644 --- a/src/Android/Android.csproj +++ b/src/Android/Android.csproj @@ -63,8 +63,15 @@ + + + + + + + @@ -194,5 +201,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Android/Renderers/BoxedView/BoxedViewRecyclerAdapter.cs b/src/Android/Renderers/BoxedView/BoxedViewRecyclerAdapter.cs new file mode 100644 index 000000000..ca91f39f6 --- /dev/null +++ b/src/Android/Renderers/BoxedView/BoxedViewRecyclerAdapter.cs @@ -0,0 +1,532 @@ +using Android.Content; +using Android.Runtime; +using Android.Support.V7.Widget; +using Android.Text; +using Android.Views; +using Android.Widget; +using Bit.App.Controls.BoxedView; +using System; +using System.Collections.Generic; +using System.Linq; +using Xamarin.Forms; +using Xamarin.Forms.Platform.Android; +using AView = Android.Views.View; + +namespace Bit.Droid.Renderers +{ + [Preserve(AllMembers = true)] + public class BoxedViewRecyclerAdapter : RecyclerView.Adapter, AView.IOnClickListener + { + private const int ViewTypeHeader = 0; + private const int ViewTypeFooter = 1; + + private float MinRowHeight => _context.ToPixels(44); + + private Dictionary _viewTypes; + private List _cellCaches; + + internal List CellCaches + { + get + { + if(_cellCaches == null) + { + FillCache(); + } + return _cellCaches; + } + } + + // Item click. correspond to AdapterView.IOnItemClickListener + private int _selectedIndex = -1; + private AView _preSelectedCell = null; + + Context _context; + BoxedView _boxedView; + RecyclerView _recyclerView; + + List _viewHolders = new List(); + + public BoxedViewRecyclerAdapter(Context context, BoxedView boxedView, RecyclerView recyclerView) + { + _context = context; + _boxedView = boxedView; + _recyclerView = recyclerView; + + _boxedView.ModelChanged += _boxedView_ModelChanged; + } + + void _boxedView_ModelChanged(object sender, EventArgs e) + { + if(_recyclerView != null) + { + _cellCaches = null; + NotifyDataSetChanged(); + } + } + + public override int ItemCount => CellCaches.Count; + + public override long GetItemId(int position) + { + return position; + } + + public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType) + { + ViewHolder viewHolder; + switch(viewType) + { + case ViewTypeHeader: + viewHolder = new HeaderViewHolder( + LayoutInflater.FromContext(_context).Inflate(Resource.Layout.HeaderCell, parent, false), + _boxedView); + break; + case ViewTypeFooter: + viewHolder = new FooterViewHolder( + LayoutInflater.FromContext(_context).Inflate(Resource.Layout.FooterCell, parent, false), + _boxedView); + break; + default: + viewHolder = new ContentViewHolder( + LayoutInflater.FromContext(_context).Inflate(Resource.Layout.ContentCell, parent, false)); + viewHolder.ItemView.SetOnClickListener(this); + break; + } + _viewHolders.Add(viewHolder); + return viewHolder; + } + + public void OnClick(AView view) + { + var position = _recyclerView.GetChildAdapterPosition(view); + + //TODO: It is desirable that the forms side has Selected property and reflects it. + // But do it at a later as iOS side doesn't have that process. + DeselectRow(); + + var cell = view.FindViewById(Resource.Id.ContentCellBody).GetChildAt(0) as BaseCellView; + + + if(cell == null || !CellCaches[position].Cell.IsEnabled) + { + //if FormsCell IsEnable is false, does nothing. + return; + } + + _boxedView.Model.RowSelected(CellCaches[position].Cell); + + cell.RowSelected(this, position); + } + + public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int position) + { + var cellInfo = CellCaches[position]; + + switch(holder.ItemViewType) + { + case ViewTypeHeader: + BindHeaderView((HeaderViewHolder)holder, (TextCell)cellInfo.Cell); + break; + case ViewTypeFooter: + BindFooterView((FooterViewHolder)holder, (TextCell)cellInfo.Cell); + break; + default: + BindContentView((ContentViewHolder)holder, cellInfo.Cell, position); + break; + } + } + + public override int GetItemViewType(int position) + { + var cellInfo = CellCaches[position]; + if(cellInfo.IsHeader) + { + return ViewTypeHeader; + } + else if(cellInfo.IsFooter) + { + return ViewTypeFooter; + } + else + { + return _viewTypes[cellInfo.Cell.GetType()]; + } + } + + public void DeselectRow() + { + if(_preSelectedCell != null) + { + _preSelectedCell.Selected = false; + _preSelectedCell = null; + } + _selectedIndex = -1; + } + + public void SelectedRow(AView cell, int position) + { + _preSelectedCell = cell; + _selectedIndex = position; + cell.Selected = true; + } + + protected override void Dispose(bool disposing) + { + if(disposing) + { + _boxedView.ModelChanged -= _boxedView_ModelChanged; + _cellCaches?.Clear(); + _cellCaches = null; + _boxedView = null; + _viewTypes = null; + foreach(var holder in _viewHolders) + { + holder.Dispose(); + } + _viewHolders.Clear(); + _viewHolders = null; + } + base.Dispose(disposing); + } + + + private void BindHeaderView(HeaderViewHolder holder, TextCell formsCell) + { + var view = holder.ItemView; + + //judging cell height + int cellHeight = (int)_context.ToPixels(44); + var individualHeight = formsCell.Height; + + if(individualHeight > 0d) + { + cellHeight = (int)_context.ToPixels(individualHeight); + } + else if(_boxedView.HeaderHeight > -1) + { + cellHeight = (int)_context.ToPixels(_boxedView.HeaderHeight); + } + + view.SetMinimumHeight(cellHeight); + view.LayoutParameters.Height = cellHeight; + + //textview setting + holder.TextView.SetPadding( + (int)view.Context.ToPixels(_boxedView.HeaderPadding.Left), + (int)view.Context.ToPixels(_boxedView.HeaderPadding.Top), + (int)view.Context.ToPixels(_boxedView.HeaderPadding.Right), + (int)view.Context.ToPixels(_boxedView.HeaderPadding.Bottom)); + + holder.TextView.Gravity = _boxedView.HeaderTextVerticalAlign.ToAndroidVertical() | GravityFlags.Left; + holder.TextView.TextAlignment = Android.Views.TextAlignment.Gravity; + holder.TextView.SetTextSize(Android.Util.ComplexUnitType.Sp, (float)_boxedView.HeaderFontSize); + holder.TextView.SetBackgroundColor(_boxedView.HeaderBackgroundColor.ToAndroid()); + holder.TextView.SetMaxLines(1); + holder.TextView.SetMinLines(1); + holder.TextView.Ellipsize = TextUtils.TruncateAt.End; + + if(_boxedView.HeaderTextColor != Color.Default) + { + holder.TextView.SetTextColor(_boxedView.HeaderTextColor.ToAndroid()); + } + + //border setting + if(_boxedView.ShowSectionTopBottomBorder) + { + holder.Border.SetBackgroundColor(_boxedView.SeparatorColor.ToAndroid()); + } + else + { + holder.Border.SetBackgroundColor(Android.Graphics.Color.Transparent); + } + + //update text + holder.TextView.Text = formsCell.Text; + } + + private void BindFooterView(FooterViewHolder holder, TextCell formsCell) + { + var view = holder.ItemView; + + //footer visible setting + if(string.IsNullOrEmpty(formsCell.Text)) + { + //if text is empty, hidden (height 0) + holder.TextView.Visibility = ViewStates.Gone; + view.Visibility = ViewStates.Gone; + } + else + { + holder.TextView.Visibility = ViewStates.Visible; + view.Visibility = ViewStates.Visible; + } + + //textview setting + holder.TextView.SetPadding( + (int)view.Context.ToPixels(_boxedView.FooterPadding.Left), + (int)view.Context.ToPixels(_boxedView.FooterPadding.Top), + (int)view.Context.ToPixels(_boxedView.FooterPadding.Right), + (int)view.Context.ToPixels(_boxedView.FooterPadding.Bottom)); + + holder.TextView.SetTextSize(Android.Util.ComplexUnitType.Sp, (float)_boxedView.FooterFontSize); + holder.TextView.SetBackgroundColor(_boxedView.FooterBackgroundColor.ToAndroid()); + if(_boxedView.FooterTextColor != Color.Default) + { + holder.TextView.SetTextColor(_boxedView.FooterTextColor.ToAndroid()); + } + + //update text + holder.TextView.Text = formsCell.Text; + } + + private void BindContentView(ContentViewHolder holder, Cell formsCell, int position) + { + AView nativeCell = null; + AView layout = holder.ItemView; + + holder.SectionIndex = CellCaches[position].SectionIndex; + holder.RowIndex = CellCaches[position].RowIndex; + + nativeCell = holder.Body.GetChildAt(0); + if(nativeCell != null) + { + holder.Body.RemoveViewAt(0); + } + + nativeCell = CellFactory.GetCell(formsCell, nativeCell, _recyclerView, _context, _boxedView); + + if(position == _selectedIndex) + { + DeselectRow(); + nativeCell.Selected = true; + _preSelectedCell = nativeCell; + } + + var minHeight = (int)Math.Max(_context.ToPixels(_boxedView.RowHeight), MinRowHeight); + + //it is neccesary to set both + layout.SetMinimumHeight(minHeight); + nativeCell.SetMinimumHeight(minHeight); + + if(!_boxedView.HasUnevenRows) + { + // if not Uneven, set the larger one of RowHeight and MinRowHeight. + layout.LayoutParameters.Height = minHeight; + } + else if(formsCell.Height > -1) + { + // if the cell itself was specified height, set it. + layout.SetMinimumHeight((int)_context.ToPixels(formsCell.Height)); + layout.LayoutParameters.Height = (int)_context.ToPixels(formsCell.Height); + } + else if(formsCell is ViewCell viewCell) + { + // if used a viewcell, calculate the size and layout it. + var size = viewCell.View.Measure(_boxedView.Width, double.PositiveInfinity); + viewCell.View.Layout(new Rectangle(0, 0, size.Request.Width, size.Request.Height)); + layout.LayoutParameters.Height = (int)_context.ToPixels(size.Request.Height); + } + else + { + layout.LayoutParameters.Height = -2; //wrap_content + } + + if(!CellCaches[position].IsLastCell || _boxedView.ShowSectionTopBottomBorder) + { + holder.Border.SetBackgroundColor(_boxedView.SeparatorColor.ToAndroid()); + } + else + { + holder.Border.SetBackgroundColor(Android.Graphics.Color.Transparent); + } + + holder.Body.AddView(nativeCell, 0); + } + + private void FillCache() + { + var model = _boxedView.Model; + int sectionCount = model.GetSectionCount(); + + var newCellCaches = new List(); + for(var sectionIndex = 0; sectionIndex < sectionCount; sectionIndex++) + { + var sectionTitle = model.GetSectionTitle(sectionIndex); + var sectionRowCount = model.GetRowCount(sectionIndex); + + Cell headerCell = new TextCell { Text = sectionTitle, Height = model.GetHeaderHeight(sectionIndex) }; + headerCell.Parent = _boxedView; + + newCellCaches.Add(new CellCache + { + Cell = headerCell, + IsHeader = true, + SectionIndex = sectionIndex, + }); + + for(int i = 0; i < sectionRowCount; i++) + { + newCellCaches.Add(new CellCache + { + Cell = model.GetCell(sectionIndex, i), + IsLastCell = i == sectionRowCount - 1, + SectionIndex = sectionIndex, + RowIndex = i + }); + } + + var footerCell = new TextCell { Text = model.GetFooterText(sectionIndex) }; + footerCell.Parent = _boxedView; + + newCellCaches.Add(new CellCache + { + Cell = footerCell, + IsFooter = true, + SectionIndex = sectionIndex, + }); + } + + _cellCaches = newCellCaches; + + if(_viewTypes == null) + { + _viewTypes = _cellCaches + .Select(x => x.Cell.GetType()) + .Distinct() + .Select((x, idx) => new { x, index = idx }) + .ToDictionary(key => key.x, val => val.index + 2); + } + else + { + var idx = _viewTypes.Values.Max() + 1; + foreach(var t in _cellCaches.Select(x => x.Cell.GetType()).Distinct().Except(_viewTypes.Keys).ToList()) + { + _viewTypes.Add(t, idx++); + } + } + } + + public void CellMoved(int fromPos, int toPos) + { + var tmp = CellCaches[fromPos]; + CellCaches.RemoveAt(fromPos); + CellCaches.Insert(toPos, tmp); + } + + [Preserve(AllMembers = true)] + internal class CellCache + { + public Cell Cell { get; set; } + public bool IsHeader { get; set; } = false; + public bool IsFooter { get; set; } = false; + public bool IsLastCell { get; set; } = false; + public int SectionIndex { get; set; } + public int RowIndex { get; set; } + } + } + + [Preserve(AllMembers = true)] + internal class ViewHolder : RecyclerView.ViewHolder + { + public ViewHolder(AView view) + : base(view) { } + + protected override void Dispose(bool disposing) + { + if(disposing) + { + ItemView?.Dispose(); + ItemView = null; + } + base.Dispose(disposing); + } + } + + [Preserve(AllMembers = true)] + internal class HeaderViewHolder : ViewHolder + { + public HeaderViewHolder(AView view, BoxedView boxedView) + : base(view) + { + TextView = view.FindViewById(Resource.Id.HeaderCellText); + Border = view.FindViewById(Resource.Id.HeaderCellBorder); + } + + public TextView TextView { get; private set; } + public LinearLayout Border { get; private set; } + + protected override void Dispose(bool disposing) + { + if(disposing) + { + TextView?.Dispose(); + TextView = null; + Border?.Dispose(); + Border = null; + } + base.Dispose(disposing); + } + } + + [Preserve(AllMembers = true)] + internal class FooterViewHolder : ViewHolder + { + public TextView TextView { get; private set; } + + public FooterViewHolder(AView view, BoxedView boxedView) + : base(view) + { + TextView = view.FindViewById(Resource.Id.FooterCellText); + } + + protected override void Dispose(bool disposing) + { + if(disposing) + { + TextView?.Dispose(); + TextView = null; + } + base.Dispose(disposing); + } + } + + [Preserve(AllMembers = true)] + internal class ContentViewHolder : ViewHolder + { + public LinearLayout Body { get; private set; } + public AView Border { get; private set; } + public int SectionIndex { get; set; } + public int RowIndex { get; set; } + + public ContentViewHolder(AView view) + : base(view) + { + Body = view.FindViewById(Resource.Id.ContentCellBody); + Border = view.FindViewById(Resource.Id.ContentCellBorder); + } + + protected override void Dispose(bool disposing) + { + if(disposing) + { + var nativeCell = Body.GetChildAt(0); + if(nativeCell is INativeElementView nativeElementView) + { + // If a ViewCell is used, it stops the ViewCellContainer from executing the dispose method. + // Because if the AiForms.Effects is used and a ViewCellContainer is disposed, it crashes. + if(!(nativeElementView.Element is ViewCell)) + { + nativeCell?.Dispose(); + } + } + Border?.Dispose(); + Border = null; + Body?.Dispose(); + Body = null; + ItemView.SetOnClickListener(null); + } + base.Dispose(disposing); + } + } +} diff --git a/src/Android/Renderers/BoxedView/BoxedViewRenderer.cs b/src/Android/Renderers/BoxedView/BoxedViewRenderer.cs new file mode 100644 index 000000000..6842682ac --- /dev/null +++ b/src/Android/Renderers/BoxedView/BoxedViewRenderer.cs @@ -0,0 +1,195 @@ +using Android.Content; +using Android.Runtime; +using Android.Support.V7.Widget; +using Android.Support.V7.Widget.Helper; +using Android.Views; +using Bit.App.Controls.BoxedView; +using Bit.Droid.Renderers; +using System; +using System.ComponentModel; +using Xamarin.Forms; +using Xamarin.Forms.Platform.Android; + +[assembly: ExportRenderer(typeof(BoxedView), typeof(BoxedViewRenderer))] +namespace Bit.Droid.Renderers +{ + [Preserve(AllMembers = true)] + public class BoxedViewRenderer : ViewRenderer + { + private Page _parentPage; + private LinearLayoutManager _layoutManager; + private ItemTouchHelper _itemTouchhelper; + private BoxedViewRecyclerAdapter _adapter; + private BoxedViewSimpleCallback _simpleCallback; + + public BoxedViewRenderer(Context context) + : base(context) + { + AutoPackage = false; + } + + protected override void OnElementChanged(ElementChangedEventArgs e) + { + base.OnElementChanged(e); + if(e.NewElement != null) + { + var recyclerView = new RecyclerView(Context); + _layoutManager = new LinearLayoutManager(Context); + recyclerView.SetLayoutManager(_layoutManager); + + SetNativeControl(recyclerView); + + Control.Focusable = false; + Control.DescendantFocusability = DescendantFocusability.AfterDescendants; + + UpdateBackgroundColor(); + UpdateRowHeight(); + + _adapter = new BoxedViewRecyclerAdapter(Context, e.NewElement, recyclerView); + Control.SetAdapter(_adapter); + + _simpleCallback = new BoxedViewSimpleCallback(e.NewElement, ItemTouchHelper.Up | ItemTouchHelper.Down, 0); + _itemTouchhelper = new ItemTouchHelper(_simpleCallback); + _itemTouchhelper.AttachToRecyclerView(Control); + + Element elm = Element; + while(elm != null) + { + elm = elm.Parent; + if(elm is Page) + { + break; + } + } + + _parentPage = elm as Page; + _parentPage.Appearing += ParentPageAppearing; + } + } + + private void ParentPageAppearing(object sender, EventArgs e) + { + Device.BeginInvokeOnMainThread(() => _adapter.DeselectRow()); + } + + protected override void OnLayout(bool changed, int left, int top, int right, int bottom) + { + base.OnLayout(changed, left, top, right, bottom); + if(!changed) + { + return; + } + + var startPos = _layoutManager.FindFirstCompletelyVisibleItemPosition(); + var endPos = _layoutManager.FindLastCompletelyVisibleItemPosition(); + + var totalH = 0; + for(var i = startPos; i <= endPos; i++) + { + var child = _layoutManager.GetChildAt(i); + if(child == null) + { + return; + } + + totalH += _layoutManager.GetChildAt(i).Height; + } + Element.VisibleContentHeight = Context.FromPixels(Math.Min(totalH, Control.Height)); + } + + protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.OnElementPropertyChanged(sender, e); + if(e.PropertyName == BoxedView.SeparatorColorProperty.PropertyName) + { + _adapter.NotifyDataSetChanged(); + } + else if(e.PropertyName == BoxedView.BackgroundColorProperty.PropertyName) + { + UpdateBackgroundColor(); + } + else if(e.PropertyName == TableView.RowHeightProperty.PropertyName) + { + UpdateRowHeight(); + } + else if(e.PropertyName == BoxedView.SelectedColorProperty.PropertyName) + { + //_adapter.NotifyDataSetChanged(); + } + else if(e.PropertyName == BoxedView.ShowSectionTopBottomBorderProperty.PropertyName) + { + _adapter.NotifyDataSetChanged(); + } + else if(e.PropertyName == TableView.HasUnevenRowsProperty.PropertyName) + { + _adapter.NotifyDataSetChanged(); + } + else if(e.PropertyName == BoxedView.ScrollToTopProperty.PropertyName) + { + UpdateScrollToTop(); + } + else if(e.PropertyName == BoxedView.ScrollToBottomProperty.PropertyName) + { + UpdateScrollToBottom(); + } + } + + private void UpdateRowHeight() + { + if(Element.RowHeight == -1) + { + Element.RowHeight = 60; + } + else + { + _adapter?.NotifyDataSetChanged(); + } + } + + private void UpdateScrollToTop() + { + if(Element.ScrollToTop) + { + _layoutManager.ScrollToPosition(0); + Element.ScrollToTop = false; + } + } + + private void UpdateScrollToBottom() + { + if(Element.ScrollToBottom) + { + if(_adapter != null) + { + _layoutManager.ScrollToPosition(_adapter.ItemCount - 1); + } + Element.ScrollToBottom = false; + } + } + + protected new void UpdateBackgroundColor() + { + if(Element.BackgroundColor != Color.Default) + { + Control.SetBackgroundColor(Element.BackgroundColor.ToAndroid()); + } + } + + protected override void Dispose(bool disposing) + { + if(disposing) + { + _parentPage.Appearing -= ParentPageAppearing; + _adapter?.Dispose(); + _adapter = null; + _layoutManager?.Dispose(); + _layoutManager = null; + _simpleCallback?.Dispose(); + _simpleCallback = null; + _itemTouchhelper?.Dispose(); + _itemTouchhelper = null; + } + base.Dispose(disposing); + } + } +} diff --git a/src/Android/Renderers/BoxedView/BoxedViewSimpleCallback.cs b/src/Android/Renderers/BoxedView/BoxedViewSimpleCallback.cs new file mode 100644 index 000000000..d505e18d9 --- /dev/null +++ b/src/Android/Renderers/BoxedView/BoxedViewSimpleCallback.cs @@ -0,0 +1,104 @@ +using Android.Runtime; +using Android.Support.V7.Widget; +using Android.Support.V7.Widget.Helper; +using Bit.App.Controls.BoxedView; +using System; + +namespace Bit.Droid.Renderers +{ + [Preserve(AllMembers = true)] + public class BoxedViewSimpleCallback : ItemTouchHelper.SimpleCallback + { + private BoxedView _boxedView; + private int _offset = 0; + + public BoxedViewSimpleCallback(BoxedView boxedView, int dragDirs, int swipeDirs) + : base(dragDirs, swipeDirs) + { + _boxedView = boxedView; + } + + public override bool OnMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) + { + if(!(viewHolder is ContentViewHolder fromContentHolder)) + { + return false; + } + if(!(target is ContentViewHolder toContentHolder)) + { + return false; + } + if(fromContentHolder.SectionIndex != toContentHolder.SectionIndex) + { + return false; + } + var section = _boxedView.Model.GetSection(fromContentHolder.SectionIndex); + if(section == null || !section.UseDragSort) + { + return false; + } + + var fromPos = viewHolder.AdapterPosition; + var toPos = target.AdapterPosition; + _offset += toPos - fromPos; + var settingsAdapter = recyclerView.GetAdapter() as BoxedViewRecyclerAdapter; + settingsAdapter.NotifyItemMoved(fromPos, toPos); // rows update + settingsAdapter.CellMoved(fromPos, toPos); // caches update + return true; + } + + public override void ClearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) + { + base.ClearView(recyclerView, viewHolder); + if(!(viewHolder is ContentViewHolder contentHolder)) + { + return; + } + + var section = _boxedView.Model.GetSection(contentHolder.SectionIndex); + var pos = contentHolder.RowIndex; + if(section.ItemsSource == null) + { + var tmp = section[pos]; + section.RemoveAt(pos); + section.Insert(pos + _offset, tmp); + } + else if(section.ItemsSource != null) + { + // must update DataSource at this timing. + var tmp = section.ItemsSource[pos]; + section.ItemsSource.RemoveAt(pos); + section.ItemsSource.Insert(pos + _offset, tmp); + } + _offset = 0; + } + + public override int GetDragDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) + { + if(!(viewHolder is ContentViewHolder contentHolder)) + { + return 0; + } + var section = _boxedView.Model.GetSection(contentHolder.SectionIndex); + if(section == null || !section.UseDragSort) + { + return 0; + } + return base.GetDragDirs(recyclerView, viewHolder); + } + + public override void OnSwiped(RecyclerView.ViewHolder viewHolder, int direction) + { + throw new NotImplementedException(); + } + + protected override void Dispose(bool disposing) + { + if(disposing) + { + _boxedView = null; + } + base.Dispose(disposing); + } + } +} diff --git a/src/Android/Renderers/BoxedView/Cells/BaseCellRenderer.cs b/src/Android/Renderers/BoxedView/Cells/BaseCellRenderer.cs new file mode 100644 index 000000000..2b92bddc6 --- /dev/null +++ b/src/Android/Renderers/BoxedView/Cells/BaseCellRenderer.cs @@ -0,0 +1,76 @@ +using Android.Content; +using Android.Runtime; +using Android.Views; +using Bit.App.Controls.BoxedView; +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Xamarin.Forms.Platform.Android; + +namespace Bit.Droid.Renderers +{ + [Preserve(AllMembers = true)] + public class BaseCellRenderer : CellRenderer where TNativeCell : BaseCellView + { + internal static class InstanceCreator + { + public static Func Create { get; } = CreateInstance(); + + private static Func CreateInstance() + { + var argsTypes = new[] { typeof(T1), typeof(T2) }; + var constructor = typeof(TInstance).GetConstructor( + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + Type.DefaultBinder, argsTypes, null); + var args = argsTypes.Select(Expression.Parameter).ToArray(); + return Expression.Lambda>(Expression.New(constructor, args), args).Compile(); + } + } + + protected override View GetCellCore(Xamarin.Forms.Cell item, View convertView, ViewGroup parent, + Context context) + { + if(!(convertView is TNativeCell nativeCell)) + { + nativeCell = InstanceCreator.Create(context, item); + } + + ClearPropertyChanged(nativeCell); + nativeCell.Cell = item; + SetUpPropertyChanged(nativeCell); + nativeCell.UpdateCell(); + return nativeCell; + } + + protected void SetUpPropertyChanged(BaseCellView nativeCell) + { + var formsCell = nativeCell.Cell as BaseCell; + formsCell.PropertyChanged += nativeCell.CellPropertyChanged; + if(formsCell.Parent is BoxedView parentElement) + { + parentElement.PropertyChanged += nativeCell.ParentPropertyChanged; + var section = parentElement.Model.GetSection(BoxedModel.GetPath(formsCell).Item1); + if(section != null) + { + formsCell.Section = section; + formsCell.Section.PropertyChanged += nativeCell.SectionPropertyChanged; + } + } + } + + private void ClearPropertyChanged(BaseCellView nativeCell) + { + var formsCell = nativeCell.Cell as BaseCell; + formsCell.PropertyChanged -= nativeCell.CellPropertyChanged; + if(formsCell.Parent is BoxedView parentElement) + { + parentElement.PropertyChanged -= nativeCell.ParentPropertyChanged; + if(formsCell.Section != null) + { + formsCell.Section.PropertyChanged -= nativeCell.SectionPropertyChanged; + } + } + } + } +} diff --git a/src/Android/Renderers/BoxedView/Cells/BaseCellView.cs b/src/Android/Renderers/BoxedView/Cells/BaseCellView.cs new file mode 100644 index 000000000..e35d6f943 --- /dev/null +++ b/src/Android/Renderers/BoxedView/Cells/BaseCellView.cs @@ -0,0 +1,353 @@ +using Android.Content; +using Android.Graphics.Drawables; +using Android.Runtime; +using Android.Util; +using Android.Views; +using Android.Widget; +using Bit.App.Controls.BoxedView; +using System; +using System.ComponentModel; +using System.Threading; +using Xamarin.Forms; +using Xamarin.Forms.Platform.Android; +using ARelativeLayout = Android.Widget.RelativeLayout; + +namespace Bit.Droid.Renderers +{ + [Preserve(AllMembers = true)] + public class BaseCellView : ARelativeLayout, INativeElementView + { + private CancellationTokenSource _iconTokenSource; + private Android.Graphics.Color _defaultTextColor; + private ColorDrawable _backgroundColor; + private ColorDrawable _selectedColor; + private RippleDrawable _ripple; + private float _defaultFontSize; + + protected Context _Context; + + public BaseCellView(Context context, Cell cell) + : base(context) + { + _Context = context; + Cell = cell; + CreateContentView(); + } + + public Cell Cell { get; set; } + public Element Element => Cell; + protected BaseCell CellBase => Cell as BaseCell; + public BoxedView CellParent => Cell.Parent as BoxedView; + public TextView TitleLabel { get; set; } + public TextView DescriptionLabel { get; set; } + public LinearLayout ContentStack { get; set; } + public LinearLayout AccessoryStack { get; set; } + + private void CreateContentView() + { + var contentView = (_Context as FormsAppCompatActivity) + .LayoutInflater + .Inflate(Resource.Layout.CellBaseView, this, true); + + contentView.LayoutParameters = new ViewGroup.LayoutParams(-1, -1); + + TitleLabel = contentView.FindViewById(Resource.Id.CellTitle); + DescriptionLabel = contentView.FindViewById(Resource.Id.CellDescription); + ContentStack = contentView.FindViewById(Resource.Id.CellContentStack); + AccessoryStack = contentView.FindViewById(Resource.Id.CellAccessoryView); + + _backgroundColor = new ColorDrawable(); + _selectedColor = new ColorDrawable(Android.Graphics.Color.Argb(125, 180, 180, 180)); + + var sel = new StateListDrawable(); + + sel.AddState(new int[] { Android.Resource.Attribute.StateSelected }, _selectedColor); + sel.AddState(new int[] { -Android.Resource.Attribute.StateSelected }, _backgroundColor); + sel.SetExitFadeDuration(250); + sel.SetEnterFadeDuration(250); + + var rippleColor = Android.Graphics.Color.Rgb(180, 180, 180); + if(CellParent.SelectedColor != Color.Default) + { + rippleColor = CellParent.SelectedColor.ToAndroid(); + } + _ripple = RendererUtils.CreateRipple(rippleColor, sel); + Background = _ripple; + + _defaultTextColor = new Android.Graphics.Color(TitleLabel.CurrentTextColor); + _defaultFontSize = TitleLabel.TextSize; + } + + public virtual void CellPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if(e.PropertyName == BaseCell.TitleProperty.PropertyName) + { + UpdateTitleText(); + } + else if(e.PropertyName == BaseCell.TitleColorProperty.PropertyName) + { + UpdateTitleColor(); + } + else if(e.PropertyName == BaseCell.TitleFontSizeProperty.PropertyName) + { + UpdateTitleFontSize(); + } + else if(e.PropertyName == BaseCell.DescriptionProperty.PropertyName) + { + UpdateDescriptionText(); + } + else if(e.PropertyName == BaseCell.DescriptionFontSizeProperty.PropertyName) + { + UpdateDescriptionFontSize(); + } + else if(e.PropertyName == BaseCell.DescriptionColorProperty.PropertyName) + { + UpdateDescriptionColor(); + } + else if(e.PropertyName == BaseCell.BackgroundColorProperty.PropertyName) + { + UpdateBackgroundColor(); + } + else if(e.PropertyName == Cell.IsEnabledProperty.PropertyName) + { + UpdateIsEnabled(); + } + } + + public virtual void ParentPropertyChanged(object sender, PropertyChangedEventArgs e) + { + // Avoid running the vain process when popping a page. + if((sender as BindableObject)?.BindingContext == null) + { + return; + } + + if(e.PropertyName == BoxedView.CellTitleColorProperty.PropertyName) + { + UpdateTitleColor(); + } + else if(e.PropertyName == BoxedView.CellTitleFontSizeProperty.PropertyName) + { + UpdateWithForceLayout(UpdateTitleFontSize); + } + else if(e.PropertyName == BoxedView.CellDescriptionColorProperty.PropertyName) + { + UpdateDescriptionColor(); + } + else if(e.PropertyName == BoxedView.CellDescriptionFontSizeProperty.PropertyName) + { + UpdateWithForceLayout(UpdateDescriptionFontSize); + } + else if(e.PropertyName == BoxedView.CellBackgroundColorProperty.PropertyName) + { + UpdateBackgroundColor(); + } + else if(e.PropertyName == BoxedView.SelectedColorProperty.PropertyName) + { + UpdateWithForceLayout(UpdateSelectedColor); + } + } + + public virtual void SectionPropertyChanged(object sender, PropertyChangedEventArgs e) + { } + + public virtual void RowSelected(BoxedViewRecyclerAdapter adapter, int position) + { } + + protected void UpdateWithForceLayout(Action updateAction) + { + updateAction(); + Invalidate(); + } + + public virtual void UpdateCell() + { + UpdateBackgroundColor(); + UpdateSelectedColor(); + UpdateTitleText(); + UpdateTitleColor(); + UpdateTitleFontSize(); + UpdateDescriptionText(); + UpdateDescriptionColor(); + UpdateDescriptionFontSize(); + + UpdateIsEnabled(); + + Invalidate(); + } + + private void UpdateBackgroundColor() + { + Selected = false; + if(CellBase.BackgroundColor != Color.Default) + { + _backgroundColor.Color = CellBase.BackgroundColor.ToAndroid(); + } + else if(CellParent != null && CellParent.CellBackgroundColor != Color.Default) + { + _backgroundColor.Color = CellParent.CellBackgroundColor.ToAndroid(); + } + else + { + _backgroundColor.Color = Android.Graphics.Color.Transparent; + } + } + + private void UpdateSelectedColor() + { + if(CellParent != null && CellParent.SelectedColor != Color.Default) + { + _selectedColor.Color = CellParent.SelectedColor.MultiplyAlpha(0.5).ToAndroid(); + _ripple.SetColor(RendererUtils.GetPressedColorSelector(CellParent.SelectedColor.ToAndroid())); + } + else + { + _selectedColor.Color = Android.Graphics.Color.Argb(125, 180, 180, 180); + _ripple.SetColor(RendererUtils.GetPressedColorSelector(Android.Graphics.Color.Rgb(180, 180, 180))); + } + } + + private void UpdateTitleText() + { + TitleLabel.Text = CellBase.Title; + // Hide TextView right padding when TextView.Text empty. + TitleLabel.Visibility = string.IsNullOrEmpty(TitleLabel.Text) ? ViewStates.Gone : ViewStates.Visible; + } + + private void UpdateTitleColor() + { + if(CellBase.TitleColor != Color.Default) + { + TitleLabel.SetTextColor(CellBase.TitleColor.ToAndroid()); + } + else if(CellParent != null && CellParent.CellTitleColor != Color.Default) + { + TitleLabel.SetTextColor(CellParent.CellTitleColor.ToAndroid()); + } + else + { + TitleLabel.SetTextColor(_defaultTextColor); + } + } + + private void UpdateTitleFontSize() + { + if(CellBase.TitleFontSize > 0) + { + TitleLabel.SetTextSize(ComplexUnitType.Sp, (float)CellBase.TitleFontSize); + } + else if(CellParent != null) + { + TitleLabel.SetTextSize(ComplexUnitType.Sp, (float)CellParent.CellTitleFontSize); + } + else + { + TitleLabel.SetTextSize(ComplexUnitType.Sp, _defaultFontSize); + } + } + + private void UpdateDescriptionText() + { + DescriptionLabel.Text = CellBase.Description; + DescriptionLabel.Visibility = string.IsNullOrEmpty(DescriptionLabel.Text) ? + ViewStates.Gone : ViewStates.Visible; + } + + private void UpdateDescriptionFontSize() + { + if(CellBase.DescriptionFontSize > 0) + { + DescriptionLabel.SetTextSize(ComplexUnitType.Sp, (float)CellBase.DescriptionFontSize); + } + else if(CellParent != null) + { + DescriptionLabel.SetTextSize(ComplexUnitType.Sp, (float)CellParent.CellDescriptionFontSize); + } + else + { + DescriptionLabel.SetTextSize(ComplexUnitType.Sp, _defaultFontSize); + } + } + + private void UpdateDescriptionColor() + { + if(CellBase.DescriptionColor != Color.Default) + { + DescriptionLabel.SetTextColor(CellBase.DescriptionColor.ToAndroid()); + } + else if(CellParent != null && CellParent.CellDescriptionColor != Color.Default) + { + DescriptionLabel.SetTextColor(CellParent.CellDescriptionColor.ToAndroid()); + } + else + { + DescriptionLabel.SetTextColor(_defaultTextColor); + } + } + + protected virtual void UpdateIsEnabled() + { + SetEnabledAppearance(CellBase.IsEnabled); + } + + protected virtual void SetEnabledAppearance(bool isEnabled) + { + if(isEnabled) + { + Focusable = false; + DescendantFocusability = DescendantFocusability.AfterDescendants; + TitleLabel.Alpha = 1f; + DescriptionLabel.Alpha = 1f; + } + else + { + // not to invoke a ripple effect and not to selected + Focusable = true; + DescendantFocusability = DescendantFocusability.BlockDescendants; + // to turn like disabled + TitleLabel.Alpha = 0.3f; + DescriptionLabel.Alpha = 0.3f; + } + } + + protected override void Dispose(bool disposing) + { + if(disposing) + { + CellBase.PropertyChanged -= CellPropertyChanged; + CellParent.PropertyChanged -= ParentPropertyChanged; + + if(CellBase.Section != null) + { + CellBase.Section.PropertyChanged -= SectionPropertyChanged; + CellBase.Section = null; + } + + TitleLabel?.Dispose(); + TitleLabel = null; + DescriptionLabel?.Dispose(); + DescriptionLabel = null; + ContentStack?.Dispose(); + ContentStack = null; + AccessoryStack?.Dispose(); + AccessoryStack = null; + Cell = null; + + _iconTokenSource?.Dispose(); + _iconTokenSource = null; + _Context = null; + + _backgroundColor?.Dispose(); + _backgroundColor = null; + _selectedColor?.Dispose(); + _selectedColor = null; + _ripple?.Dispose(); + _ripple = null; + + Background?.Dispose(); + Background = null; + } + base.Dispose(disposing); + } + } +} diff --git a/src/Android/Renderers/BoxedView/Cells/LabelCellRenderer.cs b/src/Android/Renderers/BoxedView/Cells/LabelCellRenderer.cs new file mode 100644 index 000000000..7428d093c --- /dev/null +++ b/src/Android/Renderers/BoxedView/Cells/LabelCellRenderer.cs @@ -0,0 +1,133 @@ +using Android.Content; +using Android.Runtime; +using Android.Text; +using Android.Views; +using Android.Widget; +using Bit.App.Controls.BoxedView; +using Bit.Droid.Renderers; +using System.ComponentModel; +using Xamarin.Forms; +using Xamarin.Forms.Platform.Android; + +[assembly: ExportRenderer(typeof(LabelCell), typeof(LabelCellRenderer))] +namespace Bit.Droid.Renderers +{ + [Preserve(AllMembers = true)] + public class LabelCellRenderer : BaseCellRenderer + { } + + [Preserve(AllMembers = true)] + public class LabelCellView : BaseCellView + { + public LabelCellView(Context context, Cell cell) + : base(context, cell) + { + ValueLabel = new TextView(context); + ValueLabel.SetSingleLine(true); + ValueLabel.Ellipsize = TextUtils.TruncateAt.End; + ValueLabel.Gravity = GravityFlags.Right; + + var textParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WrapContent, + ViewGroup.LayoutParams.WrapContent); + using(textParams) + { + ContentStack.AddView(ValueLabel, textParams); + } + } + + private LabelCell _LabelCell => Cell as LabelCell; + + public TextView ValueLabel { get; set; } + + public override void CellPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.CellPropertyChanged(sender, e); + if(e.PropertyName == LabelCell.ValueTextProperty.PropertyName) + { + UpdateValueText(); + } + else if(e.PropertyName == LabelCell.ValueTextFontSizeProperty.PropertyName) + { + UpdateValueTextFontSize(); + } + else if(e.PropertyName == LabelCell.ValueTextColorProperty.PropertyName) + { + UpdateValueTextColor(); + } + } + + public override void ParentPropertyChanged(object sender, PropertyChangedEventArgs e) + { + base.ParentPropertyChanged(sender, e); + if(e.PropertyName == BoxedView.CellValueTextColorProperty.PropertyName) + { + UpdateValueTextColor(); + } + else if(e.PropertyName == BoxedView.CellValueTextFontSizeProperty.PropertyName) + { + UpdateValueTextFontSize(); + } + } + + public override void UpdateCell() + { + base.UpdateCell(); + UpdateValueText(); + UpdateValueTextColor(); + UpdateValueTextFontSize(); + } + + protected override void SetEnabledAppearance(bool isEnabled) + { + if(isEnabled) + { + ValueLabel.Alpha = 1f; + } + else + { + ValueLabel.Alpha = 0.3f; + } + base.SetEnabledAppearance(isEnabled); + } + + protected void UpdateValueText() + { + ValueLabel.Text = _LabelCell.ValueText; + } + + private void UpdateValueTextFontSize() + { + if(_LabelCell.ValueTextFontSize > 0) + { + ValueLabel.SetTextSize(Android.Util.ComplexUnitType.Sp, (float)_LabelCell.ValueTextFontSize); + } + else if(CellParent != null) + { + ValueLabel.SetTextSize(Android.Util.ComplexUnitType.Sp, (float)CellParent.CellValueTextFontSize); + } + Invalidate(); + } + + private void UpdateValueTextColor() + { + if(_LabelCell.ValueTextColor != Color.Default) + { + ValueLabel.SetTextColor(_LabelCell.ValueTextColor.ToAndroid()); + } + else if(CellParent != null && CellParent.CellValueTextColor != Color.Default) + { + ValueLabel.SetTextColor(CellParent.CellValueTextColor.ToAndroid()); + } + } + + protected override void Dispose(bool disposing) + { + if(disposing) + { + ValueLabel?.Dispose(); + ValueLabel = null; + } + base.Dispose(disposing); + } + } +} diff --git a/src/Android/Renderers/RendererUtils.cs b/src/Android/Renderers/RendererUtils.cs new file mode 100644 index 000000000..8a66a58e9 --- /dev/null +++ b/src/Android/Renderers/RendererUtils.cs @@ -0,0 +1,79 @@ +using Android.Content.Res; +using Android.Graphics.Drawables; +using Android.Views; +using Xamarin.Forms; + +namespace Bit.Droid.Renderers +{ + [Android.Runtime.Preserve(AllMembers = true)] + public static class RendererUtils + { + public static GravityFlags ToAndroidVertical(this LayoutAlignment formsAlignment) + { + switch(formsAlignment) + { + case LayoutAlignment.Start: + return GravityFlags.Top; + case LayoutAlignment.Center: + return GravityFlags.CenterVertical; + case LayoutAlignment.End: + return GravityFlags.Bottom; + default: + return GravityFlags.FillHorizontal; + } + } + + public static GravityFlags ToAndroidHorizontal(this LayoutAlignment formsAlignment) + { + switch(formsAlignment) + { + case LayoutAlignment.Start: + return GravityFlags.Start; + case LayoutAlignment.Center: + return GravityFlags.CenterHorizontal; + case LayoutAlignment.End: + return GravityFlags.End; + default: + return GravityFlags.FillVertical; + } + } + + public static GravityFlags ToAndroidVertical(this Xamarin.Forms.TextAlignment formsAlignment) + { + switch(formsAlignment) + { + case Xamarin.Forms.TextAlignment.Start: + return GravityFlags.Left | GravityFlags.CenterVertical; + case Xamarin.Forms.TextAlignment.Center: + return GravityFlags.Center | GravityFlags.CenterVertical; + case Xamarin.Forms.TextAlignment.End: + return GravityFlags.Right | GravityFlags.CenterVertical; + default: + return GravityFlags.Right | GravityFlags.CenterVertical; + } + } + + public static RippleDrawable CreateRipple(Android.Graphics.Color color, Drawable background = null) + { + if(background == null) + { + var mask = new ColorDrawable(Android.Graphics.Color.White); + return new RippleDrawable(GetPressedColorSelector(color), null, mask); + } + return new RippleDrawable(GetPressedColorSelector(color), background, null); + } + + public static ColorStateList GetPressedColorSelector(int pressedColor) + { + return new ColorStateList( + new int[][] + { + new int[]{} + }, + new int[] + { + pressedColor, + }); + } + } +} diff --git a/src/Android/Resources/Resource.designer.cs b/src/Android/Resources/Resource.designer.cs index a8de379ee..91dd1286d 100644 --- a/src/Android/Resources/Resource.designer.cs +++ b/src/Android/Resources/Resource.designer.cs @@ -6534,9 +6534,39 @@ namespace Bit.Droid // aapt resource value: 0x7f0a003a public const int CTRL = 2131361850; + // aapt resource value: 0x7f0a00a9 + public const int CellAccessoryView = 2131361961; + + // aapt resource value: 0x7f0a00a5 + public const int CellBody = 2131361957; + + // aapt resource value: 0x7f0a00a6 + public const int CellContentStack = 2131361958; + + // aapt resource value: 0x7f0a00a8 + public const int CellDescription = 2131361960; + + // aapt resource value: 0x7f0a00a7 + public const int CellTitle = 2131361959; + + // aapt resource value: 0x7f0a00aa + public const int ContentCellBody = 2131361962; + + // aapt resource value: 0x7f0a00ab + public const int ContentCellBorder = 2131361963; + // aapt resource value: 0x7f0a003b public const int FUNCTION = 2131361851; + // aapt resource value: 0x7f0a00ba + public const int FooterCellText = 2131361978; + + // aapt resource value: 0x7f0a00bc + public const int HeaderCellBorder = 2131361980; + + // aapt resource value: 0x7f0a00bb + public const int HeaderCellText = 2131361979; + // aapt resource value: 0x7f0a003c public const int META = 2131361852; @@ -6546,8 +6576,8 @@ namespace Bit.Droid // aapt resource value: 0x7f0a003e public const int SYM = 2131361854; - // aapt resource value: 0x7f0a00e5 - public const int action0 = 2131362021; + // aapt resource value: 0x7f0a00ef + public const int action0 = 2131362031; // aapt resource value: 0x7f0a008d public const int action_bar = 2131361933; @@ -6570,17 +6600,17 @@ namespace Bit.Droid // aapt resource value: 0x7f0a0069 public const int action_bar_title = 2131361897; - // aapt resource value: 0x7f0a00e2 - public const int action_container = 2131362018; + // aapt resource value: 0x7f0a00ec + public const int action_container = 2131362028; // aapt resource value: 0x7f0a008e public const int action_context_bar = 2131361934; - // aapt resource value: 0x7f0a00e9 - public const int action_divider = 2131362025; + // aapt resource value: 0x7f0a00f3 + public const int action_divider = 2131362035; - // aapt resource value: 0x7f0a00e3 - public const int action_image = 2131362019; + // aapt resource value: 0x7f0a00ed + public const int action_image = 2131362029; // aapt resource value: 0x7f0a0003 public const int action_menu_divider = 2131361795; @@ -6597,11 +6627,11 @@ namespace Bit.Droid // aapt resource value: 0x7f0a006b public const int action_mode_close_button = 2131361899; - // aapt resource value: 0x7f0a00e4 - public const int action_text = 2131362020; + // aapt resource value: 0x7f0a00ee + public const int action_text = 2131362030; - // aapt resource value: 0x7f0a00f2 - public const int actions = 2131362034; + // aapt resource value: 0x7f0a00fc + public const int actions = 2131362044; // aapt resource value: 0x7f0a006c public const int activity_chooser_view_content = 2131361900; @@ -6657,8 +6687,8 @@ namespace Bit.Droid // aapt resource value: 0x7f0a0072 public const int buttonPanel = 2131361906; - // aapt resource value: 0x7f0a00e6 - public const int cancel_action = 2131362022; + // aapt resource value: 0x7f0a00f0 + public const int cancel_action = 2131362032; // aapt resource value: 0x7f0a004c public const int center = 2131361868; @@ -6672,8 +6702,8 @@ namespace Bit.Droid // aapt resource value: 0x7f0a0086 public const int checkbox = 2131361926; - // aapt resource value: 0x7f0a00ee - public const int chronometer = 2131362030; + // aapt resource value: 0x7f0a00f8 + public const int chronometer = 2131362040; // aapt resource value: 0x7f0a0061 public const int clip_horizontal = 2131361889; @@ -6684,8 +6714,8 @@ namespace Bit.Droid // aapt resource value: 0x7f0a0040 public const int collapseActionView = 2131361856; - // aapt resource value: 0x7f0a00a7 - public const int container = 2131361959; + // aapt resource value: 0x7f0a00ae + public const int container = 2131361966; // aapt resource value: 0x7f0a0082 public const int content = 2131361922; @@ -6693,8 +6723,8 @@ namespace Bit.Droid // aapt resource value: 0x7f0a0075 public const int contentPanel = 2131361909; - // aapt resource value: 0x7f0a00a8 - public const int coordinator = 2131361960; + // aapt resource value: 0x7f0a00af + public const int coordinator = 2131361967; // aapt resource value: 0x7f0a007c public const int custom = 2131361916; @@ -6708,20 +6738,20 @@ namespace Bit.Droid // aapt resource value: 0x7f0a006f public const int default_activity_button = 2131361903; - // aapt resource value: 0x7f0a00aa - public const int design_bottom_sheet = 2131361962; + // aapt resource value: 0x7f0a00b1 + public const int design_bottom_sheet = 2131361969; - // aapt resource value: 0x7f0a00af - public const int design_menu_item_action_area = 2131361967; + // aapt resource value: 0x7f0a00b6 + public const int design_menu_item_action_area = 2131361974; - // aapt resource value: 0x7f0a00ae - public const int design_menu_item_action_area_stub = 2131361966; + // aapt resource value: 0x7f0a00b5 + public const int design_menu_item_action_area_stub = 2131361973; - // aapt resource value: 0x7f0a00ad - public const int design_menu_item_text = 2131361965; + // aapt resource value: 0x7f0a00b4 + public const int design_menu_item_text = 2131361972; - // aapt resource value: 0x7f0a00ac - public const int design_navigation_view = 2131361964; + // aapt resource value: 0x7f0a00b3 + public const int design_navigation_view = 2131361971; // aapt resource value: 0x7f0a0027 public const int disableHome = 2131361831; @@ -6732,8 +6762,8 @@ namespace Bit.Droid // aapt resource value: 0x7f0a0037 public const int end = 2131361847; - // aapt resource value: 0x7f0a00f4 - public const int end_padder = 2131362036; + // aapt resource value: 0x7f0a00fe + public const int end_padder = 2131362046; // aapt resource value: 0x7f0a0046 public const int enterAlways = 2131361862; @@ -6765,11 +6795,11 @@ namespace Bit.Droid // aapt resource value: 0x7f0a005c public const int @fixed = 2131361884; - // aapt resource value: 0x7f0a00b1 - public const int flyoutcontent_appbar = 2131361969; + // aapt resource value: 0x7f0a00b8 + public const int flyoutcontent_appbar = 2131361976; - // aapt resource value: 0x7f0a00b2 - public const int flyoutcontent_recycler = 2131361970; + // aapt resource value: 0x7f0a00b9 + public const int flyoutcontent_recycler = 2131361977; // aapt resource value: 0x7f0a0067 public const int forever = 2131361895; @@ -6789,8 +6819,8 @@ namespace Bit.Droid // aapt resource value: 0x7f0a0071 public const int icon = 2131361905; - // aapt resource value: 0x7f0a00f3 - public const int icon_group = 2131362035; + // aapt resource value: 0x7f0a00fd + public const int icon_group = 2131362045; // aapt resource value: 0x7f0a0041 public const int ifRoom = 2131361857; @@ -6798,8 +6828,8 @@ namespace Bit.Droid // aapt resource value: 0x7f0a006e public const int image = 2131361902; - // aapt resource value: 0x7f0a00ef - public const int info = 2131362031; + // aapt resource value: 0x7f0a00f9 + public const int info = 2131362041; // aapt resource value: 0x7f0a0068 public const int italic = 2131361896; @@ -6810,8 +6840,8 @@ namespace Bit.Droid // aapt resource value: 0x7f0a004e public const int labeled = 2131361870; - // aapt resource value: 0x7f0a00a6 - public const int largeLabel = 2131361958; + // aapt resource value: 0x7f0a00ad + public const int largeLabel = 2131361965; // aapt resource value: 0x7f0a0054 public const int left = 2131361876; @@ -6828,23 +6858,23 @@ namespace Bit.Droid // aapt resource value: 0x7f0a0070 public const int list_item = 2131361904; - // aapt resource value: 0x7f0a00f5 - public const int main_appbar = 2131362037; - - // aapt resource value: 0x7f0a00f8 - public const int main_scrollview = 2131362040; - - // aapt resource value: 0x7f0a00f7 - public const int main_tablayout = 2131362039; - - // aapt resource value: 0x7f0a00f6 - public const int main_toolbar = 2131362038; - // aapt resource value: 0x7f0a00ff - public const int masked = 2131362047; + public const int main_appbar = 2131362047; - // aapt resource value: 0x7f0a00e8 - public const int media_actions = 2131362024; + // aapt resource value: 0x7f0a0102 + public const int main_scrollview = 2131362050; + + // aapt resource value: 0x7f0a0101 + public const int main_tablayout = 2131362049; + + // aapt resource value: 0x7f0a0100 + public const int main_toolbar = 2131362048; + + // aapt resource value: 0x7f0a0109 + public const int masked = 2131362057; + + // aapt resource value: 0x7f0a00f2 + public const int media_actions = 2131362034; // aapt resource value: 0x7f0a009c public const int message = 2131361948; @@ -6855,143 +6885,143 @@ namespace Bit.Droid // aapt resource value: 0x7f0a0059 public const int mini = 2131361881; - // aapt resource value: 0x7f0a00cf - public const int mr_art = 2131361999; - - // aapt resource value: 0x7f0a00c0 - public const int mr_cast_checkbox = 2131361984; - - // aapt resource value: 0x7f0a00b9 - public const int mr_cast_close_button = 2131361977; - - // aapt resource value: 0x7f0a00b4 - public const int mr_cast_group_icon = 2131361972; - - // aapt resource value: 0x7f0a00b5 - public const int mr_cast_group_name = 2131361973; - - // aapt resource value: 0x7f0a00b3 - public const int mr_cast_list = 2131361971; - - // aapt resource value: 0x7f0a00b8 - public const int mr_cast_meta = 2131361976; - - // aapt resource value: 0x7f0a00ba - public const int mr_cast_meta_art = 2131361978; - - // aapt resource value: 0x7f0a00bc - public const int mr_cast_meta_subtitle = 2131361980; - - // aapt resource value: 0x7f0a00bb - public const int mr_cast_meta_title = 2131361979; - - // aapt resource value: 0x7f0a00be - public const int mr_cast_route_icon = 2131361982; - - // aapt resource value: 0x7f0a00bf - public const int mr_cast_route_name = 2131361983; - - // aapt resource value: 0x7f0a00bd - public const int mr_cast_stop_button = 2131361981; - - // aapt resource value: 0x7f0a00c1 - public const int mr_cast_volume_layout = 2131361985; - - // aapt resource value: 0x7f0a00c2 - public const int mr_cast_volume_slider = 2131361986; - - // aapt resource value: 0x7f0a00c4 - public const int mr_chooser_list = 2131361988; - - // aapt resource value: 0x7f0a00c7 - public const int mr_chooser_route_desc = 2131361991; - - // aapt resource value: 0x7f0a00c5 - public const int mr_chooser_route_icon = 2131361989; - - // aapt resource value: 0x7f0a00c6 - public const int mr_chooser_route_name = 2131361990; - - // aapt resource value: 0x7f0a00c3 - public const int mr_chooser_title = 2131361987; - - // aapt resource value: 0x7f0a00cc - public const int mr_close = 2131361996; - - // aapt resource value: 0x7f0a00d2 - public const int mr_control_divider = 2131362002; - - // aapt resource value: 0x7f0a00dd - public const int mr_control_playback_ctrl = 2131362013; - - // aapt resource value: 0x7f0a00e0 - public const int mr_control_subtitle = 2131362016; - - // aapt resource value: 0x7f0a00df - public const int mr_control_title = 2131362015; - - // aapt resource value: 0x7f0a00de - public const int mr_control_title_container = 2131362014; - - // aapt resource value: 0x7f0a00cd - public const int mr_custom_control = 2131361997; - - // aapt resource value: 0x7f0a00ce - public const int mr_default_control = 2131361998; - - // aapt resource value: 0x7f0a00c9 - public const int mr_dialog_area = 2131361993; - - // aapt resource value: 0x7f0a00d8 - public const int mr_dialog_header_name = 2131362008; - - // aapt resource value: 0x7f0a00c8 - public const int mr_expandable_area = 2131361992; - - // aapt resource value: 0x7f0a00e1 - public const int mr_group_expand_collapse = 2131362017; - - // aapt resource value: 0x7f0a00b6 - public const int mr_group_volume_route_name = 2131361974; - - // aapt resource value: 0x7f0a00b7 - public const int mr_group_volume_slider = 2131361975; - - // aapt resource value: 0x7f0a00d0 - public const int mr_media_main_control = 2131362000; - - // aapt resource value: 0x7f0a00cb - public const int mr_name = 2131361995; - // aapt resource value: 0x7f0a00d9 - public const int mr_picker_close_button = 2131362009; - - // aapt resource value: 0x7f0a00da - public const int mr_picker_list = 2131362010; - - // aapt resource value: 0x7f0a00db - public const int mr_picker_route_icon = 2131362011; - - // aapt resource value: 0x7f0a00dc - public const int mr_picker_route_name = 2131362012; - - // aapt resource value: 0x7f0a00d1 - public const int mr_playback_control = 2131362001; + public const int mr_art = 2131362009; // aapt resource value: 0x7f0a00ca - public const int mr_title_bar = 2131361994; + public const int mr_cast_checkbox = 2131361994; - // aapt resource value: 0x7f0a00d3 - public const int mr_volume_control = 2131362003; + // aapt resource value: 0x7f0a00c3 + public const int mr_cast_close_button = 2131361987; - // aapt resource value: 0x7f0a00d4 - public const int mr_volume_group_list = 2131362004; + // aapt resource value: 0x7f0a00be + public const int mr_cast_group_icon = 2131361982; + + // aapt resource value: 0x7f0a00bf + public const int mr_cast_group_name = 2131361983; + + // aapt resource value: 0x7f0a00bd + public const int mr_cast_list = 2131361981; + + // aapt resource value: 0x7f0a00c2 + public const int mr_cast_meta = 2131361986; + + // aapt resource value: 0x7f0a00c4 + public const int mr_cast_meta_art = 2131361988; + + // aapt resource value: 0x7f0a00c6 + public const int mr_cast_meta_subtitle = 2131361990; + + // aapt resource value: 0x7f0a00c5 + public const int mr_cast_meta_title = 2131361989; + + // aapt resource value: 0x7f0a00c8 + public const int mr_cast_route_icon = 2131361992; + + // aapt resource value: 0x7f0a00c9 + public const int mr_cast_route_name = 2131361993; + + // aapt resource value: 0x7f0a00c7 + public const int mr_cast_stop_button = 2131361991; + + // aapt resource value: 0x7f0a00cb + public const int mr_cast_volume_layout = 2131361995; + + // aapt resource value: 0x7f0a00cc + public const int mr_cast_volume_slider = 2131361996; + + // aapt resource value: 0x7f0a00ce + public const int mr_chooser_list = 2131361998; + + // aapt resource value: 0x7f0a00d1 + public const int mr_chooser_route_desc = 2131362001; + + // aapt resource value: 0x7f0a00cf + public const int mr_chooser_route_icon = 2131361999; + + // aapt resource value: 0x7f0a00d0 + public const int mr_chooser_route_name = 2131362000; + + // aapt resource value: 0x7f0a00cd + public const int mr_chooser_title = 2131361997; // aapt resource value: 0x7f0a00d6 - public const int mr_volume_item_icon = 2131362006; + public const int mr_close = 2131362006; + + // aapt resource value: 0x7f0a00dc + public const int mr_control_divider = 2131362012; + + // aapt resource value: 0x7f0a00e7 + public const int mr_control_playback_ctrl = 2131362023; + + // aapt resource value: 0x7f0a00ea + public const int mr_control_subtitle = 2131362026; + + // aapt resource value: 0x7f0a00e9 + public const int mr_control_title = 2131362025; + + // aapt resource value: 0x7f0a00e8 + public const int mr_control_title_container = 2131362024; // aapt resource value: 0x7f0a00d7 - public const int mr_volume_slider = 2131362007; + public const int mr_custom_control = 2131362007; + + // aapt resource value: 0x7f0a00d8 + public const int mr_default_control = 2131362008; + + // aapt resource value: 0x7f0a00d3 + public const int mr_dialog_area = 2131362003; + + // aapt resource value: 0x7f0a00e2 + public const int mr_dialog_header_name = 2131362018; + + // aapt resource value: 0x7f0a00d2 + public const int mr_expandable_area = 2131362002; + + // aapt resource value: 0x7f0a00eb + public const int mr_group_expand_collapse = 2131362027; + + // aapt resource value: 0x7f0a00c0 + public const int mr_group_volume_route_name = 2131361984; + + // aapt resource value: 0x7f0a00c1 + public const int mr_group_volume_slider = 2131361985; + + // aapt resource value: 0x7f0a00da + public const int mr_media_main_control = 2131362010; + + // aapt resource value: 0x7f0a00d5 + public const int mr_name = 2131362005; + + // aapt resource value: 0x7f0a00e3 + public const int mr_picker_close_button = 2131362019; + + // aapt resource value: 0x7f0a00e4 + public const int mr_picker_list = 2131362020; + + // aapt resource value: 0x7f0a00e5 + public const int mr_picker_route_icon = 2131362021; + + // aapt resource value: 0x7f0a00e6 + public const int mr_picker_route_name = 2131362022; + + // aapt resource value: 0x7f0a00db + public const int mr_playback_control = 2131362011; + + // aapt resource value: 0x7f0a00d4 + public const int mr_title_bar = 2131362004; + + // aapt resource value: 0x7f0a00dd + public const int mr_volume_control = 2131362013; + + // aapt resource value: 0x7f0a00de + public const int mr_volume_group_list = 2131362014; + + // aapt resource value: 0x7f0a00e0 + public const int mr_volume_item_icon = 2131362016; + + // aapt resource value: 0x7f0a00e1 + public const int mr_volume_slider = 2131362017; // aapt resource value: 0x7f0a0014 public const int mtrl_child_content_container = 2131361812; @@ -7002,8 +7032,8 @@ namespace Bit.Droid // aapt resource value: 0x7f0a002f public const int multiply = 2131361839; - // aapt resource value: 0x7f0a00ab - public const int navigation_header_container = 2131361963; + // aapt resource value: 0x7f0a00b2 + public const int navigation_header_container = 2131361970; // aapt resource value: 0x7f0a0042 public const int never = 2131361858; @@ -7014,14 +7044,14 @@ namespace Bit.Droid // aapt resource value: 0x7f0a0025 public const int normal = 2131361829; - // aapt resource value: 0x7f0a00f1 - public const int notification_background = 2131362033; + // aapt resource value: 0x7f0a00fb + public const int notification_background = 2131362043; - // aapt resource value: 0x7f0a00eb - public const int notification_main_column = 2131362027; + // aapt resource value: 0x7f0a00f5 + public const int notification_main_column = 2131362037; - // aapt resource value: 0x7f0a00ea - public const int notification_main_column_container = 2131362026; + // aapt resource value: 0x7f0a00f4 + public const int notification_main_column_container = 2131362036; // aapt resource value: 0x7f0a0060 public const int outline = 2131361888; @@ -7050,11 +7080,11 @@ namespace Bit.Droid // aapt resource value: 0x7f0a0055 public const int right = 2131361877; - // aapt resource value: 0x7f0a00f0 - public const int right_icon = 2131362032; + // aapt resource value: 0x7f0a00fa + public const int right_icon = 2131362042; - // aapt resource value: 0x7f0a00ec - public const int right_side = 2131362028; + // aapt resource value: 0x7f0a00f6 + public const int right_side = 2131362038; // aapt resource value: 0x7f0a000c public const int save_image_matrix = 2131361804; @@ -7119,14 +7149,14 @@ namespace Bit.Droid // aapt resource value: 0x7f0a004f public const int selected = 2131361871; - // aapt resource value: 0x7f0a00f9 - public const int shellcontent_appbar = 2131362041; + // aapt resource value: 0x7f0a0103 + public const int shellcontent_appbar = 2131362051; - // aapt resource value: 0x7f0a00fb - public const int shellcontent_scrollview = 2131362043; + // aapt resource value: 0x7f0a0105 + public const int shellcontent_scrollview = 2131362053; - // aapt resource value: 0x7f0a00fa - public const int shellcontent_toolbar = 2131362042; + // aapt resource value: 0x7f0a0104 + public const int shellcontent_toolbar = 2131362052; // aapt resource value: 0x7f0a0083 public const int shortcut = 2131361923; @@ -7140,11 +7170,11 @@ namespace Bit.Droid // aapt resource value: 0x7f0a002c public const int showTitle = 2131361836; - // aapt resource value: 0x7f0a00fc - public const int sliding_tabs = 2131362044; + // aapt resource value: 0x7f0a0106 + public const int sliding_tabs = 2131362054; - // aapt resource value: 0x7f0a00a5 - public const int smallLabel = 2131361957; + // aapt resource value: 0x7f0a00ac + public const int smallLabel = 2131361964; // aapt resource value: 0x7f0a0016 public const int snackbar_action = 2131361814; @@ -7176,8 +7206,8 @@ namespace Bit.Droid // aapt resource value: 0x7f0a0056 public const int start = 2131361878; - // aapt resource value: 0x7f0a00e7 - public const int status_bar_latest_event_content = 2131362023; + // aapt resource value: 0x7f0a00f1 + public const int status_bar_latest_event_content = 2131362033; // aapt resource value: 0x7f0a005b public const int stretch = 2131361883; @@ -7218,8 +7248,8 @@ namespace Bit.Droid // aapt resource value: 0x7f0a005a public const int textStart = 2131361882; - // aapt resource value: 0x7f0a00b0 - public const int text_input_password_toggle = 2131361968; + // aapt resource value: 0x7f0a00b7 + public const int text_input_password_toggle = 2131361975; // aapt resource value: 0x7f0a0018 public const int textinput_counter = 2131361816; @@ -7230,8 +7260,8 @@ namespace Bit.Droid // aapt resource value: 0x7f0a001a public const int textinput_helper_text = 2131361818; - // aapt resource value: 0x7f0a00ed - public const int time = 2131362029; + // aapt resource value: 0x7f0a00f7 + public const int time = 2131362039; // aapt resource value: 0x7f0a0023 public const int title = 2131361827; @@ -7242,8 +7272,8 @@ namespace Bit.Droid // aapt resource value: 0x7f0a007e public const int title_template = 2131361918; - // aapt resource value: 0x7f0a00fd - public const int toolbar = 2131362045; + // aapt resource value: 0x7f0a0107 + public const int toolbar = 2131362055; // aapt resource value: 0x7f0a0045 public const int top = 2131361861; @@ -7251,8 +7281,8 @@ namespace Bit.Droid // aapt resource value: 0x7f0a007d public const int topPanel = 2131361917; - // aapt resource value: 0x7f0a00a9 - public const int touch_outside = 2131361961; + // aapt resource value: 0x7f0a00b0 + public const int touch_outside = 2131361968; // aapt resource value: 0x7f0a000f public const int transition_current_scene = 2131361807; @@ -7284,11 +7314,11 @@ namespace Bit.Droid // aapt resource value: 0x7f0a001b public const int view_offset_helper = 2131361819; - // aapt resource value: 0x7f0a00fe - public const int visible = 2131362046; + // aapt resource value: 0x7f0a0108 + public const int visible = 2131362056; - // aapt resource value: 0x7f0a00d5 - public const int volume_item_container = 2131362005; + // aapt resource value: 0x7f0a00df + public const int volume_item_container = 2131362015; // aapt resource value: 0x7f0a0043 public const int withText = 2131361859; @@ -7507,166 +7537,178 @@ namespace Bit.Droid public const int browser_actions_context_menu_row = 2130968607; // aapt resource value: 0x7f040020 - public const int design_bottom_navigation_item = 2130968608; + public const int CellBaseView = 2130968608; // aapt resource value: 0x7f040021 - public const int design_bottom_sheet_dialog = 2130968609; + public const int ContentCell = 2130968609; // aapt resource value: 0x7f040022 - public const int design_layout_snackbar = 2130968610; + public const int design_bottom_navigation_item = 2130968610; // aapt resource value: 0x7f040023 - public const int design_layout_snackbar_include = 2130968611; + public const int design_bottom_sheet_dialog = 2130968611; // aapt resource value: 0x7f040024 - public const int design_layout_tab_icon = 2130968612; + public const int design_layout_snackbar = 2130968612; // aapt resource value: 0x7f040025 - public const int design_layout_tab_text = 2130968613; + public const int design_layout_snackbar_include = 2130968613; // aapt resource value: 0x7f040026 - public const int design_menu_item_action_area = 2130968614; + public const int design_layout_tab_icon = 2130968614; // aapt resource value: 0x7f040027 - public const int design_navigation_item = 2130968615; + public const int design_layout_tab_text = 2130968615; // aapt resource value: 0x7f040028 - public const int design_navigation_item_header = 2130968616; + public const int design_menu_item_action_area = 2130968616; // aapt resource value: 0x7f040029 - public const int design_navigation_item_separator = 2130968617; + public const int design_navigation_item = 2130968617; // aapt resource value: 0x7f04002a - public const int design_navigation_item_subheader = 2130968618; + public const int design_navigation_item_header = 2130968618; // aapt resource value: 0x7f04002b - public const int design_navigation_menu = 2130968619; + public const int design_navigation_item_separator = 2130968619; // aapt resource value: 0x7f04002c - public const int design_navigation_menu_item = 2130968620; + public const int design_navigation_item_subheader = 2130968620; // aapt resource value: 0x7f04002d - public const int design_text_input_password_icon = 2130968621; + public const int design_navigation_menu = 2130968621; // aapt resource value: 0x7f04002e - public const int FlyoutContent = 2130968622; + public const int design_navigation_menu_item = 2130968622; // aapt resource value: 0x7f04002f - public const int mr_cast_dialog = 2130968623; + public const int design_text_input_password_icon = 2130968623; // aapt resource value: 0x7f040030 - public const int mr_cast_group_item = 2130968624; + public const int FlyoutContent = 2130968624; // aapt resource value: 0x7f040031 - public const int mr_cast_group_volume_item = 2130968625; + public const int FooterCell = 2130968625; // aapt resource value: 0x7f040032 - public const int mr_cast_media_metadata = 2130968626; + public const int HeaderCell = 2130968626; // aapt resource value: 0x7f040033 - public const int mr_cast_route_item = 2130968627; + public const int mr_cast_dialog = 2130968627; // aapt resource value: 0x7f040034 - public const int mr_chooser_dialog = 2130968628; + public const int mr_cast_group_item = 2130968628; // aapt resource value: 0x7f040035 - public const int mr_chooser_list_item = 2130968629; + public const int mr_cast_group_volume_item = 2130968629; // aapt resource value: 0x7f040036 - public const int mr_controller_material_dialog_b = 2130968630; + public const int mr_cast_media_metadata = 2130968630; // aapt resource value: 0x7f040037 - public const int mr_controller_volume_item = 2130968631; + public const int mr_cast_route_item = 2130968631; // aapt resource value: 0x7f040038 - public const int mr_dialog_header_item = 2130968632; + public const int mr_chooser_dialog = 2130968632; // aapt resource value: 0x7f040039 - public const int mr_picker_dialog = 2130968633; + public const int mr_chooser_list_item = 2130968633; // aapt resource value: 0x7f04003a - public const int mr_picker_route_item = 2130968634; + public const int mr_controller_material_dialog_b = 2130968634; // aapt resource value: 0x7f04003b - public const int mr_playback_control = 2130968635; + public const int mr_controller_volume_item = 2130968635; // aapt resource value: 0x7f04003c - public const int mr_volume_control = 2130968636; + public const int mr_dialog_header_item = 2130968636; // aapt resource value: 0x7f04003d - public const int mtrl_layout_snackbar = 2130968637; + public const int mr_picker_dialog = 2130968637; // aapt resource value: 0x7f04003e - public const int mtrl_layout_snackbar_include = 2130968638; + public const int mr_picker_route_item = 2130968638; // aapt resource value: 0x7f04003f - public const int notification_action = 2130968639; + public const int mr_playback_control = 2130968639; // aapt resource value: 0x7f040040 - public const int notification_action_tombstone = 2130968640; + public const int mr_volume_control = 2130968640; // aapt resource value: 0x7f040041 - public const int notification_media_action = 2130968641; + public const int mtrl_layout_snackbar = 2130968641; // aapt resource value: 0x7f040042 - public const int notification_media_cancel_action = 2130968642; + public const int mtrl_layout_snackbar_include = 2130968642; // aapt resource value: 0x7f040043 - public const int notification_template_big_media = 2130968643; + public const int notification_action = 2130968643; // aapt resource value: 0x7f040044 - public const int notification_template_big_media_custom = 2130968644; + public const int notification_action_tombstone = 2130968644; // aapt resource value: 0x7f040045 - public const int notification_template_big_media_narrow = 2130968645; + public const int notification_media_action = 2130968645; // aapt resource value: 0x7f040046 - public const int notification_template_big_media_narrow_custom = 2130968646; + public const int notification_media_cancel_action = 2130968646; // aapt resource value: 0x7f040047 - public const int notification_template_custom_big = 2130968647; + public const int notification_template_big_media = 2130968647; // aapt resource value: 0x7f040048 - public const int notification_template_icon_group = 2130968648; + public const int notification_template_big_media_custom = 2130968648; // aapt resource value: 0x7f040049 - public const int notification_template_lines_media = 2130968649; + public const int notification_template_big_media_narrow = 2130968649; // aapt resource value: 0x7f04004a - public const int notification_template_media = 2130968650; + public const int notification_template_big_media_narrow_custom = 2130968650; // aapt resource value: 0x7f04004b - public const int notification_template_media_custom = 2130968651; + public const int notification_template_custom_big = 2130968651; // aapt resource value: 0x7f04004c - public const int notification_template_part_chronometer = 2130968652; + public const int notification_template_icon_group = 2130968652; // aapt resource value: 0x7f04004d - public const int notification_template_part_time = 2130968653; + public const int notification_template_lines_media = 2130968653; // aapt resource value: 0x7f04004e - public const int RootLayout = 2130968654; + public const int notification_template_media = 2130968654; // aapt resource value: 0x7f04004f - public const int select_dialog_item_material = 2130968655; + public const int notification_template_media_custom = 2130968655; // aapt resource value: 0x7f040050 - public const int select_dialog_multichoice_material = 2130968656; + public const int notification_template_part_chronometer = 2130968656; // aapt resource value: 0x7f040051 - public const int select_dialog_singlechoice_material = 2130968657; + public const int notification_template_part_time = 2130968657; // aapt resource value: 0x7f040052 - public const int ShellContent = 2130968658; + public const int RootLayout = 2130968658; // aapt resource value: 0x7f040053 - public const int support_simple_spinner_dropdown_item = 2130968659; + public const int select_dialog_item_material = 2130968659; // aapt resource value: 0x7f040054 - public const int Tabbar = 2130968660; + public const int select_dialog_multichoice_material = 2130968660; // aapt resource value: 0x7f040055 - public const int Toolbar = 2130968661; + public const int select_dialog_singlechoice_material = 2130968661; + + // aapt resource value: 0x7f040056 + public const int ShellContent = 2130968662; + + // aapt resource value: 0x7f040057 + public const int support_simple_spinner_dropdown_item = 2130968663; + + // aapt resource value: 0x7f040058 + public const int Tabbar = 2130968664; + + // aapt resource value: 0x7f040059 + public const int Toolbar = 2130968665; static Layout() { diff --git a/src/Android/Resources/layout/CellBaseView.axml b/src/Android/Resources/layout/CellBaseView.axml new file mode 100644 index 000000000..384a44632 --- /dev/null +++ b/src/Android/Resources/layout/CellBaseView.axml @@ -0,0 +1,50 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/Android/Resources/layout/ContentCell.axml b/src/Android/Resources/layout/ContentCell.axml new file mode 100644 index 000000000..4aa65924c --- /dev/null +++ b/src/Android/Resources/layout/ContentCell.axml @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/src/Android/Resources/layout/FooterCell.axml b/src/Android/Resources/layout/FooterCell.axml new file mode 100644 index 000000000..09ca8cb59 --- /dev/null +++ b/src/Android/Resources/layout/FooterCell.axml @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/src/Android/Resources/layout/HeaderCell.axml b/src/Android/Resources/layout/HeaderCell.axml new file mode 100644 index 000000000..562cf5c62 --- /dev/null +++ b/src/Android/Resources/layout/HeaderCell.axml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/src/App/Controls/BoxedView/BoxedModel.cs b/src/App/Controls/BoxedView/BoxedModel.cs new file mode 100644 index 000000000..af478a454 --- /dev/null +++ b/src/App/Controls/BoxedView/BoxedModel.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xamarin.Forms; +using Xamarin.Forms.Internals; + +namespace Bit.App.Controls.BoxedView +{ + public class BoxedModel : TableModel + { + private static readonly BindableProperty PathProperty = BindableProperty.Create( + "Path", typeof(Tuple), typeof(Cell), null); + + private BoxedRoot _root; + private IEnumerable _visibleSections; + + public BoxedModel(BoxedRoot root) + { + _root = root; + _visibleSections = _root.Where(x => x.IsVisible); + } + + public override Cell GetCell(int section, int row) + { + var cell = (Cell)GetItem(section, row); + SetPath(cell, new Tuple(section, row)); + return cell; + } + + public override object GetItem(int section, int row) + { + return _visibleSections.ElementAt(section)[row]; + } + + public override int GetRowCount(int section) + { + return _visibleSections.ElementAt(section).Count; + } + + public override int GetSectionCount() + { + return _visibleSections.Count(); + } + + public virtual BoxedSection GetSection(int section) + { + return _visibleSections.ElementAtOrDefault(section); + } + + public override string GetSectionTitle(int section) + { + return _visibleSections.ElementAt(section).Title; + } + + public virtual string GetFooterText(int section) + { + return _visibleSections.ElementAt(section).FooterText; + } + + protected override void OnRowSelected(object item) + { + base.OnRowSelected(item); + (item as BaseCell)?.OnTapped(); + } + + public virtual double GetHeaderHeight(int section) + { + return _visibleSections.ElementAt(section).HeaderHeight; + } + + public static Tuple GetPath(Cell item) + { + if(item == null) + { + throw new ArgumentNullException(nameof(item)); + } + return item.GetValue(PathProperty) as Tuple; + } + + private static void SetPath(Cell item, Tuple index) + { + item?.SetValue(PathProperty, index); + } + } +} diff --git a/src/App/Controls/BoxedView/BoxedRoot.cs b/src/App/Controls/BoxedView/BoxedRoot.cs new file mode 100644 index 000000000..3f061f4ee --- /dev/null +++ b/src/App/Controls/BoxedView/BoxedRoot.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Specialized; +using System.ComponentModel; +using Xamarin.Forms; + +namespace Bit.App.Controls.BoxedView +{ + public class BoxedRoot : TableSectionBase + { + public BoxedRoot() + { + SetupEvents(); + } + + public event EventHandler SectionCollectionChanged; + + private void ChildCollectionChanged(object sender, NotifyCollectionChangedEventArgs args) + { + SectionCollectionChanged?.Invoke(this, EventArgs.Empty); + } + + private void ChildPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if(e.PropertyName == TitleProperty.PropertyName) + { + OnPropertyChanged(TitleProperty.PropertyName); + } + else if(e.PropertyName == BoxedSection.FooterTextProperty.PropertyName) + { + OnPropertyChanged(BoxedSection.FooterTextProperty.PropertyName); + } + else if(e.PropertyName == BoxedSection.IsVisibleProperty.PropertyName) + { + OnPropertyChanged(BoxedSection.IsVisibleProperty.PropertyName); + } + } + + private void SetupEvents() + { + CollectionChanged += (sender, args) => + { + if(args.NewItems != null) + { + foreach(BoxedSection section in args.NewItems) + { + section.CollectionChanged += ChildCollectionChanged; + section.PropertyChanged += ChildPropertyChanged; + } + } + if(args.OldItems != null) + { + foreach(BoxedSection section in args.OldItems) + { + section.CollectionChanged -= ChildCollectionChanged; + section.PropertyChanged -= ChildPropertyChanged; + } + } + }; + } + } +} diff --git a/src/App/Controls/BoxedView/BoxedSection.cs b/src/App/Controls/BoxedView/BoxedSection.cs new file mode 100644 index 000000000..a04cb4622 --- /dev/null +++ b/src/App/Controls/BoxedView/BoxedSection.cs @@ -0,0 +1,147 @@ +using System.Collections; +using System.Collections.Specialized; +using Xamarin.Forms; + +namespace Bit.App.Controls.BoxedView +{ + public class BoxedSection : TableSectionBase + { + public static BindableProperty IsVisibleProperty = BindableProperty.Create( + nameof(IsVisible), typeof(bool), typeof(BoxedSection), true, defaultBindingMode: BindingMode.OneWay); + + public static BindableProperty FooterTextProperty = BindableProperty.Create( + nameof(FooterText), typeof(string), typeof(BoxedSection), default(string), + defaultBindingMode: BindingMode.OneWay); + + public static BindableProperty ItemTemplateProperty = BindableProperty.Create( + nameof(ItemTemplate), typeof(DataTemplate), typeof(BoxedSection), default(DataTemplate), + defaultBindingMode: BindingMode.OneWay); + + public static BindableProperty ItemsSourceProperty = BindableProperty.Create( + nameof(ItemsSource), typeof(IList), typeof(BoxedSection), default(IList), + defaultBindingMode: BindingMode.OneWay, propertyChanged: ItemsChanged); + + public static BindableProperty HeaderHeightProperty = BindableProperty.Create( + nameof(HeaderHeight), typeof(double), typeof(BoxedSection), -1d, defaultBindingMode: BindingMode.OneWay); + + public static BindableProperty UseDragSortProperty = BindableProperty.Create( + nameof(UseDragSort), typeof(bool), typeof(BoxedSection), false, defaultBindingMode: BindingMode.OneWay); + + public BoxedSection() + { } + + public BoxedSection(string title) + : base(title) + { } + + public bool IsVisible + { + get => (bool)GetValue(IsVisibleProperty); + set => SetValue(IsVisibleProperty, value); + } + + public string FooterText + { + get => (string)GetValue(FooterTextProperty); + set => SetValue(FooterTextProperty, value); + } + + public DataTemplate ItemTemplate + { + get => (DataTemplate)GetValue(ItemTemplateProperty); + set => SetValue(ItemTemplateProperty, value); + } + + public IList ItemsSource + { + get => (IList)GetValue(ItemsSourceProperty); + set => SetValue(ItemsSourceProperty, value); + } + + public double HeaderHeight + { + get => (double)GetValue(HeaderHeightProperty); + set => SetValue(HeaderHeightProperty, value); + } + + public bool UseDragSort + { + get => (bool)GetValue(UseDragSortProperty); + set => SetValue(UseDragSortProperty, value); + } + + private static void ItemsChanged(BindableObject bindable, object oldValue, object newValue) + { + var section = bindable as BoxedSection; + if(section.ItemTemplate == null) + { + return; + } + + if(oldValue is INotifyCollectionChanged oldObservableCollection) + { + oldObservableCollection.CollectionChanged -= section.OnItemsSourceCollectionChanged; + } + if(newValue is INotifyCollectionChanged newObservableCollection) + { + newObservableCollection.CollectionChanged += section.OnItemsSourceCollectionChanged; + } + + section.Clear(); + + if(newValue is IList newValueAsEnumerable) + { + foreach(var item in newValueAsEnumerable) + { + var view = CreateChildViewFor(section.ItemTemplate, item, section); + section.Add(view); + } + } + } + + private void OnItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if(e.Action == NotifyCollectionChangedAction.Replace) + { + RemoveAt(e.OldStartingIndex); + var item = e.NewItems[e.NewStartingIndex]; + var view = CreateChildViewFor(ItemTemplate, item, this); + Insert(e.NewStartingIndex, view); + } + else if(e.Action == NotifyCollectionChangedAction.Add) + { + if(e.NewItems != null) + { + for(var i = 0; i < e.NewItems.Count; ++i) + { + var item = e.NewItems[i]; + var view = CreateChildViewFor(ItemTemplate, item, this); + Insert(i + e.NewStartingIndex, view); + } + } + } + else if(e.Action == NotifyCollectionChangedAction.Remove) + { + if(e.OldItems != null) + { + RemoveAt(e.OldStartingIndex); + } + } + else if(e.Action == NotifyCollectionChangedAction.Reset) + { + Clear(); + } + } + + private static Cell CreateChildViewFor(DataTemplate template, object item, BindableObject container) + { + if(template is DataTemplateSelector selector) + { + template = selector.SelectTemplate(item, container); + } + // Binding context + template.SetValue(BindingContextProperty, item); + return template.CreateContent() as Cell; + } + } +} diff --git a/src/App/Controls/BoxedView/BoxedView.cs b/src/App/Controls/BoxedView/BoxedView.cs new file mode 100644 index 000000000..8fd14d4bf --- /dev/null +++ b/src/App/Controls/BoxedView/BoxedView.cs @@ -0,0 +1,602 @@ +using System; +using System.Collections; +using System.Collections.Specialized; +using System.Linq; +using Xamarin.Forms; + +namespace Bit.App.Controls.BoxedView +{ + [ContentProperty("Root")] + public class BoxedView : TableView + { + private BoxedRoot _root; + + public new event EventHandler ModelChanged; + + public BoxedView() + { + VerticalOptions = HorizontalOptions = LayoutOptions.FillAndExpand; + Root = new BoxedRoot(); + Model = new BoxedModel(Root); + } + + public new BoxedModel Model { get; set; } + + public new BoxedRoot Root + { + get => _root; + set + { + if(_root != null) + { + _root.PropertyChanged -= RootOnPropertyChanged; + _root.CollectionChanged -= OnCollectionChanged; + _root.SectionCollectionChanged -= OnSectionCollectionChanged; + } + + _root = value; + + // Transfer binding context to the children (maybe...) + SetInheritedBindingContext(_root, BindingContext); + + _root.PropertyChanged += RootOnPropertyChanged; + _root.CollectionChanged += OnCollectionChanged; + _root.SectionCollectionChanged += OnSectionCollectionChanged; + } + } + + protected override void OnBindingContextChanged() + { + base.OnBindingContextChanged(); + if(Root != null) + { + SetInheritedBindingContext(Root, BindingContext); + } + } + + private void RootOnPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if(e.PropertyName == TableSectionBase.TitleProperty.PropertyName || + e.PropertyName == BoxedSection.FooterTextProperty.PropertyName || + e.PropertyName == BoxedSection.IsVisibleProperty.PropertyName) + { + OnModelChanged(); + } + } + + protected override void OnPropertyChanged(string propertyName = null) + { + base.OnPropertyChanged(propertyName); + var changed = propertyName == HasUnevenRowsProperty.PropertyName || + propertyName == HeaderHeightProperty.PropertyName || + propertyName == HeaderFontSizeProperty.PropertyName || + propertyName == HeaderTextColorProperty.PropertyName || + propertyName == HeaderBackgroundColorProperty.PropertyName || + propertyName == HeaderTextVerticalAlignProperty.PropertyName || + propertyName == HeaderPaddingProperty.PropertyName || + propertyName == FooterFontSizeProperty.PropertyName || + propertyName == FooterTextColorProperty.PropertyName || + propertyName == FooterBackgroundColorProperty.PropertyName || + propertyName == FooterPaddingProperty.PropertyName; + if(changed) + { + OnModelChanged(); + } + } + + public void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + OnModelChanged(); + } + + public void OnSectionCollectionChanged(object sender, EventArgs childCollectionChangedEventArgs) + { + OnModelChanged(); + } + + protected new void OnModelChanged() + { + var cells = Root?.SelectMany(r => r); + if(cells == null) + { + return; + } + foreach(var cell in cells) + { + cell.Parent = this; + } + ModelChanged?.Invoke(this, EventArgs.Empty); + } + + // Make the unnecessary property existing at TableView sealed. + private new int Intent { get; set; } + + public static new BindableProperty BackgroundColorProperty = + BindableProperty.Create( + nameof(BackgroundColor), + typeof(Color), + typeof(BoxedView), + default(Color), + defaultBindingMode: BindingMode.OneWay + ); + + public new Color BackgroundColor + { + get { return (Color)GetValue(BackgroundColorProperty); } + set { SetValue(BackgroundColorProperty, value); } + } + + public static BindableProperty SeparatorColorProperty = + BindableProperty.Create( + nameof(SeparatorColor), + typeof(Color), + typeof(BoxedView), + Color.FromRgb(199, 199, 204), + defaultBindingMode: BindingMode.OneWay + ); + + public Color SeparatorColor + { + get { return (Color)GetValue(SeparatorColorProperty); } + set { SetValue(SeparatorColorProperty, value); } + } + + public static BindableProperty SelectedColorProperty = + BindableProperty.Create( + nameof(SelectedColor), + typeof(Color), + typeof(BoxedView), + default(Color), + defaultBindingMode: BindingMode.OneWay + ); + + public Color SelectedColor + { + get { return (Color)GetValue(SelectedColorProperty); } + set { SetValue(SelectedColorProperty, value); } + } + + public static BindableProperty HeaderPaddingProperty = + BindableProperty.Create( + nameof(HeaderPadding), + typeof(Thickness), + typeof(BoxedView), + new Thickness(14, 8, 8, 8), + defaultBindingMode: BindingMode.OneWay + ); + + public Thickness HeaderPadding + { + get { return (Thickness)GetValue(HeaderPaddingProperty); } + set { SetValue(HeaderPaddingProperty, value); } + } + + public static BindableProperty HeaderTextColorProperty = + BindableProperty.Create( + nameof(HeaderTextColor), + typeof(Color), + typeof(BoxedView), + default(Color), + defaultBindingMode: BindingMode.OneWay + ); + + public Color HeaderTextColor + { + get { return (Color)GetValue(HeaderTextColorProperty); } + set { SetValue(HeaderTextColorProperty, value); } + } + + public static BindableProperty HeaderFontSizeProperty = + BindableProperty.Create( + nameof(HeaderFontSize), + typeof(double), + typeof(BoxedView), + -1.0d, + defaultBindingMode: BindingMode.OneWay, + defaultValueCreator: bindable => Device.GetNamedSize(NamedSize.Small, (BoxedView)bindable) + ); + + [TypeConverter(typeof(FontSizeConverter))] + public double HeaderFontSize + { + get { return (double)GetValue(HeaderFontSizeProperty); } + set { SetValue(HeaderFontSizeProperty, value); } + } + + public static BindableProperty HeaderTextVerticalAlignProperty = + BindableProperty.Create( + nameof(HeaderTextVerticalAlign), + typeof(LayoutAlignment), + typeof(BoxedView), + LayoutAlignment.End, + defaultBindingMode: BindingMode.OneWay + ); + + public LayoutAlignment HeaderTextVerticalAlign + { + get { return (LayoutAlignment)GetValue(HeaderTextVerticalAlignProperty); } + set { SetValue(HeaderTextVerticalAlignProperty, value); } + } + + public static BindableProperty HeaderBackgroundColorProperty = + BindableProperty.Create( + nameof(HeaderBackgroundColor), + typeof(Color), + typeof(BoxedView), + default(Color), + defaultBindingMode: BindingMode.OneWay + ); + + public Color HeaderBackgroundColor + { + get { return (Color)GetValue(HeaderBackgroundColorProperty); } + set { SetValue(HeaderBackgroundColorProperty, value); } + } + + public static BindableProperty HeaderHeightProperty = + BindableProperty.Create( + nameof(HeaderHeight), + typeof(double), + typeof(BoxedView), + -1d, + defaultBindingMode: BindingMode.OneWay + ); + + public double HeaderHeight + { + get { return (double)GetValue(HeaderHeightProperty); } + set { SetValue(HeaderHeightProperty, value); } + } + + public static BindableProperty FooterTextColorProperty = + BindableProperty.Create( + nameof(FooterTextColor), + typeof(Color), + typeof(BoxedView), + default(Color), + defaultBindingMode: BindingMode.OneWay + ); + + public Color FooterTextColor + { + get { return (Color)GetValue(FooterTextColorProperty); } + set { SetValue(FooterTextColorProperty, value); } + } + + public static BindableProperty FooterFontSizeProperty = + BindableProperty.Create( + nameof(FooterFontSize), + typeof(double), + typeof(BoxedView), + -1.0d, + defaultBindingMode: BindingMode.OneWay, + defaultValueCreator: bindable => Device.GetNamedSize(NamedSize.Small, (BoxedView)bindable) + ); + + [TypeConverter(typeof(FontSizeConverter))] + public double FooterFontSize + { + get { return (double)GetValue(FooterFontSizeProperty); } + set { SetValue(FooterFontSizeProperty, value); } + } + + public static BindableProperty FooterBackgroundColorProperty = + BindableProperty.Create( + nameof(FooterBackgroundColor), + typeof(Color), + typeof(BoxedView), + default(Color), + defaultBindingMode: BindingMode.OneWay + ); + + public Color FooterBackgroundColor + { + get { return (Color)GetValue(FooterBackgroundColorProperty); } + set { SetValue(FooterBackgroundColorProperty, value); } + } + + public static BindableProperty FooterPaddingProperty = + BindableProperty.Create( + nameof(FooterPadding), + typeof(Thickness), + typeof(BoxedView), + new Thickness(14, 8, 14, 8), + defaultBindingMode: BindingMode.OneWay + ); + + public Thickness FooterPadding + { + get { return (Thickness)GetValue(FooterPaddingProperty); } + set { SetValue(FooterPaddingProperty, value); } + } + + public static BindableProperty CellTitleColorProperty = + BindableProperty.Create( + nameof(CellTitleColor), + typeof(Color), + typeof(BoxedView), + default(Color), + defaultBindingMode: BindingMode.OneWay + ); + + public Color CellTitleColor + { + get { return (Color)GetValue(CellTitleColorProperty); } + set { SetValue(CellTitleColorProperty, value); } + } + + public static BindableProperty CellTitleFontSizeProperty = + BindableProperty.Create( + nameof(CellTitleFontSize), + typeof(double), + typeof(BoxedView), + -1.0, + defaultBindingMode: BindingMode.OneWay, + defaultValueCreator: bindable => Device.GetNamedSize(NamedSize.Default, (BoxedView)bindable) + ); + + + [TypeConverter(typeof(FontSizeConverter))] + public double CellTitleFontSize + { + get { return (double)GetValue(CellTitleFontSizeProperty); } + set { SetValue(CellTitleFontSizeProperty, value); } + } + + public static BindableProperty CellValueTextColorProperty = + BindableProperty.Create( + nameof(CellValueTextColor), + typeof(Color), + typeof(BoxedView), + default(Color), + defaultBindingMode: BindingMode.OneWay + ); + + public Color CellValueTextColor + { + get { return (Color)GetValue(CellValueTextColorProperty); } + set { SetValue(CellValueTextColorProperty, value); } + } + + public static BindableProperty CellValueTextFontSizeProperty = + BindableProperty.Create( + nameof(CellValueTextFontSize), + typeof(double), + typeof(BoxedView), + -1.0d, + defaultBindingMode: BindingMode.OneWay + ); + + [TypeConverter(typeof(FontSizeConverter))] + public double CellValueTextFontSize + { + get { return (double)GetValue(CellValueTextFontSizeProperty); } + set { SetValue(CellValueTextFontSizeProperty, value); } + } + + public static BindableProperty CellDescriptionColorProperty = + BindableProperty.Create( + nameof(CellDescriptionColor), + typeof(Color), + typeof(BoxedView), + default(Color), + defaultBindingMode: BindingMode.OneWay + ); + + public Color CellDescriptionColor + { + get { return (Color)GetValue(CellDescriptionColorProperty); } + set { SetValue(CellDescriptionColorProperty, value); } + } + + public static BindableProperty CellDescriptionFontSizeProperty = + BindableProperty.Create( + nameof(CellDescriptionFontSize), + typeof(double), + typeof(BoxedView), + -1.0d, + defaultBindingMode: BindingMode.OneWay + ); + + [TypeConverter(typeof(FontSizeConverter))] + public double CellDescriptionFontSize + { + get { return (double)GetValue(CellDescriptionFontSizeProperty); } + set { SetValue(CellDescriptionFontSizeProperty, value); } + } + + public static BindableProperty CellBackgroundColorProperty = + BindableProperty.Create( + nameof(CellBackgroundColor), + typeof(Color), + typeof(BoxedView), + default(Color), + defaultBindingMode: BindingMode.OneWay + ); + + public Color CellBackgroundColor + { + get { return (Color)GetValue(CellBackgroundColorProperty); } + set { SetValue(CellBackgroundColorProperty, value); } + } + + public static BindableProperty CellAccentColorProperty = + BindableProperty.Create( + nameof(CellAccentColor), + typeof(Color), + typeof(BoxedView), + Color.Accent, + defaultBindingMode: BindingMode.OneWay + ); + + public Color CellAccentColor + { + get { return (Color)GetValue(CellAccentColorProperty); } + set { SetValue(CellAccentColorProperty, value); } + } + + public static BindableProperty ShowSectionTopBottomBorderProperty = + BindableProperty.Create( + nameof(ShowSectionTopBottomBorder), + typeof(bool), + typeof(BoxedView), + true, + defaultBindingMode: BindingMode.OneWay + ); + + public bool ShowSectionTopBottomBorder + { + get { return (bool)GetValue(ShowSectionTopBottomBorderProperty); } + set { SetValue(ShowSectionTopBottomBorderProperty, value); } + } + + public static BindableProperty ScrollToBottomProperty = + BindableProperty.Create( + nameof(ScrollToBottom), + typeof(bool), + typeof(BoxedView), + default(bool), + defaultBindingMode: BindingMode.TwoWay + ); + + public bool ScrollToBottom + { + get { return (bool)GetValue(ScrollToBottomProperty); } + set { SetValue(ScrollToBottomProperty, value); } + } + + public static BindableProperty ScrollToTopProperty = + BindableProperty.Create( + nameof(ScrollToTop), + typeof(bool), + typeof(BoxedView), + default(bool), + defaultBindingMode: BindingMode.TwoWay + ); + + public bool ScrollToTop + { + get { return (bool)GetValue(ScrollToTopProperty); } + set { SetValue(ScrollToTopProperty, value); } + } + + public static BindableProperty VisibleContentHeightProperty = + BindableProperty.Create( + nameof(VisibleContentHeight), + typeof(double), + typeof(BoxedView), + -1d, + defaultBindingMode: BindingMode.OneWayToSource + ); + + public double VisibleContentHeight + { + get { return (double)GetValue(VisibleContentHeightProperty); } + set { SetValue(VisibleContentHeightProperty, value); } + } + + public static BindableProperty ItemsSourceProperty = + BindableProperty.Create( + nameof(ItemsSource), + typeof(IEnumerable), + typeof(BoxedView), + default(IEnumerable), + defaultBindingMode: BindingMode.OneWay, + propertyChanged: ItemsChanged + ); + + public IEnumerable ItemsSource + { + get { return (IEnumerable)GetValue(ItemsSourceProperty); } + set { SetValue(ItemsSourceProperty, value); } + } + + public static BindableProperty ItemTemplateProperty = + BindableProperty.Create( + nameof(ItemTemplate), + typeof(DataTemplate), + typeof(BoxedView), + default(DataTemplate), + defaultBindingMode: BindingMode.OneWay + ); + + public DataTemplate ItemTemplate + { + get { return (DataTemplate)GetValue(ItemTemplateProperty); } + set { SetValue(ItemTemplateProperty, value); } + } + + private static void ItemsChanged(BindableObject bindable, object oldValue, object newValue) + { + var boxedView = bindable as BoxedView; + if(boxedView.ItemTemplate == null) + { + return; + } + + if(oldValue is INotifyCollectionChanged oldObservableCollection) + { + oldObservableCollection.CollectionChanged -= boxedView.OnItemsSourceCollectionChanged; + } + if(newValue is INotifyCollectionChanged newObservableCollection) + { + newObservableCollection.CollectionChanged += boxedView.OnItemsSourceCollectionChanged; + } + + boxedView.Root.Clear(); + + if(newValue is IList newValueAsEnumerable) + { + foreach(var item in newValueAsEnumerable) + { + var view = CreateChildViewFor(boxedView.ItemTemplate, item, boxedView); + boxedView.Root.Add(view); + } + } + } + + private void OnItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if(e.Action == NotifyCollectionChangedAction.Replace) + { + Root.RemoveAt(e.OldStartingIndex); + var item = e.NewItems[e.NewStartingIndex]; + var view = CreateChildViewFor(ItemTemplate, item, this); + Root.Insert(e.NewStartingIndex, view); + } + else if(e.Action == NotifyCollectionChangedAction.Add) + { + if(e.NewItems != null) + { + for(var i = 0; i < e.NewItems.Count; ++i) + { + var item = e.NewItems[i]; + var view = CreateChildViewFor(ItemTemplate, item, this); + Root.Insert(i + e.NewStartingIndex, view); + } + } + } + else if(e.Action == NotifyCollectionChangedAction.Remove) + { + if(e.OldItems != null) + { + Root.RemoveAt(e.OldStartingIndex); + } + } + else if(e.Action == NotifyCollectionChangedAction.Reset) + { + Root.Clear(); + } + } + + private static BoxedSection CreateChildViewFor(DataTemplate template, object item, BindableObject container) + { + if(template is DataTemplateSelector selector) + { + template = selector.SelectTemplate(item, container); + } + template.SetValue(BindingContextProperty, item); + return template.CreateContent() as BoxedSection; + } + } +} diff --git a/src/App/Controls/BoxedView/Cells/BaseCell.cs b/src/App/Controls/BoxedView/Cells/BaseCell.cs new file mode 100644 index 000000000..7a9bde8e2 --- /dev/null +++ b/src/App/Controls/BoxedView/Cells/BaseCell.cs @@ -0,0 +1,87 @@ +using System; +using Xamarin.Forms; + +namespace Bit.App.Controls.BoxedView +{ + public class BaseCell : Cell + { + public new event EventHandler Tapped; + + internal new void OnTapped() + { + Tapped?.Invoke(this, EventArgs.Empty); + } + + public static BindableProperty TitleProperty = BindableProperty.Create( + nameof(Title), typeof(string), typeof(BaseCell), default(string), defaultBindingMode: BindingMode.OneWay); + + public static BindableProperty TitleColorProperty = BindableProperty.Create( + nameof(TitleColor), typeof(Color), typeof(BaseCell), default(Color), + defaultBindingMode: BindingMode.OneWay); + + public static BindableProperty TitleFontSizeProperty = BindableProperty.Create( + nameof(TitleFontSize), typeof(double), typeof(BaseCell), -1.0, defaultBindingMode: BindingMode.OneWay); + + public static BindableProperty DescriptionProperty = BindableProperty.Create( + nameof(Description), typeof(string), typeof(BaseCell), default(string), + defaultBindingMode: BindingMode.OneWay); + + public static BindableProperty DescriptionColorProperty = BindableProperty.Create( + nameof(DescriptionColor), typeof(Color), typeof(BaseCell), default(Color), + defaultBindingMode: BindingMode.OneWay); + + public static BindableProperty DescriptionFontSizeProperty = BindableProperty.Create( + nameof(DescriptionFontSize), typeof(double), typeof(BaseCell), -1.0d, + defaultBindingMode: BindingMode.OneWay); + + public static BindableProperty BackgroundColorProperty = BindableProperty.Create( + nameof(BackgroundColor), typeof(Color), typeof(BaseCell), default(Color), + defaultBindingMode: BindingMode.OneWay); + + public string Title + { + get => (string)GetValue(TitleProperty); + set => SetValue(TitleProperty, value); + } + + public Color TitleColor + { + get => (Color)GetValue(TitleColorProperty); + set => SetValue(TitleColorProperty, value); + } + + [TypeConverter(typeof(FontSizeConverter))] + public double TitleFontSize + { + get => (double)GetValue(TitleFontSizeProperty); + set => SetValue(TitleFontSizeProperty, value); + } + + public string Description + { + get => (string)GetValue(DescriptionProperty); + set => SetValue(DescriptionProperty, value); + } + + public Color DescriptionColor + { + get => (Color)GetValue(DescriptionColorProperty); + set => SetValue(DescriptionColorProperty, value); + } + + [TypeConverter(typeof(FontSizeConverter))] + public double DescriptionFontSize + { + get => (double)GetValue(DescriptionFontSizeProperty); + set => SetValue(DescriptionFontSizeProperty, value); + } + + public Color BackgroundColor + { + get => (Color)GetValue(BackgroundColorProperty); + set => SetValue(BackgroundColorProperty, value); + } + + public BoxedSection Section { get; set; } + } +} diff --git a/src/App/Controls/BoxedView/Cells/LabelCell.cs b/src/App/Controls/BoxedView/Cells/LabelCell.cs new file mode 100644 index 000000000..2e4f4b73e --- /dev/null +++ b/src/App/Controls/BoxedView/Cells/LabelCell.cs @@ -0,0 +1,38 @@ +using Xamarin.Forms; + +namespace Bit.App.Controls.BoxedView +{ + public class LabelCell : BaseCell + { + public static BindableProperty ValueTextProperty = BindableProperty.Create( + nameof(ValueText), typeof(string), typeof(LabelCell), default(string), + defaultBindingMode: BindingMode.OneWay); + + public static BindableProperty ValueTextColorProperty = BindableProperty.Create( + nameof(ValueTextColor), typeof(Color), typeof(LabelCell), default(Color), + defaultBindingMode: BindingMode.OneWay); + + public static BindableProperty ValueTextFontSizeProperty = BindableProperty.Create( + nameof(ValueTextFontSize), typeof(double), typeof(LabelCell), -1.0d, + defaultBindingMode: BindingMode.OneWay); + + public string ValueText + { + get => (string)GetValue(ValueTextProperty); + set => SetValue(ValueTextProperty, value); + } + + public Color ValueTextColor + { + get => (Color)GetValue(ValueTextColorProperty); + set => SetValue(ValueTextColorProperty, value); + } + + [TypeConverter(typeof(FontSizeConverter))] + public double ValueTextFontSize + { + get => (double)GetValue(ValueTextFontSizeProperty); + set => SetValue(ValueTextFontSizeProperty, value); + } + } +} diff --git a/src/App/Pages/BaseViewModel.cs b/src/App/Pages/BaseViewModel.cs index 1f2e412c8..da6a1d453 100644 --- a/src/App/Pages/BaseViewModel.cs +++ b/src/App/Pages/BaseViewModel.cs @@ -1,4 +1,5 @@ using Bit.Core.Utilities; +using Xamarin.Forms; namespace Bit.App.Pages { @@ -11,5 +12,7 @@ namespace Bit.App.Pages get => _pageTitle; set => SetProperty(ref _pageTitle, value); } + + public ContentPage Page { get; set; } } } diff --git a/src/App/Pages/SettingsPage.xaml b/src/App/Pages/SettingsPage.xaml index 7654d4b81..da33251a0 100644 --- a/src/App/Pages/SettingsPage.xaml +++ b/src/App/Pages/SettingsPage.xaml @@ -4,21 +4,17 @@ x:Class="Bit.App.Pages.SettingsPage" xmlns:pages="clr-namespace:Bit.App.Pages" xmlns:controls="clr-namespace:Bit.App.Controls" + xmlns:bv="clr-namespace:Bit.App.Controls.BoxedView" x:DataType="pages:SettingsPageViewModel" Title="{Binding PageTitle}"> - - - + + + + +