自定义下拉刷新不适用于 iOS

Posted

技术标签:

【中文标题】自定义下拉刷新不适用于 iOS【英文标题】:Custom Pull-To-Refresh not working on iOS 【发布时间】:2018-03-11 08:42:14 【问题描述】:

我正在按照 James Montemagno 的教程为我的布局添加下拉刷新支持,它在 android 上完美运行,但当我导航到与 Android 相同的页面时,ios 会生成以下错误。

System.InvalidCastException:

我试图显示的页面是一个简单的 StackLayout,它再次在 Android 上完美运行。

这是本教程中我的 iOS 渲染器类

[assembly: ExportRenderer(typeof(RefreshableLayout), typeof(RefreshableLayoutiOS))]
namespace SocialNetwork.iOS.Renderers

[Preserve(AllMembers = true)]
public class RefreshableLayoutiOS : ViewRenderer<RefreshableLayout, UIView>

    public async static void Init()
    
        var temp = DateTime.Now;
    

    UIRefreshControl refreshControl;

    protected override void OnElementChanged(ElementChangedEventArgs<RefreshableLayout> e)
    
        base.OnElementChanged(e);

        if (e.OldElement != null || Element == null)
            return;

        refreshControl = new UIRefreshControl();

        refreshControl.ValueChanged += OnRefresh;

        try
        
            TryInsertRefresh(this);
        
        catch (Exception ex)
        
            Debug.WriteLine("View is not supported in PullToRefreshLayout: " + ex);
        

        UpdateColors();
        UpdateIsRefreshing();
        UpdateIsSwipeToRefreshEnabled();
    

    bool set;
    nfloat origininalY;

    bool TryOffsetRefresh(UIView view, bool refreshing, int index = 0)
    
        if (view is UITableView)
        
            var uiTableView = view as UITableView;
            if (!set)
            
                origininalY = uiTableView.ContentOffset.Y;
                set = true;
            

            if (refreshing)
                uiTableView.SetContentOffset(new CoreGraphics.CGPoint(0, origininalY - refreshControl.Frame.Size.Height), true);
            else
                uiTableView.SetContentOffset(new CoreGraphics.CGPoint(0, origininalY), true);
            return true;
        

        if (view is UICollectionView)
        

            var uiCollectionView = view as UICollectionView;
            if (!set)
            
                origininalY = uiCollectionView.ContentOffset.Y;
                set = true;
            
            if (refreshing)
                uiCollectionView.SetContentOffset(new CoreGraphics.CGPoint(0, origininalY - refreshControl.Frame.Size.Height), true);
            else
                uiCollectionView.SetContentOffset(new CoreGraphics.CGPoint(0, origininalY), true);
            return true;
        


        if (view is UIWebView)
        
            //can't do anything
            return true;
        


        if (view is UIScrollView)
        
            var uiScrollView = view as UIScrollView;

            if (!set)
            
                origininalY = uiScrollView.ContentOffset.Y;
                set = true;
            
            if (refreshing)
                uiScrollView.SetContentOffset(new CoreGraphics.CGPoint(0, origininalY - refreshControl.Frame.Size.Height), true);
            else
                uiScrollView.SetContentOffset(new CoreGraphics.CGPoint(0, origininalY), true);
            return true;
        

        if (view.Subviews == null)
            return false;

        for (int i = 0; i < view.Subviews.Length; i++)
        
            var control = view.Subviews[i];
            if (TryOffsetRefresh(control, refreshing, i))
                return true;
        

        return false;
    


    bool TryInsertRefresh(UIView view, int index = 0)
    


        if (view is UITableView)
        
            var uiTableView = view as UITableView;
            uiTableView = view as UITableView;
            view.InsertSubview(refreshControl, index);
            return true;
        


        if (view is UICollectionView)
        
            var uiCollectionView = view as UICollectionView;
            uiCollectionView = view as UICollectionView;
            view.InsertSubview(refreshControl, index);
            return true;
        


        if (view is UIWebView)
        
            var uiWebView = view as UIWebView;
            uiWebView.ScrollView.InsertSubview(refreshControl, index);
            return true;
        

        if (view is UIScrollView)
        
            var uiScrollView = view as UIScrollView;
            view.InsertSubview(refreshControl, index);
            uiScrollView.AlwaysBounceVertical = true;
            return true;
        

        if (view.Subviews == null)
            return false;

        for (int i = 0; i < view.Subviews.Length; i++)
        
            var control = view.Subviews[i];
            if (TryInsertRefresh(control, i))
                return true;
        

        return false;
    

    BindableProperty rendererProperty;

    BindableProperty RendererProperty
    
        get
        
            if (rendererProperty != null)
                return rendererProperty;

            var type = Type.GetType("Xamarin.Forms.Platform.iOS.Platform, Xamarin.Forms.Platform.iOS");
            var prop = type.GetField("RendererProperty");
            var val = prop.GetValue(null);
            rendererProperty = val as BindableProperty;

            return rendererProperty;
        
    

    void UpdateColors()
    
        if (RefreshView == null)
            return;
        if (RefreshView.RefreshColor != Color.Default)
            refreshControl.TintColor = RefreshView.RefreshColor.ToUIColor();
        if (RefreshView.RefreshBackgroundColor != Color.Default)
            refreshControl.BackgroundColor = RefreshView.RefreshBackgroundColor.ToUIColor();
    


    void UpdateIsRefreshing()
    
        IsRefreshing = RefreshView.IsRefreshing;
    

    void UpdateIsSwipeToRefreshEnabled()
    
        refreshControl.Enabled = RefreshView.IsPullToRefreshEnabled;
    

    public RefreshableLayout RefreshView
    
        get  return Element; 
    



    bool isRefreshing;

    public bool IsRefreshing
    
        get  return isRefreshing; 
        set
        
            bool changed = IsRefreshing != value;

            isRefreshing = value;
            if (isRefreshing)
                refreshControl.BeginRefreshing();
            else
                refreshControl.EndRefreshing();

            if (changed)
                TryOffsetRefresh(this, IsRefreshing);
        
    

    void OnRefresh(object sender, EventArgs e)
    
        if (RefreshView?.RefreshCommand?.CanExecute(RefreshView?.RefreshCommandParameter) ?? false)
        
            RefreshView.RefreshCommand.Execute(RefreshView?.RefreshCommandParameter);
        
    

    protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
    
        base.OnElementPropertyChanged(sender, e);
        if (e.PropertyName == RefreshableLayout.IsPullToRefreshEnabledProperty.PropertyName)
            UpdateIsSwipeToRefreshEnabled();
        else if (e.PropertyName == RefreshableLayout.IsRefreshingProperty.PropertyName)
            UpdateIsRefreshing();
        else if (e.PropertyName == RefreshableLayout.RefreshColorProperty.PropertyName)
            UpdateColors();
        else if (e.PropertyName == RefreshableLayout.RefreshBackgroundColorProperty.PropertyName)
            UpdateColors();
    

    protected override void Dispose(bool disposing)
    
        base.Dispose(disposing);
        if (refreshControl != null)
        
            refreshControl.ValueChanged -= OnRefresh;
        
    


