ListView: avoid that disabling RefreshAllowed cancels refresh indicator on Android, fixes #8384 (#14816)
* Add test for issue #8384 * ListView: avoid that disabling RefreshAllowed cancels refresh indicator on Android, fixes #8384 RefreshAllowed is bound to ListView.RefreshCommand.CanExecute(). Often an implementation of RefreshCommand might update its CanExecute status after execution starts. This caused ListViewRenderer to immediately disable the SwipeRefreshLayout, thereby cancelling the refresh/activity indicator on top of the list view. The solution is to NOT disable it while refreshing, but waiting for the next chance when current refresh activity/command is done. * Only enable Issue8384 constructor for XF controls app build (not for UI unit tests) Otherwise we get 'InitializeComponent' not found error when compiling UI unit test projects.
This commit is contained in:
Родитель
e3faa59566
Коммит
085ad8796c
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Title="Test 8384"
|
||||
x:Class="Xamarin.Forms.Controls.Issues.Issue8384">
|
||||
|
||||
<StackLayout>
|
||||
<Label Text="Test for issue #8384" />
|
||||
<ListView IsPullToRefreshEnabled="True" ItemsSource="{Binding Items}" RefreshCommand="{Binding Refresh}"
|
||||
IsRefreshing="{Binding IsRefreshing}">
|
||||
</ListView>
|
||||
<StackLayout>
|
||||
<Label Text="Test steps (Android):" />
|
||||
<Label Text="1. Swipe to refresh the list (refresh takes 4 seconds)" />
|
||||
<Label Text="2. Refresh/busy indicator appears at the top" />
|
||||
<Label Text="3. Refresh/busy indicator should remain visible until list content changes (refresh is finished).
|
||||
due to issue #8384 the indicator vanishes too early if Command.CanExecute toggles (in this test after 1 second)." />
|
||||
<Label Text="4. Repeat steps 1 to 3 multiple times" />
|
||||
</StackLayout>
|
||||
</StackLayout>
|
||||
</ContentPage>
|
|
@ -0,0 +1,145 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Xamarin.Forms;
|
||||
using Xamarin.Forms.CustomAttributes;
|
||||
using Xamarin.Forms.Internals;
|
||||
|
||||
namespace Xamarin.Forms.Controls.Issues
|
||||
{
|
||||
// Learn more about making custom code visible in the Xamarin.Forms previewer
|
||||
// by visiting https://aka.ms/xamarinforms-previewer
|
||||
[DesignTimeVisible(false)]
|
||||
[Preserve(AllMembers = true)]
|
||||
[Issue(IssueTracker.Github, 8384,
|
||||
"[Bug] [5.0] [Android] [Bug] ListView RefreshCommand ActivityIndicator does disappear on Android if CanExecute is changed to false",
|
||||
PlatformAffected.Android)]
|
||||
public partial class Issue8384 : ContentPage
|
||||
{
|
||||
public Issue8384()
|
||||
{
|
||||
#if APP
|
||||
InitializeComponent();
|
||||
BindingContext = new ViewModelIssue8384();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
class ViewModelIssue8384 : INotifyPropertyChanged
|
||||
{
|
||||
public class MyCommand : Command
|
||||
{
|
||||
private bool _allow;
|
||||
|
||||
public MyCommand(Action<object> execute, Func<object, bool> canExecute) : base(execute, canExecute)
|
||||
{
|
||||
Allow = true;
|
||||
}
|
||||
|
||||
public bool Allow
|
||||
{
|
||||
get
|
||||
{
|
||||
return _allow;
|
||||
}
|
||||
set
|
||||
{
|
||||
_allow = value;
|
||||
ChangeCanExecute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<string> _items;
|
||||
private bool _isRefreshing;
|
||||
private MyCommand _refresh;
|
||||
|
||||
static readonly List<string> FIRST_LIST = new List<string>() {
|
||||
"one", "two", "three"
|
||||
};
|
||||
|
||||
static readonly List<string> SECOND_LIST = new List<string>() {
|
||||
"four", "five", "six"
|
||||
};
|
||||
|
||||
public bool IsRefreshing
|
||||
{
|
||||
get
|
||||
{
|
||||
return _isRefreshing;
|
||||
}
|
||||
set
|
||||
{
|
||||
_isRefreshing = value;
|
||||
OnPropertyChanged("IsRefreshing");
|
||||
}
|
||||
}
|
||||
|
||||
public ViewModelIssue8384()
|
||||
{
|
||||
Items = FIRST_LIST;
|
||||
|
||||
Refresh = new MyCommand(Execute, CanExecute);
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
private void OnPropertyChanged(string propertyName)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
private async void Execute(object parameter)
|
||||
{
|
||||
IsRefreshing = true;
|
||||
Debug.WriteLine("Refresh start");
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
|
||||
// Side note: doing this off the main thread throws an exception
|
||||
Device.BeginInvokeOnMainThread(() => { _refresh.Allow = false; });
|
||||
|
||||
await Task.Delay(3000).ConfigureAwait(false);
|
||||
Items = (Items == FIRST_LIST) ? SECOND_LIST : FIRST_LIST;
|
||||
|
||||
Debug.WriteLine("Refresh end");
|
||||
IsRefreshing = false;
|
||||
|
||||
Device.BeginInvokeOnMainThread(() => { _refresh.Allow = true; });
|
||||
}
|
||||
|
||||
private bool CanExecute(object parameter)
|
||||
{
|
||||
return _refresh.Allow;
|
||||
}
|
||||
|
||||
public List<string> Items
|
||||
{
|
||||
get
|
||||
{
|
||||
return _items;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
_items = value;
|
||||
OnPropertyChanged("Items");
|
||||
}
|
||||
}
|
||||
|
||||
public MyCommand Refresh
|
||||
{
|
||||
get
|
||||
{
|
||||
return _refresh;
|
||||
}
|
||||
set
|
||||
{
|
||||
_refresh = value;
|
||||
OnPropertyChanged("Refresh");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1809,6 +1809,7 @@
|
|||
<Compile Include="$(MSBuildThisFileDirectory)Issue14697.xaml.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Issue8383.xaml.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Issue8383-2.xaml.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Issue8384.xaml.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Issue13577.xaml.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Issue14505.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Issue14505-II.cs" />
|
||||
|
@ -2320,6 +2321,9 @@
|
|||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Issue8383-2.xaml">
|
||||
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Issue8384.xaml">
|
||||
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="$(MSBuildThisFileDirectory)Issue13577.xaml">
|
||||
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using Android.Content;
|
||||
using Android.Runtime;
|
||||
using Android.Views;
|
||||
|
@ -428,13 +429,26 @@ namespace Xamarin.Forms.Platform.Android
|
|||
}
|
||||
else
|
||||
_refresh.Refreshing = isRefreshing;
|
||||
|
||||
// Allow to disable SwipeToRefresh layout AFTER refresh is done
|
||||
UpdateIsSwipeToRefreshEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateIsSwipeToRefreshEnabled()
|
||||
{
|
||||
if (_refresh != null)
|
||||
_refresh.Enabled = Element.IsPullToRefreshEnabled && (Element as IListViewController).RefreshAllowed;
|
||||
{
|
||||
var isEnabled = Element.IsPullToRefreshEnabled && (Element as IListViewController).RefreshAllowed;
|
||||
_refresh.Post(() =>
|
||||
{
|
||||
// NOTE: only disable while NOT refreshing, otherwise Command bindings CanExecute behavior will effectively
|
||||
// cancel refresh animation. If not possible right now we will be called by UpdateIsRefreshing().
|
||||
// For details see https://github.com/xamarin/Xamarin.Forms/issues/8384
|
||||
if (isEnabled || !_refresh.Refreshing)
|
||||
_refresh.Enabled = isEnabled;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateFastScrollEnabled()
|
||||
|
|
Загрузка…
Ссылка в новой задаче