mirror of
https://github.com/bitwarden/mobile
synced 2025-12-19 09:43:27 +00:00
EC-295 Added custom renderer for CollectionView to attach ItemTouchHelper callback to handle swiping natively in Android
This commit is contained in:
@@ -156,6 +156,8 @@
|
|||||||
<Compile Include="Services\FileService.cs" />
|
<Compile Include="Services\FileService.cs" />
|
||||||
<Compile Include="Services\AutofillHandler.cs" />
|
<Compile Include="Services\AutofillHandler.cs" />
|
||||||
<Compile Include="Constants.cs" />
|
<Compile Include="Constants.cs" />
|
||||||
|
<Compile Include="Renderers\CollectionView\ExtendedCollectionViewRenderer.cs" />
|
||||||
|
<Compile Include="Utilities\RecyclerSwipeItemTouchCallback.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AndroidAsset Include="Assets\bwi-font.ttf" />
|
<AndroidAsset Include="Assets\bwi-font.ttf" />
|
||||||
@@ -294,6 +296,7 @@
|
|||||||
<Folder Include="Resources\values-v30\" />
|
<Folder Include="Resources\values-v30\" />
|
||||||
<Folder Include="Resources\drawable-v26\" />
|
<Folder Include="Resources\drawable-v26\" />
|
||||||
<Folder Include="Resources\drawable-night-v26\" />
|
<Folder Include="Resources\drawable-night-v26\" />
|
||||||
|
<Folder Include="Renderers\CollectionView\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
|
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
|
||||||
</Project>
|
</Project>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
using System;
|
||||||
|
using Android.Content;
|
||||||
|
using AndroidX.RecyclerView.Widget;
|
||||||
|
using Bit.App.Controls;
|
||||||
|
using Bit.App.Utilities;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Droid.Renderers.CollectionView;
|
||||||
|
using Bit.Droid.Utilities;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
using Xamarin.Forms.Platform.Android;
|
||||||
|
using static Android.Content.ClipData;
|
||||||
|
using static AndroidX.RecyclerView.Widget.RecyclerView;
|
||||||
|
|
||||||
|
[assembly: ExportRenderer(typeof(ExtendedCollectionView), typeof(ExtendedCollectionViewRenderer))]
|
||||||
|
namespace Bit.Droid.Renderers.CollectionView
|
||||||
|
{
|
||||||
|
public class CustomGroupableItemsViewAdapter<TItemsView, TItemsViewSource> : GroupableItemsViewAdapter<TItemsView, TItemsViewSource>
|
||||||
|
where TItemsView : GroupableItemsView
|
||||||
|
where TItemsViewSource : IGroupableItemsViewSource
|
||||||
|
{
|
||||||
|
protected internal CustomGroupableItemsViewAdapter(TItemsView groupableItemsView, Func<View, Context, ItemContentView> createView = null)
|
||||||
|
: base(groupableItemsView, createView)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public object GetItemAt(int position)
|
||||||
|
{
|
||||||
|
return ItemsSource.GetItem(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ExtendedCollectionViewRenderer : GroupableItemsViewRenderer<ExtendedCollectionView, CustomGroupableItemsViewAdapter<ExtendedCollectionView, IGroupableItemsViewSource>, IGroupableItemsViewSource>
|
||||||
|
{
|
||||||
|
ItemTouchHelper _itemTouchHelper;
|
||||||
|
|
||||||
|
public ExtendedCollectionViewRenderer(Context context) : base(context)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override CustomGroupableItemsViewAdapter<ExtendedCollectionView, IGroupableItemsViewSource> CreateAdapter()
|
||||||
|
{
|
||||||
|
return new CustomGroupableItemsViewAdapter<ExtendedCollectionView, IGroupableItemsViewSource>(ItemsView);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void SetUpNewElement(ExtendedCollectionView newElement)
|
||||||
|
{
|
||||||
|
base.SetUpNewElement(newElement);
|
||||||
|
|
||||||
|
if (newElement is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemTouchCallback = new RecyclerSwipeItemTouchCallback<CipherViewCellViewModel>(ItemTouchHelper.Right, this.Context, new CipherViewModelSwipeableItem(),
|
||||||
|
viewHolder =>
|
||||||
|
{
|
||||||
|
if (viewHolder is TemplatedItemViewHolder templatedViewHolder
|
||||||
|
&&
|
||||||
|
templatedViewHolder.View?.BindingContext is CipherViewCellViewModel vm)
|
||||||
|
{
|
||||||
|
return vm;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
itemTouchCallback.OnSwipedCommand = new Command<ViewHolder>(viewHolder =>
|
||||||
|
{
|
||||||
|
ItemsViewAdapter.NotifyItemChanged(viewHolder.LayoutPosition);
|
||||||
|
|
||||||
|
ItemsView.OnSwipeCommand?.Execute(ItemsViewAdapter.GetItemAt(viewHolder.BindingAdapterPosition));
|
||||||
|
});
|
||||||
|
_itemTouchHelper = new ItemTouchHelper(itemTouchCallback);
|
||||||
|
_itemTouchHelper.AttachToRecyclerView(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void TearDownOldElement(ItemsView oldElement)
|
||||||
|
{
|
||||||
|
base.TearDownOldElement(oldElement);
|
||||||
|
|
||||||
|
if (oldElement is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_itemTouchHelper.AttachToRecyclerView(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
156
src/Android/Utilities/RecyclerSwipeItemTouchCallback.cs
Normal file
156
src/Android/Utilities/RecyclerSwipeItemTouchCallback.cs
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Android.Graphics;
|
||||||
|
using Android.Graphics.Drawables;
|
||||||
|
using Android.Util;
|
||||||
|
using Android.Views;
|
||||||
|
using AndroidX.RecyclerView.Widget;
|
||||||
|
using Bit.App.Abstractions;
|
||||||
|
using Xamarin.Forms.Platform.Android;
|
||||||
|
using FontImageSource = Xamarin.Forms.FontImageSource;
|
||||||
|
|
||||||
|
namespace Bit.Droid.Utilities
|
||||||
|
{
|
||||||
|
public class RecyclerSwipeItemTouchCallback<TItem> : ItemTouchHelper.SimpleCallback
|
||||||
|
{
|
||||||
|
private Paint _clearPaint;
|
||||||
|
private readonly ColorDrawable _background = new ColorDrawable();
|
||||||
|
private readonly Android.Content.Context _context;
|
||||||
|
private readonly ISwipeableItem<TItem> _swipeableItem;
|
||||||
|
private readonly Func<RecyclerView.ViewHolder, TItem> _viewHolderToTItem;
|
||||||
|
private Dictionary<string, Typeface> _fontFamilyTypefaceCache = new Dictionary<string, Typeface>();
|
||||||
|
|
||||||
|
public RecyclerSwipeItemTouchCallback(int swipeDir, Android.Content.Context context, ISwipeableItem<TItem> swipeableItem, Func<RecyclerView.ViewHolder, TItem> viewHolderToTItem)
|
||||||
|
: base(0, swipeDir)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_swipeableItem = swipeableItem;
|
||||||
|
_viewHolderToTItem = viewHolderToTItem;
|
||||||
|
|
||||||
|
_clearPaint = new Paint();
|
||||||
|
_clearPaint.SetXfermode(new PorterDuffXfermode(PorterDuff.Mode.Clear));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand OnSwipedCommand { get; set; }
|
||||||
|
|
||||||
|
public override bool OnMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnChildDrawOver(Canvas c, RecyclerView recyclerView,
|
||||||
|
RecyclerView.ViewHolder viewHolder, float dX, float dY,
|
||||||
|
int actionState, bool isCurrentlyActive)
|
||||||
|
{
|
||||||
|
|
||||||
|
var itemView = viewHolder.ItemView;
|
||||||
|
int itemHeight = itemView.Bottom - itemView.Top;
|
||||||
|
var isCanceled = (dX == 0f) && !isCurrentlyActive;
|
||||||
|
|
||||||
|
if (isCanceled)
|
||||||
|
{
|
||||||
|
ClearCanvas(c, itemView.Right + dX, itemView.Top, itemView.Right, itemView.Bottom);
|
||||||
|
base.OnChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(_swipeableItem.GetSwipeIcon(_viewHolderToTItem(viewHolder)) is FontImageSource fontSource))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var paint = GetIconPaint(itemView, fontSource);
|
||||||
|
|
||||||
|
var width = (int)(paint.MeasureText(fontSource.Glyph) + .5f);
|
||||||
|
var baseline = (int)(-paint.Ascent() + .5f);
|
||||||
|
var height = (int)(baseline + paint.Descent() + .5f);
|
||||||
|
|
||||||
|
int itemTop = itemView.Top + (itemHeight - height) / 2;
|
||||||
|
int itemMargin = (itemHeight - height) / 2;
|
||||||
|
int itemBottom = itemTop + height;
|
||||||
|
|
||||||
|
_background.Color = _swipeableItem.GetBackgroundColor(_viewHolderToTItem(viewHolder)).ToAndroid();
|
||||||
|
if (dX < 0)
|
||||||
|
{
|
||||||
|
_background.SetBounds((int)(itemView.Right + dX), itemView.Top, itemView.Right, itemView.Bottom);
|
||||||
|
_background.Draw(c);
|
||||||
|
|
||||||
|
int itemLeft = itemView.Right - itemMargin - width;
|
||||||
|
int itemRight = itemView.Right - itemMargin;
|
||||||
|
|
||||||
|
c.DrawText(fontSource.Glyph, itemLeft, itemBottom, paint);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_background.SetBounds((int)(itemView.Left + dX), itemView.Top, itemView.Left, itemView.Bottom);
|
||||||
|
_background.Draw(c);
|
||||||
|
|
||||||
|
int itemLeft = itemView.Left + itemMargin;
|
||||||
|
int itemRight = itemView.Left + itemMargin + width;
|
||||||
|
|
||||||
|
c.DrawText(fontSource.Glyph, itemLeft, itemBottom, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Paint GetIconPaint(View itemView, FontImageSource fontSource)
|
||||||
|
{
|
||||||
|
var paint = new Paint
|
||||||
|
{
|
||||||
|
TextSize = TypedValue.ApplyDimension(ComplexUnitType.Dip, (float)fontSource.Size, _context.Resources.DisplayMetrics),
|
||||||
|
Color = fontSource.Color.ToAndroid(),
|
||||||
|
TextAlign = Paint.Align.Left,
|
||||||
|
AntiAlias = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (fontSource.FontFamily != null)
|
||||||
|
{
|
||||||
|
if (!_fontFamilyTypefaceCache.TryGetValue(fontSource.FontFamily, out var typeface))
|
||||||
|
{
|
||||||
|
var font = new Xamarin.Forms.Font();
|
||||||
|
// HACK: there is no way to set the font family of Font
|
||||||
|
// and the only public extension method to get the typeface is thorugh a Xamarin.Forms.Font
|
||||||
|
// so we use reflection here to set the font family and take advantage of ToTypeface method
|
||||||
|
// Also, we need to box the font in order to use reflection to set the property because it's a struct
|
||||||
|
object fontBoxed = font;
|
||||||
|
var pinfo = typeof(Xamarin.Forms.Font)
|
||||||
|
.GetProperty(nameof(Xamarin.Forms.Font.FontFamily));
|
||||||
|
pinfo.SetValue(fontBoxed, fontSource.FontFamily, null);
|
||||||
|
typeface = ((Xamarin.Forms.Font)fontBoxed).ToTypeface();
|
||||||
|
_fontFamilyTypefaceCache.Add(fontSource.FontFamily, typeface);
|
||||||
|
}
|
||||||
|
|
||||||
|
paint.SetTypeface(typeface);
|
||||||
|
}
|
||||||
|
|
||||||
|
int alpha = Math.Abs(((int)((itemView.TranslationX / itemView.Width) * 510)));
|
||||||
|
paint.Alpha = Math.Min(alpha, 255);
|
||||||
|
|
||||||
|
return paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetSwipeDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)
|
||||||
|
{
|
||||||
|
if (viewHolder is TemplatedItemViewHolder templatedViewHolder
|
||||||
|
&&
|
||||||
|
_swipeableItem.CanSwipe(_viewHolderToTItem(viewHolder)))
|
||||||
|
{
|
||||||
|
return base.GetSwipeDirs(recyclerView, viewHolder);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearCanvas(Canvas c, float left, float top, float right, float bottom)
|
||||||
|
{
|
||||||
|
if (c != null)
|
||||||
|
c.DrawRect(left, top, right, bottom, _clearPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnSwiped(RecyclerView.ViewHolder viewHolder, int direction)
|
||||||
|
{
|
||||||
|
OnSwipedCommand?.Execute(viewHolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/App/Abstractions/ISwipeableItem.cs
Normal file
11
src/App/Abstractions/ISwipeableItem.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Abstractions
|
||||||
|
{
|
||||||
|
public interface ISwipeableItem<TItem>
|
||||||
|
{
|
||||||
|
bool CanSwipe(TItem item);
|
||||||
|
FontImageSource GetSwipeIcon(TItem item);
|
||||||
|
Color GetBackgroundColor(TItem item);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.App.Utilities;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Controls
|
||||||
|
{
|
||||||
|
public class CipherViewModelSwipeableItem : ISwipeableItem<CipherViewCellViewModel>
|
||||||
|
{
|
||||||
|
readonly Dictionary<CipherType, FontImageSource> _imageCache = new Dictionary<CipherType, FontImageSource>();
|
||||||
|
|
||||||
|
public bool CanSwipe(CipherViewCellViewModel item)
|
||||||
|
{
|
||||||
|
if (item?.Cipher is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.Cipher.Type == CipherType.Login
|
||||||
|
||
|
||||||
|
item.Cipher.Type == CipherType.Card
|
||||||
|
||
|
||||||
|
item.Cipher.Type == CipherType.SecureNote;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Xamarin.Forms.Color GetBackgroundColor(CipherViewCellViewModel item)
|
||||||
|
{
|
||||||
|
if (item?.Cipher is null)
|
||||||
|
{
|
||||||
|
return ThemeManager.GetResourceColor("PrimaryColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.Cipher.Type == CipherType.Login
|
||||||
|
&&
|
||||||
|
string.IsNullOrEmpty(item.Cipher.Login?.Password))
|
||||||
|
{
|
||||||
|
return ThemeManager.GetResourceColor("SeparatorColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.Cipher.Type == CipherType.Card
|
||||||
|
&&
|
||||||
|
string.IsNullOrEmpty(item.Cipher.Card?.Number))
|
||||||
|
{
|
||||||
|
return ThemeManager.GetResourceColor("SeparatorColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.Cipher.Type == CipherType.SecureNote
|
||||||
|
&&
|
||||||
|
string.IsNullOrEmpty(item.Cipher.Notes))
|
||||||
|
{
|
||||||
|
return ThemeManager.GetResourceColor("SeparatorColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ThemeManager.GetResourceColor("PrimaryColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
public FontImageSource GetSwipeIcon(CipherViewCellViewModel item)
|
||||||
|
{
|
||||||
|
if (item?.Cipher is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_imageCache.TryGetValue(item.Cipher.Type, out var image))
|
||||||
|
{
|
||||||
|
image = new IconFontImageSource { Color = ThemeManager.GetResourceColor("BackgroundColor") };
|
||||||
|
switch (item.Cipher.Type)
|
||||||
|
{
|
||||||
|
case CipherType.Login:
|
||||||
|
image.Glyph = BitwardenIcons.Key;
|
||||||
|
break;
|
||||||
|
case CipherType.Card:
|
||||||
|
image.Glyph = BitwardenIcons.Hashtag;
|
||||||
|
break;
|
||||||
|
case CipherType.SecureNote:
|
||||||
|
image.Glyph = BitwardenIcons.Clone;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
_imageCache.Add(item.Cipher.Type, image);
|
||||||
|
}
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,19 @@
|
|||||||
using Xamarin.Forms;
|
using System.Windows.Input;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
namespace Bit.App.Controls
|
namespace Bit.App.Controls
|
||||||
{
|
{
|
||||||
public class ExtendedCollectionView : CollectionView
|
public class ExtendedCollectionView : CollectionView
|
||||||
{
|
{
|
||||||
|
public static BindableProperty OnSwipeCommandProperty =
|
||||||
|
BindableProperty.Create(nameof(OnSwipeCommand), typeof(ICommand), typeof(ExtendedCollectionView));
|
||||||
|
|
||||||
|
public ICommand OnSwipeCommand
|
||||||
|
{
|
||||||
|
get => (ICommand)GetValue(OnSwipeCommandProperty);
|
||||||
|
set => SetValue(OnSwipeCommandProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
public string ExtraDataForLogging { get; set; }
|
public string ExtraDataForLogging { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,7 +189,8 @@
|
|||||||
SelectionMode="Single"
|
SelectionMode="Single"
|
||||||
SelectionChanged="RowSelected"
|
SelectionChanged="RowSelected"
|
||||||
StyleClass="list, list-platform"
|
StyleClass="list, list-platform"
|
||||||
ExtraDataForLogging="Groupings Page" />
|
ExtraDataForLogging="Groupings Page"
|
||||||
|
OnSwipeCommand="{Binding SwipeItemActionCommand}"/>
|
||||||
</RefreshView>
|
</RefreshView>
|
||||||
</StackLayout>
|
</StackLayout>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|||||||
@@ -29,9 +29,11 @@ namespace Bit.App.Pages
|
|||||||
return GroupTemplate;
|
return GroupTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
return listItem.Cipher.Type != Core.Enums.CipherType.Identity && SwipeableCipherTemplate != null
|
return CipherTemplate;
|
||||||
? SwipeableCipherTemplate
|
|
||||||
: CipherTemplate;
|
//return listItem.Cipher.Type != Core.Enums.CipherType.Identity && SwipeableCipherTemplate != null
|
||||||
|
// ? SwipeableCipherTemplate
|
||||||
|
// : CipherTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user