【问题标题】:How do I hide Xamarin.Forms TableView header/footer with a custom renderer on iOS?如何在 iOS 上使用自定义渲染器隐藏 Xamarin.Forms TableView 页眉/页脚?
【发布时间】:2018-06-12 05:33:43
【问题描述】:

我想创建一个页面布局,在其中显示多个 TableViews 以​​及其他视图,例如每个视图之间的 Label

在此过程中,我需要具备以下能力:

  1. 使TableView 不默认为垂直扩展布局选项(默认值似乎不可更改),以便它们可以垂直堆叠在StackLayout 中。
  2. 隐藏相应的UITableView 部分页眉/页脚,以便它们之间没有多余的垂直间距。

我通过以下CustomTableView 控件实现了这一点:

public class CustomTableView : TableView
{
    public bool NoHeader { get; set; }
    public bool NoFooter { get; set; }

    protected override SizeRequest OnSizeRequest(double widthConstraint, double heightConstraint)
    {
        if(!VerticalOptions.Expands)
        {
            // Call OnSizeRequest that is overwritten in custom renderer
            var baseOnSizeRequest = GetVisualElementOnSizeRequest();
            return baseOnSizeRequest(widthConstraint, heightConstraint);
        }

        return base.OnSizeRequest(widthConstraint, heightConstraint);
    }

    public Func<double, double, SizeRequest> GetVisualElementOnSizeRequest()
    {
        var handle = typeof(VisualElement).GetMethod(
            "OnSizeRequest",
            BindingFlags.Instance | BindingFlags.NonPublic,
            null,
            new Type[] { typeof(double), typeof(double) },
            null)?.MethodHandle;

        var pointer = handle.Value.GetFunctionPointer();
        return (Func<double, double, SizeRequest>)Activator.CreateInstance(
            typeof(Func<double, double, SizeRequest>), this, pointer);
    }
}

以及遵循 this SO answer 的建议的自定义 iOS 渲染器,用于隐藏 UITableView 页眉/页脚:

public class CustomTableViewRenderer : TableViewRenderer
{
    public override SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint)
    {
        Control.LayoutIfNeeded();
        var size = new Size(Control.ContentSize.Width, Control.ContentSize.Height);
        return new SizeRequest(size);
    }

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

        if(e.NewElement is CustomTableView view)
        {
            Control.ScrollEnabled = false;
            SetSource();
        }
    }

    protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        base.OnElementPropertyChanged(sender, e);
        var view = (CustomTableView)Element;
        if(e.PropertyName == TableView.HasUnevenRowsProperty.PropertyName)
        {
            SetSource();
        }
    }

    private void SetSource()
    {
        var view = (CustomTableView)Element;
        if(view.NoFooter || view.NoHeader)
        {
            Control.Source = new CustomTableViewModelRenderer(view);
        }
        else
        {
            Control.Source = Element.HasUnevenRows ? new UnEvenTableViewModelRenderer(Element) :
                new TableViewModelRenderer(Element);
        }
    }

    public class CustomTableViewModelRenderer : UnEvenTableViewModelRenderer
    {
        private readonly CustomTableView _view;

        public CustomTableViewModelRenderer(CustomTableView model)
            : base(model)
        {
            _view = model;
        }

        public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath)
        {
            if(_view.HasUnevenRows)
            {
                return UITableView.AutomaticDimension;
            }

            return base.GetHeightForRow(tableView, indexPath);
        }

        public override nfloat GetHeightForHeader(UITableView tableView, nint section)
        {
            if(_view.NoHeader)
            {
                return 0.00001f;
            }

            return base.GetHeightForHeader(tableView, section);
        }

        public override UIView GetViewForHeader(UITableView tableView, nint section)
        {
            if(_view.NoHeader)
            {
                return new UIView(new CGRect(0, 0, 0, 0));
            }

            return base.GetViewForHeader(tableView, section);
        }

        public override nfloat GetHeightForFooter(UITableView tableView, nint section)
        {
            if(_view.NoFooter)
            {
                return 0.00001f;
            }

            return 10f;
        }

        public override UIView GetViewForFooter(UITableView tableView, nint section)
        {
            if(_view.NoFooter)
            {
                return new UIView(new CGRect(0, 0, 0, 0));
            }

            return base.GetViewForFooter(tableView, section);
        }
    }
}

然后我创建一个简单的 Xamarin.Forms ContentPage 来实现它:

