diff --git a/src/Android/Android.csproj b/src/Android/Android.csproj
index 7abf7d6dd..7b569ba8e 100644
--- a/src/Android/Android.csproj
+++ b/src/Android/Android.csproj
@@ -156,6 +156,8 @@
+
+
@@ -294,6 +296,7 @@
+
\ No newline at end of file
diff --git a/src/Android/Renderers/CollectionView/ExtendedCollectionViewRenderer.cs b/src/Android/Renderers/CollectionView/ExtendedCollectionViewRenderer.cs
new file mode 100644
index 000000000..314a8e47d
--- /dev/null
+++ b/src/Android/Renderers/CollectionView/ExtendedCollectionViewRenderer.cs
@@ -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 : GroupableItemsViewAdapter
+ where TItemsView : GroupableItemsView
+ where TItemsViewSource : IGroupableItemsViewSource
+ {
+ protected internal CustomGroupableItemsViewAdapter(TItemsView groupableItemsView, Func createView = null)
+ : base(groupableItemsView, createView)
+ {
+ }
+
+ public object GetItemAt(int position)
+ {
+ return ItemsSource.GetItem(position);
+ }
+ }
+
+ public class ExtendedCollectionViewRenderer : GroupableItemsViewRenderer, IGroupableItemsViewSource>
+ {
+ ItemTouchHelper _itemTouchHelper;
+
+ public ExtendedCollectionViewRenderer(Context context) : base(context)
+ {
+ }
+
+ protected override CustomGroupableItemsViewAdapter CreateAdapter()
+ {
+ return new CustomGroupableItemsViewAdapter(ItemsView);
+ }
+
+ protected override void SetUpNewElement(ExtendedCollectionView newElement)
+ {
+ base.SetUpNewElement(newElement);
+
+ if (newElement is null)
+ {
+ return;
+ }
+
+ var itemTouchCallback = new RecyclerSwipeItemTouchCallback(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 =>
+ {
+ 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);
+ }
+ }
+}
diff --git a/src/Android/Utilities/RecyclerSwipeItemTouchCallback.cs b/src/Android/Utilities/RecyclerSwipeItemTouchCallback.cs
new file mode 100644
index 000000000..c48e6c957
--- /dev/null
+++ b/src/Android/Utilities/RecyclerSwipeItemTouchCallback.cs
@@ -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 : ItemTouchHelper.SimpleCallback
+ {
+ private Paint _clearPaint;
+ private readonly ColorDrawable _background = new ColorDrawable();
+ private readonly Android.Content.Context _context;
+ private readonly ISwipeableItem _swipeableItem;
+ private readonly Func _viewHolderToTItem;
+ private Dictionary _fontFamilyTypefaceCache = new Dictionary();
+
+ public RecyclerSwipeItemTouchCallback(int swipeDir, Android.Content.Context context, ISwipeableItem swipeableItem, Func 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);
+ }
+ }
+}
diff --git a/src/App/Abstractions/ISwipeableItem.cs b/src/App/Abstractions/ISwipeableItem.cs
new file mode 100644
index 000000000..0218330b0
--- /dev/null
+++ b/src/App/Abstractions/ISwipeableItem.cs
@@ -0,0 +1,11 @@
+using Xamarin.Forms;
+
+namespace Bit.App.Abstractions
+{
+ public interface ISwipeableItem
+ {
+ bool CanSwipe(TItem item);
+ FontImageSource GetSwipeIcon(TItem item);
+ Color GetBackgroundColor(TItem item);
+ }
+}
diff --git a/src/App/Controls/CipherViewCell/CipherViewModelSwipeableItem.cs b/src/App/Controls/CipherViewCell/CipherViewModelSwipeableItem.cs
new file mode 100644
index 000000000..8c8b99300
--- /dev/null
+++ b/src/App/Controls/CipherViewCell/CipherViewModelSwipeableItem.cs
@@ -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
+ {
+ readonly Dictionary _imageCache = new Dictionary();
+
+ 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;
+ }
+ }
+}
diff --git a/src/App/Controls/ExtendedCollectionView.cs b/src/App/Controls/ExtendedCollectionView.cs
index 79e1bac9d..819d94153 100644
--- a/src/App/Controls/ExtendedCollectionView.cs
+++ b/src/App/Controls/ExtendedCollectionView.cs
@@ -1,9 +1,19 @@
-using Xamarin.Forms;
+using System.Windows.Input;
+using Xamarin.Forms;
namespace Bit.App.Controls
{
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; }
}
}
diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml
index c62bce60d..ea9a6cdfc 100644
--- a/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml
+++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPage.xaml
@@ -189,7 +189,8 @@
SelectionMode="Single"
SelectionChanged="RowSelected"
StyleClass="list, list-platform"
- ExtraDataForLogging="Groupings Page" />
+ ExtraDataForLogging="Groupings Page"
+ OnSwipeCommand="{Binding SwipeItemActionCommand}"/>
diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItemSelector.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItemSelector.cs
index 19a2379b2..7da590bbb 100644
--- a/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItemSelector.cs
+++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItemSelector.cs
@@ -29,9 +29,11 @@ namespace Bit.App.Pages
return GroupTemplate;
}
- return listItem.Cipher.Type != Core.Enums.CipherType.Identity && SwipeableCipherTemplate != null
- ? SwipeableCipherTemplate
- : CipherTemplate;
+ return CipherTemplate;
+
+ //return listItem.Cipher.Type != Core.Enums.CipherType.Identity && SwipeableCipherTemplate != null
+ // ? SwipeableCipherTemplate
+ // : CipherTemplate;
}
return null;