我得到了这个tutorial 和这个GitHub 的代码

编辑:

XAML

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
         xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
         x:Class="SocialNetwork.TestScrollPage" xmlns:local="clr-namespace:SocialNetwork.Renderers">
<ContentPage.Content>
    <StackLayout>
        <local:RefreshableLayout x:Name="RefreshableLayout" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
            <StackLayout>

            </StackLayout>
        </local:RefreshableLayout>
    </StackLayout>
</ContentPage.Content>

CS:

public partial class TestScrollPage : ContentPage

    public TestScrollPage ()
    
        InitializeComponent ();
        RefreshableLayout.RefreshCommand = new Command(() => RefreshPage());
    

    public void RefreshPage()
    
        RefreshableLayout.IsRefreshing = false;
        DisplayAlert("ok", "ok", "ok");
    

然后我使用Detail = new TestScrollPage();导航到页面

编辑 2:

    public partial class RefreshableLayout : ContentView

    public static readonly BindableProperty IsRefreshingProperty =
        BindableProperty.Create(nameof(IsRefreshing), typeof(bool), typeof(RefreshableLayout), false);

    /// <summary>
    /// Gets or sets a value indicating whether this instance is refreshing.
    /// </summary>
    /// <value><c>true</c> if this instance is refreshing; otherwise, <c>false</c>.</value>
    public bool IsRefreshing
    
        get  return (bool)GetValue(IsRefreshingProperty); 
        set
        
            if ((bool)GetValue(IsRefreshingProperty) == value)
                OnPropertyChanged(nameof(IsRefreshing));

            SetValue(IsRefreshingProperty, value);
        
    

    /// <summary>
    /// The is pull to refresh enabled property.
    /// </summary>
    public static readonly BindableProperty IsPullToRefreshEnabledProperty =
        BindableProperty.Create(nameof(IsPullToRefreshEnabled), typeof(bool), typeof(RefreshableLayout), true);

    /// <summary>
    /// Gets or sets a value indicating whether this instance is pull to refresh enabled.
    /// </summary>
    /// <value><c>true</c> if this instance is pull to refresh enabled; otherwise, <c>false</c>.</value>
    public bool IsPullToRefreshEnabled
    
        get  return (bool)GetValue(IsPullToRefreshEnabledProperty); 
        set  SetValue(IsPullToRefreshEnabledProperty, value); 
    


    /// <summary>
    /// The refresh command property.
    /// </summary>
    public static readonly BindableProperty RefreshCommandProperty =
        BindableProperty.Create(nameof(RefreshCommand), typeof(ICommand), typeof(RefreshableLayout));

    /// <summary>
    /// Gets or sets the refresh command.
    /// </summary>
    /// <value>The refresh command.</value>
    public ICommand RefreshCommand
    
        get  return (ICommand)GetValue(RefreshCommandProperty); 
        set  SetValue(RefreshCommandProperty, value); 
    

    /// <summary>
    /// Gets the Refresh command 
    /// </summary>
    public static readonly BindableProperty RefreshCommandParameterProperty =
        BindableProperty.Create(nameof(RefreshCommandParameter),
            typeof(object),
            typeof(RefreshableLayout),
            null,
            propertyChanged: (bindable, oldvalue, newvalue) => ((RefreshableLayout)bindable).RefreshCommandCanExecuteChanged(bindable, EventArgs.Empty));

    /// <summary>
    /// Gets or sets the Refresh command parameter
    /// </summary>
    public object RefreshCommandParameter
    
        get  return GetValue(RefreshCommandParameterProperty); 
        set  SetValue(RefreshCommandParameterProperty, value); 
    

    /// <summary>
    /// Executes if enabled or not based on can execute changed
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="eventArgs"></param>
    void RefreshCommandCanExecuteChanged(object sender, EventArgs eventArgs)
    
        ICommand cmd = RefreshCommand;
        if (cmd != null)
            IsEnabled = cmd.CanExecute(RefreshCommandParameter);
    

    /// <summary>
    /// Color property of refresh spinner color 
    /// </summary>
    public static readonly BindableProperty RefreshColorProperty =
        BindableProperty.Create(nameof(RefreshColor), typeof(Color), typeof(RefreshableLayout), Color.Default);

    /// <summary>
    /// Refresh  color
    /// </summary>
    public Color RefreshColor
    
        get  return (Color)GetValue(RefreshColorProperty); 
        set  SetValue(RefreshColorProperty, value); 
    



    /// <summary>
    /// Color property of refresh background color
    /// </summary>
    public static readonly BindableProperty RefreshBackgroundColorProperty =
        BindableProperty.Create(nameof(RefreshBackgroundColor), typeof(Color), typeof(RefreshableLayout), Color.Default);

    /// <summary>
    /// Refresh background color
    /// </summary>
    public Color RefreshBackgroundColor
    
        get  return (Color)GetValue(RefreshBackgroundColorProperty); 
        set  SetValue(RefreshBackgroundColorProperty, value); 
    


    /// <param name="widthConstraint">The available width for the element to use.</param>
    /// <param name="heightConstraint">The available height for the element to use.</param>
    /// <summary>
    /// Optimization as we can get the size here of our content all in DIP
    /// </summary>
    protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
    
        if (Content == null)
            return new SizeRequest(new Size(100, 100));

        return base.OnMeasure(widthConstraint, heightConstraint);
    

