1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-21 18:53:29 +00:00

BoxedView with LabelCell

This commit is contained in:
Kyle Spearrin
2019-04-04 22:28:03 -04:00
parent 2b2342bcad
commit 61e95e03c8
21 changed files with 2945 additions and 285 deletions

View File

@@ -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<Type, int> _viewTypes;
private List<CellCache> _cellCaches;
internal List<CellCache> 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<ViewHolder> _viewHolders = new List<ViewHolder>();
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<LinearLayout>(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<CellCache>();
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<TextView>(Resource.Id.HeaderCellText);
Border = view.FindViewById<LinearLayout>(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<TextView>(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<LinearLayout>(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);
}
}
}

View File

@@ -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<BoxedView, RecyclerView>
{
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<BoxedView> 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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<TNativeCell> : CellRenderer where TNativeCell : BaseCellView
{
internal static class InstanceCreator<T1, T2, TInstance>
{
public static Func<T1, T2, TInstance> Create { get; } = CreateInstance();
private static Func<T1, T2, TInstance> 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<Func<T1, T2, TInstance>>(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<Context, Xamarin.Forms.Cell, TNativeCell>.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;
}
}
}
}
}

View File

@@ -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<TextView>(Resource.Id.CellTitle);
DescriptionLabel = contentView.FindViewById<TextView>(Resource.Id.CellDescription);
ContentStack = contentView.FindViewById<LinearLayout>(Resource.Id.CellContentStack);
AccessoryStack = contentView.FindViewById<LinearLayout>(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);
}
}
}

View File

@@ -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<LabelCellView>
{ }
[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);
}
}
}

View File

@@ -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,
});
}
}
}