|
using System;
using System.ComponentModel;
using Android.Content;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using AndroidX.Core.Widget;
using AndroidX.SwipeRefreshLayout.Widget;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Controls.PlatformConfiguration.AndroidSpecific;
using Microsoft.Maui.Graphics;
using AListView = Android.Widget.ListView;
using AView = Android.Views.View;
namespace Microsoft.Maui.Controls.Compatibility.Platform.Android
{
[Obsolete("Use Microsoft.Maui.Controls.Handlers.Compatibility.ListViewRenderer instead")]
public class ListViewRenderer : ViewRenderer<ListView, AListView>, SwipeRefreshLayout.IOnRefreshListener
{
ListViewAdapter _adapter;
bool _disposed;
IVisualElementRenderer _headerRenderer;
IVisualElementRenderer _footerRenderer;
Container _headerView;
Container _footerView;
bool _isAttached;
ScrollToRequestedEventArgs _pendingScrollTo;
SwipeRefreshLayout _refresh;
IListViewController Controller => Element;
ITemplatedItemsView<Cell> TemplatedItemsView => Element;
ScrollBarVisibility _defaultHorizontalScrollVisibility = 0;
ScrollBarVisibility _defaultVerticalScrollVisibility = 0;
public ListViewRenderer(Context context) : base(context)
{
AutoPackage = false;
}
void SwipeRefreshLayout.IOnRefreshListener.OnRefresh()
{
IListViewController controller = Element;
controller.SendRefreshing();
}
protected override void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
Controller.ScrollToRequested -= OnScrollToRequested;
if (_headerRenderer != null)
{
Platform.ClearRenderer(_headerRenderer.View);
_headerRenderer.Dispose();
_headerRenderer = null;
}
_headerView?.Dispose();
_headerView = null;
if (_footerRenderer != null)
{
Platform.ClearRenderer(_footerRenderer.View);
_footerRenderer.Dispose();
_footerRenderer = null;
}
_footerView?.Dispose();
_footerView = null;
if (Control != null)
{
// Unhook the adapter from the ListView before disposing of it
Control.Adapter = null;
Control.SetOnScrollListener(null);
}
if (_adapter != null)
{
_adapter.Dispose();
_adapter = null;
}
}
base.Dispose(disposing);
}
protected override Size MinimumSize()
{
return new Size(40, 40);
}
protected virtual SwipeRefreshLayout CreateNativePullToRefresh(Context context)
=> new SwipeRefreshLayoutWithFixedNestedScrolling(context);
protected override void OnAttachedToWindow()
{
base.OnAttachedToWindow();
if (Control != null)
Control.NestedScrollingEnabled = (Parent.GetParentOfType<NestedScrollView>() != null);
_isAttached = true;
_adapter.IsAttachedToWindow = _isAttached;
UpdateIsRefreshing(isInitialValue: true);
}
protected override void OnDetachedFromWindow()
{
base.OnDetachedFromWindow();
_isAttached = false;
_adapter.IsAttachedToWindow = _isAttached;
}
protected override AListView CreateNativeControl()
{
return new AListView(Context);
}
protected override void OnElementChanged(ElementChangedEventArgs<ListView> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
((IListViewController)e.OldElement).ScrollToRequested -= OnScrollToRequested;
if (Control != null)
{
// Unhook the adapter from the ListView before disposing of it
Control.Adapter = null;
Control.SetOnScrollListener(null);
}
if (_adapter != null)
{
_adapter.Dispose();
_adapter = null;
}
}
if (e.NewElement != null)
{
AListView nativeListView = Control;
if (nativeListView == null)
{
var ctx = Context;
nativeListView = CreateNativeControl();
_refresh = CreateNativePullToRefresh(ctx);
_refresh.SetOnRefreshListener(this);
_refresh.AddView(nativeListView, new LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent));
SetNativeControl(nativeListView, _refresh);
_headerView = new Container(ctx);
nativeListView.AddHeaderView(_headerView, null, false);
_footerView = new Container(ctx);
nativeListView.AddFooterView(_footerView, null, false);
}
((IListViewController)e.NewElement).ScrollToRequested += OnScrollToRequested;
Control?.SetOnScrollListener(new ListViewScrollDetector(this));
nativeListView.DividerHeight = 0;
nativeListView.Focusable = false;
nativeListView.DescendantFocusability = DescendantFocusability.AfterDescendants;
nativeListView.OnFocusChangeListener = this;
nativeListView.Adapter = _adapter = e.NewElement.IsGroupingEnabled && e.NewElement.OnThisPlatform().IsFastScrollEnabled() ? new GroupedListViewAdapter(Context, nativeListView, e.NewElement) : new ListViewAdapter(Context, nativeListView, e.NewElement);
_adapter.HeaderView = _headerView;
_adapter.FooterView = _footerView;
_adapter.IsAttachedToWindow = _isAttached;
UpdateHeader();
UpdateFooter();
UpdateIsSwipeToRefreshEnabled();
UpdateFastScrollEnabled();
UpdateSelectionMode();
UpdateSpinnerColor();
UpdateHorizontalScrollBarVisibility();
UpdateVerticalScrollBarVisibility();
}
}
internal void ClickOn(AView viewCell)
{
if (Control == null)
{
return;
}
var position = Control.GetPositionForView(viewCell);
var id = Control.GetItemIdAtPosition(position);
#pragma warning disable CA1416 // Introduced in API 23: https://developer.android.com/reference/android/view/HapticFeedbackConstants#CONTEXT_CLICK
viewCell.PerformHapticFeedback(FeedbackConstants.ContextClick);
#pragma warning restore CA1416
_adapter.OnItemClick(Control, viewCell, position, id);
}
internal void LongClickOn(AView viewCell)
{
if (Control == null)
{
return;
}
var position = Control.GetPositionForView(viewCell);
var id = Control.GetItemIdAtPosition(position);
#pragma warning disable CA1416 // Introduced in API 23: https://developer.android.com/reference/android/view/HapticFeedbackConstants#CONTEXT_CLICK
viewCell.PerformHapticFeedback(FeedbackConstants.ContextClick);
#pragma warning restore CA1416
_adapter.OnItemLongClick(Control, viewCell, position, id);
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == "HeaderElement")
UpdateHeader();
else if (e.PropertyName == "FooterElement")
UpdateFooter();
else if (e.PropertyName == "RefreshAllowed")
UpdateIsSwipeToRefreshEnabled();
else if (e.PropertyName == ListView.IsPullToRefreshEnabledProperty.PropertyName)
UpdateIsSwipeToRefreshEnabled();
else if (e.PropertyName == ListView.IsRefreshingProperty.PropertyName)
UpdateIsRefreshing();
else if (e.PropertyName == ListView.SeparatorColorProperty.PropertyName || e.PropertyName == ListView.SeparatorVisibilityProperty.PropertyName)
_adapter.NotifyDataSetChanged();
else if (e.PropertyName == PlatformConfiguration.AndroidSpecific.ListView.IsFastScrollEnabledProperty.PropertyName)
UpdateFastScrollEnabled();
else if (e.PropertyName == ListView.SelectionModeProperty.PropertyName)
UpdateSelectionMode();
else if (e.PropertyName == ListView.RefreshControlColorProperty.PropertyName)
UpdateSpinnerColor();
else if (e.PropertyName == ScrollView.HorizontalScrollBarVisibilityProperty.PropertyName)
UpdateHorizontalScrollBarVisibility();
else if (e.PropertyName == ScrollView.VerticalScrollBarVisibilityProperty.PropertyName)
UpdateVerticalScrollBarVisibility();
}
protected override void OnLayout(bool changed, int l, int t, int r, int b)
{
base.OnLayout(changed, l, t, r, b);
if (_pendingScrollTo != null)
{
OnScrollToRequested(this, _pendingScrollTo);
_pendingScrollTo = null;
}
}
void OnScrollToRequested(object sender, ScrollToRequestedEventArgs e)
{
if (!_isAttached)
{
_pendingScrollTo = e;
return;
}
Cell cell;
int scrollPosition;
var scrollArgs = (ITemplatedItemsListScrollToRequestedEventArgs)e;
var templatedItems = TemplatedItemsView.TemplatedItems;
if (Element.IsGroupingEnabled)
{
var results = templatedItems.GetGroupAndIndexOfItem(scrollArgs.Group, scrollArgs.Item);
int indexOfGroup = results.Item1;
int indexOfItem = results.Item2;
if (indexOfGroup == -1)
return;
int itemIndex = indexOfItem == -1 ? 0 : indexOfItem;
var group = templatedItems.GetGroup(indexOfGroup);
if (group.Count == 0)
cell = group.HeaderContent;
else
cell = group[itemIndex];
//Increment Scroll Position by 1 when Grouping is enabled. Android offsets position of cells when using header.
scrollPosition = templatedItems.GetGlobalIndexForGroup(group) + itemIndex + 1;
}
else
{
scrollPosition = templatedItems.GetGlobalIndexOfItem(scrollArgs.Item);
if (scrollPosition == -1)
return;
cell = templatedItems[scrollPosition];
}
//Android offsets position of cells when using header
int realPositionWithHeader = scrollPosition + 1;
if (e.Position == ScrollToPosition.MakeVisible)
{
if (e.ShouldAnimate)
Control.SmoothScrollToPosition(realPositionWithHeader);
else
Control.SetSelection(realPositionWithHeader);
return;
}
int height = Control.Height;
var cellHeight = (int)cell.RenderHeight;
if (cellHeight == -1)
{
int first = Control.FirstVisiblePosition;
if (first <= scrollPosition && scrollPosition <= Control.LastVisiblePosition)
cellHeight = Control.GetChildAt(scrollPosition - first).Height;
else
{
AView view = _adapter.GetView(scrollPosition, null, null);
view.Measure(MeasureSpecFactory.MakeMeasureSpec(Control.Width, MeasureSpecMode.AtMost), MeasureSpecFactory.MakeMeasureSpec(0, MeasureSpecMode.Unspecified));
cellHeight = view.MeasuredHeight;
}
}
var y = 0;
if (e.Position == ScrollToPosition.Center)
y = height / 2 - cellHeight / 2;
else if (e.Position == ScrollToPosition.End)
y = height - cellHeight;
if (e.ShouldAnimate)
Control.SmoothScrollToPositionFromTop(realPositionWithHeader, y);
else
Control.SetSelectionFromTop(realPositionWithHeader, y);
}
void UpdateFooter()
{
var footer = (VisualElement)Controller.FooterElement;
if (_footerRenderer != null)
{
var reflectableType = _footerRenderer as System.Reflection.IReflectableType;
var rendererType = reflectableType != null ? reflectableType.GetTypeInfo().AsType() : _footerRenderer.GetType();
if (footer == null || Registrar.Registered.GetHandlerTypeForObject(footer) != rendererType)
{
if (_footerView != null)
_footerView.Child = null;
Platform.ClearRenderer(_footerRenderer.View);
_footerRenderer.Dispose();
_footerRenderer = null;
}
}
if (footer == null)
return;
if (_footerRenderer != null)
_footerRenderer.SetElement(footer);
else
{
_footerRenderer = Platform.CreateRenderer(footer, Context);
if (_footerView != null)
_footerView.Child = _footerRenderer;
}
Platform.SetRenderer(footer, _footerRenderer);
}
void UpdateHeader()
{
var header = (VisualElement)Controller.HeaderElement;
if (_headerRenderer != null)
{
var reflectableType = _headerRenderer as System.Reflection.IReflectableType;
var rendererType = reflectableType != null ? reflectableType.GetTypeInfo().AsType() : _headerRenderer.GetType();
if (header == null || Registrar.Registered.GetHandlerTypeForObject(header) != rendererType)
{
if (_headerView != null)
_headerView.Child = null;
Platform.ClearRenderer(_headerRenderer.View);
_headerRenderer.Dispose();
_headerRenderer = null;
}
}
if (header == null)
return;
if (_headerRenderer != null)
_headerRenderer.SetElement(header);
else
{
_headerRenderer = Platform.CreateRenderer(header, Context);
if (_headerView != null)
_headerView.Child = _headerRenderer;
}
Platform.SetRenderer(header, _headerRenderer);
}
void UpdateIsRefreshing(bool isInitialValue = false)
{
if (_refresh != null)
{
var isRefreshing = Element.IsRefreshing;
if (isRefreshing && isInitialValue)
{
_refresh.Refreshing = false;
_refresh.Post(() =>
{
if (_refresh.IsDisposed())
return;
_refresh.Refreshing = true;
});
}
else
_refresh.Refreshing = isRefreshing;
}
}
void UpdateIsSwipeToRefreshEnabled()
{
if (_refresh != null)
_refresh.Enabled = Element.IsPullToRefreshEnabled && (Element as IListViewController).RefreshAllowed;
}
void UpdateFastScrollEnabled()
{
if (Control != null)
{
Control.FastScrollEnabled = Element.OnThisPlatform().IsFastScrollEnabled();
}
}
void UpdateSelectionMode()
{
if (Control != null)
{
if (Element.SelectionMode == ListViewSelectionMode.None)
{
Control.ChoiceMode = ChoiceMode.None;
Element.SelectedItem = null;
}
else if (Element.SelectionMode == ListViewSelectionMode.Single)
{
Control.ChoiceMode = ChoiceMode.Single;
}
}
}
void UpdateSpinnerColor()
{
if (_refresh != null && Element.RefreshControlColor != null)
_refresh.SetColorSchemeColors(Element.RefreshControlColor.ToAndroid());
}
void UpdateHorizontalScrollBarVisibility()
{
if (_defaultHorizontalScrollVisibility == 0)
{
_defaultHorizontalScrollVisibility = Control.HorizontalScrollBarEnabled ? ScrollBarVisibility.Always : ScrollBarVisibility.Never;
}
var newHorizontalScrollVisiblility = Element.HorizontalScrollBarVisibility;
if (newHorizontalScrollVisiblility == ScrollBarVisibility.Default)
{
newHorizontalScrollVisiblility = _defaultHorizontalScrollVisibility;
}
Control.HorizontalScrollBarEnabled = newHorizontalScrollVisiblility == ScrollBarVisibility.Always;
}
void UpdateVerticalScrollBarVisibility()
{
if (_defaultVerticalScrollVisibility == 0)
_defaultVerticalScrollVisibility = Control.VerticalScrollBarEnabled ? ScrollBarVisibility.Always : ScrollBarVisibility.Never;
var newVerticalScrollVisibility = Element.VerticalScrollBarVisibility;
if (newVerticalScrollVisibility == ScrollBarVisibility.Default)
newVerticalScrollVisibility = _defaultVerticalScrollVisibility;
Control.VerticalScrollBarEnabled = newVerticalScrollVisibility == ScrollBarVisibility.Always;
}
internal class Container : ViewGroup
{
IVisualElementRenderer _child;
public Container(IntPtr p, global::Android.Runtime.JniHandleOwnership o) : base(p, o)
{
// Added default constructor to prevent crash when accessing header/footer row in ListViewAdapter.Dispose
}
public Container(Context context) : base(context)
{
}
public IVisualElementRenderer Child
{
set
{
if (_child != null)
RemoveView(_child.View);
_child = value;
if (value != null)
AddView(value.View);
}
}
protected override void OnLayout(bool changed, int l, int t, int r, int b)
{
if (_child == null)
return;
_child.UpdateLayout();
}
protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
if (_child?.Element == null)
{
SetMeasuredDimension(0, 0);
return;
}
VisualElement element = _child.Element;
Context ctx = Context;
var width = (int)ctx.FromPixels(MeasureSpecFactory.GetSize(widthMeasureSpec));
SizeRequest request = element.Measure(width, double.PositiveInfinity, MeasureFlags.IncludeMargins);
Microsoft.Maui.Controls.Compatibility.Layout.LayoutChildIntoBoundingRegion(element, new Rect(0, 0, width, request.Request.Height));
int widthSpec = MeasureSpecFactory.MakeMeasureSpec((int)ctx.ToPixels(width), MeasureSpecMode.Exactly);
int heightSpec = MeasureSpecFactory.MakeMeasureSpec((int)ctx.ToPixels(request.Request.Height), MeasureSpecMode.Exactly);
_child.View.Measure(widthMeasureSpec, heightMeasureSpec);
SetMeasuredDimension(widthSpec, heightSpec);
}
}
class SwipeRefreshLayoutWithFixedNestedScrolling : SwipeRefreshLayout
{
float _touchSlop;
float _initialDownY;
bool _nestedScrollAccepted;
bool _nestedScrollCalled;
public SwipeRefreshLayoutWithFixedNestedScrolling(Context ctx) : base(ctx)
{
_touchSlop = ViewConfiguration.Get(ctx).ScaledTouchSlop;
}
public override bool OnInterceptTouchEvent(MotionEvent ev)
{
if (ev.Action == MotionEventActions.Down)
_initialDownY = ev.GetAxisValue(Axis.Y);
var isBeingDragged = base.OnInterceptTouchEvent(ev);
if (!isBeingDragged && ev.Action == MotionEventActions.Move && _nestedScrollAccepted && !_nestedScrollCalled)
{
var y = ev.GetAxisValue(Axis.Y);
var dy = (y - _initialDownY) / 2;
isBeingDragged = dy > _touchSlop;
}
return isBeingDragged;
}
public override void OnNestedScrollAccepted(AView child, AView target, [GeneratedEnum] ScrollAxis axes)
{
base.OnNestedScrollAccepted(child, target, axes);
_nestedScrollAccepted = true;
_nestedScrollCalled = false;
}
public override void OnStopNestedScroll(AView child)
{
base.OnStopNestedScroll(child);
_nestedScrollAccepted = false;
}
public override void OnNestedScroll(AView target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)
{
base.OnNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
_nestedScrollCalled = true;
}
}
class ListViewScrollDetector : Java.Lang.Object, AbsListView.IOnScrollListener
{
class TrackElement
{
public TrackElement(int position)
{
_position = position;
}
readonly int _position;
AView _trackedView;
int _trackedViewPrevPosition;
int _trackedViewPrevTop;
public void SyncState(AbsListView view)
{
if (view.ChildCount > 0)
{
_trackedView = GetChild(view);
_trackedViewPrevTop = GetY();
_trackedViewPrevPosition = view.GetPositionForView(_trackedView);
}
}
public void Reset()
{
_trackedView = null;
}
public bool IsSafeToTrack(AbsListView view)
{
return _trackedView != null && _trackedView.Parent == view && view.GetPositionForView(_trackedView) == _trackedViewPrevPosition;
}
public int GetDeltaY()
{
return GetY() - _trackedViewPrevTop;
}
AView GetChild(AbsListView view)
{
switch (_position)
{
case 0:
return view.GetChildAt(0);
case 1:
case 2:
return view.GetChildAt(view.ChildCount / 2);
case 3:
return view.GetChildAt(view.ChildCount - 1);
default:
return null;
}
}
int GetY()
{
return _position <= 1 ? _trackedView.Bottom : _trackedView.Top;
}
}
readonly ListView _element;
readonly float _density;
int _contentOffset;
public ListViewScrollDetector(ListViewRenderer renderer)
{
_element = renderer.Element;
_density = renderer.Context.Resources.DisplayMetrics.Density;
}
void SendScrollEvent(double y)
{
var element = _element;
double offset = Math.Abs(y) / _density;
var args = new ScrolledEventArgs(0, offset);
element?.SendScrolled(args);
}
readonly TrackElement[] _trackElements =
{
new TrackElement(0), // Top view, bottom Y
new TrackElement(1), // Mid view, bottom Y
new TrackElement(2), // Mid view, top Y
new TrackElement(3) // Bottom view, top Y
};
public void OnScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
{
var wasTracked = false;
foreach (TrackElement t in _trackElements)
{
if (!wasTracked)
{
if (t.IsSafeToTrack(view))
{
wasTracked = true;
_contentOffset += t.GetDeltaY();
SendScrollEvent(_contentOffset);
t.SyncState(view);
}
else
{
t.Reset();
t.SyncState(view);
}
}
else
{
t.SyncState(view);
}
}
}
public void OnScrollStateChanged(AbsListView view, ScrollState scrollState)
{
if (scrollState == ScrollState.TouchScroll || scrollState == ScrollState.Fling)
{
foreach (TrackElement t in _trackElements)
{
t.SyncState(view);
}
}
}
}
}
}
|