【问题讨论】:

哪一行给出了 InvalidCastException? 运行Detail = new TestScrollPage();时发生 您使用的是主从页面吗?发布您的共享代码,从哪里引发异常? @hichame.yessou 我知道它不是页面,因为它只发生在渲染器中,如果我从 iOS 中删除渲染器类,它会加载页面,只是没有拉动刷新能力。 @Dan 无法从您的代码中分析更多内容。你能提供一个小样本吗? 【参考方案1】:

请阅读此documentation 关于 Xamarin Liver Player 的内容。它声明了限制:

Xamarin 表单不支持自定义渲染器。

在您使用 Xamarin Liver Player 时还有一些其他限制或问题。所以我建议您使用模拟器或真实的物理设备来测试您的项目。

如果您没有 Mac。您也可以尝试下载 Enterprise Visual Studio,让模拟器映射到 Windows。

【讨论】:

以上是关于自定义下拉刷新不适用于 iOS的主要内容,如果未能解决你的问题,请参考以下文章

iOS用Sketch制作APP下拉刷新的GIF动画

自定义单元格的刷新控制问题 - Swift 4 IOS 11

ios实现下拉刷新,上拉加载

IOS 常用UI控件

小程序怎么实现下拉刷新

Android自定义下拉刷新动画--仿百度外卖下拉刷新