public class TablePage : ContentPage
{
    public TablePage()
    {
        Table1 = new PageTableView
        {
            BackgroundColor = Color.Cyan,
            Root = new TableRoot
            {
                new TableSection("Table 1")
                {
                    new LabelEntryTableCell("Label A"),
                    new LabelEntryTableCell("Label B")
                }
            }
        };

        var label1 = new Label
        {
            Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
                "Vivamus accumsan lacus orci. Nulla in enim erat.",
            Margin = new Thickness(15, 5, 15, 0),
            BackgroundColor = Color.Yellow
        };

        Table2 = new PageTableView
        {
            BackgroundColor = Color.Red,
            Root = new TableRoot
            {
                new TableSection("Table 2")
                {
                    new LabelEntryTableCell("Label C"),
                    new LabelEntryTableCell("Label D")
                }
            }
        };

        var label2 = new Label
        {
            Text = "Duis vulputate mattis elit. " +
                "Donec vulputate lorem vitae elit posuere, quis consequat ligula imperdiet.",
            Margin = new Thickness(15, 5, 15, 0),
            BackgroundColor = Color.Green
        };

        StackLayout = new StackLayout
        {
            Children = { Table1, label1, Table2, label2 },
            Spacing = 0
        };

        BackgroundColor = Color.Gray;
        Title = "Custom Table View";
        Content = new ScrollView
        {
            Content = StackLayout
        };
    }

    public PageTableView Table1 { get; set; }
    public PageTableView Table2 { get; set; }
    public StackLayout StackLayout { get; set; }
}

public class PageTableView : CustomTableView
{
    public PageTableView()
    {
        Intent = TableIntent.Settings;
        HasUnevenRows = true;
        RowHeight = -1;
        VerticalOptions = LayoutOptions.Start;
        NoFooter = true;
    }
}

public class LabelEntryTableCell : ViewCell
{
    public LabelEntryTableCell(string labelText)
    {
        var label = new Label { Text = labelText };
        var entry = new Entry();

        View = new StackLayout
        {
            Children = { label, entry },
            BackgroundColor = Color.White,
            Padding = 10
        };
    }
}

这一切似乎都按预期工作,但是,当TablePage 第一次显示CustomTableView 的大部分内容时,会被截断。然后我可以将设备旋转到横向,问题就会自行纠正,按照我的意图显示我的布局。然后将设备旋转回纵向仍然显示正确的布局,CustomTableView 的更多信息也不再在该方向上被截断。这种情况每次都会发生。在此处查看演示:

我也可以通过将其添加到 TablePage 来解决问题:

protected override void OnAppearing()
{
    base.OnAppearing();

    // This will also fix the problem, but you see a flash of the old layout
    // when the page first displays.

    StackLayout.RedrawLayout();
}

...

public class CustomStackLayout : StackLayout
{
    public void RedrawLayout()
    {
        InvalidateLayout();
    }
}

我该如何解决这个问题?每当我执行旋转/OnAppearing hack 来纠正它时,我的自定义控件/渲染器/页面都会按预期工作,但我不知道为什么。看来我需要在UITableView 上调用一些重绘方法。

示例项目

如果您想查看此隔离测试场景的完整源代码/项目,您可以在 GitHub here 上找到它。

【问题讨论】:

    标签: xamarin.forms xamarin.ios


    【解决方案1】:

    我在 iOS 中知道这个问题。当tableView第一次加载时,即使我们写了LayoutIfNeeded(),我们也无法得到正确的contentSize。但是在页面出现之后我们刷新它,我们可以得到正确的 contentSize。这就是为什么

    这也将解决问题,但您会看到旧布局的闪光

    解决方案1:我的建议是我们可以尝试在tableView显示时延迟几毫秒刷新tableView:

    在 TablePage 中,我订阅了一个 MessagingCenter 来刷新布局

    MessagingCenter.Subscribe<object>(this, "Refresh", (sender) =>
    {
        StackLayout.RedrawLayout();
    });
    

    然后我在渲染器中发送消息:

    //I define this bool field to make sure this message only be sent at the first time after tableView loaded
    private bool shouldRefresh = true;
    
    public override async void WillDisplay(UITableView tableView, UITableViewCell cell, NSIndexPath indexPath)
    {
    
        if (tableView.NumberOfRowsInSection(0) - 1 == indexPath.Row)
        {
            if (shouldRefresh)
            {
                await Task.Delay(10);
                MessagingCenter.Send<object>(this, "Refresh");
                shouldRefresh = false;
            }  
        }
    }
    

    Solutin2:我们也可以尝试定义一个估计的 rowHeight 以在 tableView 首次加载时给出 contentSize:

    public override nfloat EstimatedHeight(UITableView tableView, NSIndexPath indexPath)
    {
        return 100;
    }
    

    但在这个解决方案中,高度必须由我们自己预先定义和计算。

    【讨论】:

    • 感谢有关覆盖 WillDisplay 的提示。我能够根据这个想法创建一个解决方案,尽管没有使用MessagingCenter。我刚刚将包装CustomStackLayout 的引用传递给CustomTableView,并在渲染器中从WillDisplay 调用RedrawLayout。有效,但似乎仍然有点老套。如果可能的话,我希望能够以某种方式最初计算出正确的高度。如果没有其他好的解决方案,我会接受你的回答。
    • @kspearrin 是的,我知道如果我们能找到一种在初始时间计算高度的方法是最好的。
    • @kspearrin 打扰一下,您找到解决这个问题的更好方法了吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-01-26
    • 1970-01-01
    • 1970-01-01
    • 2018-10-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多