如何使用 Xamarin Forms 创建具有垂直粘性标题和水平粘性第一列的表格? [英] How to create a table with vertically sticky header and horizontally sticky first column using Xamarin Forms?

查看:41
本文介绍了如何使用 Xamarin Forms 创建具有垂直粘性标题和水平粘性第一列的表格?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在展示表格数据时,我认为在某些情况下,有一个始终可见的标题行和一个始终可见的第一列确实可以提高表格的可读性和整体可用性,尤其是在表格中有大量数据的情况下.当表格必须同时支持水平和垂直滚动时,就会出现问题.在查看过去比赛的比分时,可以从 NBA 应用程序中找到这种表格的一个很好的例子.这是来自 NBA Android 应用程序的示例图像:来自 NBA 移动应用程序的示例表格

When displaying tabular data, I think that in some cases having an always visible header row and an always visible first column can really improve the readability and the overall usability of a table, especially if there is a lot of data in the table. The problem occurs when the table has to support both horizontal and vertical scrolling. A good example of such a table can be found from the NBA application when viewing box score of a past game. Here's an example image from the NBA Android application: Example table from NBA mobile application

从图片中可以清楚地看到,标题行与实际表格数据水平对齐,第一列与表格数据垂直对齐.我不知道阻止以相同的触摸动作水平和垂直滚动是非自愿的还是自愿的决定,但这是一个我不关心的小细节.

As you can clearly see from the image the header row is horizontally aligned with the actual table data and the first column is vertically aligned with the table data. I don't know whether or not it's an involuntary or a voluntary decision to prevent scrolling both horizontally and vertically with the same touch motion but that's a minor detail I don't care about.

我不知道如何使用 Xamarin Forms 实现这一点.我对闭源/付费解决方案不感兴趣,因为我想真正学习如何自己完成.我确实意识到我很可能必须为 Android 和 IOS 使用自定义渲染器.我目前的想法是我有一个绝对布局,其中包含以下元素:

I don't know how to implement this using Xamarin Forms. I am not interested in a closed source / paid solution since I would like to actually learn how to accomplish this by myself. I do realize that I most likely have to use custom renderers for both Android and IOS. My current idea is that I have an absolute layout where I have the following elements:

  • 第一个单元格(它是静止的,也是唯一静止的)
  • 水平滚动视图中标题行的其余部分
  • listview/stacklayout + 垂直滚动视图中的第一列
  • listview + 水平滚动视图/stacklayout + 水平和垂直滚动视图中的实际表格数据

通过此设置,我将捕获触摸事件并将其发送到其他列表视图/滚动视图,从而同步滚动.事实上,通过将表格数据设置在与第一列相同的垂直滚动视图中,我可以轻松实现第一列和实际表格数据的同步滚动.但我不知道如何将水平滚动同步到标题行,我相信这不能通过巧妙的组件结构来实现.到目前为止,我仅在 Android 上进行了测试,我可以在滚动视图自定义渲染器的 OnTouchEvent 方法中捕获触摸事件,但我不知道如何将其从自定义渲染器发送到标题行滚动视图.

With this setup I would capture the touch event and send it to the other listviews/scrollviews, thus synchronizing the scrolling. In fact I can easily achieve the synchronized scrolling with the first column and the actual table data by setting the table data inside the same vertical scrollview as the first column. But I don't know how to synchronize the horizontal scrolling to the header row and I do believe that this can't be accomplished by clever component structure. I have tested only on Android so far that I can capture the touch event in a scrollview custom renderer's OnTouchEvent -method but I don't know how I could send this to the header row scrollview from the custom renderer.

这是说明我的方法的 XAML 草案.

Here is a draft XAML illustrating my approach.

<AbsoluteLayout xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             HorizontalOptions="FillAndExpand">
    <ScrollView
        Orientation="Horizontal"
        x:Name="HeaderScrollView"
        AbsoluteLayout.LayoutBounds="0,0,1,1"
        AbsoluteLayout.LayoutFlags="All">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="200" />
                <ColumnDefinition Width="100" />
                <ColumnDefinition Width="100" />
                <ColumnDefinition Width="100" />
                <ColumnDefinition Width="100" />
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="50" />
            </Grid.RowDefinitions>
            <!-- Skip first column, leave it empty for stationary cell -->
            <Label Text="Column 1" Grid.Row="0" Grid.Column="1" />
            <Label Text="Column 2" Grid.Row="0" Grid.Column="2" />
            <Label Text="Column 3" Grid.Row="0" Grid.Column="3" />
            <Label Text="Column 4" Grid.Row="0" Grid.Column="4" />
        </Grid>
    </ScrollView>
    <ScrollView
        x:Name="FirstColumnScrollView"
        Orientation="Vertical"
        AbsoluteLayout.LayoutBounds="0,50,1,1"
        AbsoluteLayout.LayoutFlags="SizeProportional"
        BackgroundColor="Aqua">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="200" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>
            <StackLayout
                Grid.Column="0"
                Grid.Row="0"
                BindableLayout.ItemsSource="{Binding DataSource}">
                <BindableLayout.ItemTemplate>
                    <DataTemplate>
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="50" />
                            </Grid.RowDefinitions>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="150" />
                            </Grid.ColumnDefinitions>
                            <Label Text="{Binding Column1}" Grid.Row="0" Grid.Column="0" />
                        </Grid>
                    </DataTemplate>
                </BindableLayout.ItemTemplate>
            </StackLayout>
            <ScrollView
                x:Name="TableDataScrollView"
                Grid.Column="1"
                Grid.Row="0"
                Orientation="Horizontal">
                <StackLayout
                    BindableLayout.ItemsSource="{Binding DataSource}">
                    <BindableLayout.ItemTemplate>
                        <DataTemplate>
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="100" />
                                    <ColumnDefinition Width="100" />
                                    <ColumnDefinition Width="100" />
                                    <ColumnDefinition Width="100" />
                                </Grid.ColumnDefinitions>
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="50" />
                                </Grid.RowDefinitions>
                                <Label Text="{Binding Column2}" Grid.Row="0" Grid.Column="0" />
                                <Label Text="{Binding Column3}" Grid.Row="0" Grid.Column="1" />
                                <Label Text="{Binding Column4}" Grid.Row="0" Grid.Column="2" />
                                <Label Text="{Binding Column5}" Grid.Row="0" Grid.Column="3" />
                            </Grid>
                        </DataTemplate>
                    </BindableLayout.ItemTemplate>
                </StackLayout>
            </ScrollView>
        </Grid>
    </ScrollView>
    <Label Text="First Column" BackgroundColor="White" AbsoluteLayout.LayoutBounds="0,0,200,50" />
</AbsoluteLayout>

如您所见,问题在于 HeaderScrollView 和 TableDataScrollView 之间的水平滚动事件未共享,我不知道如何以最佳方式或根本不知道如何实现这一点.

As you can see the problem is that horizontal scrolling events between HeaderScrollView and TableDataScrollView are not shared and I don't know how to accomplish this in the best way possible or at all.

我非常感谢所有的帮助和反馈!

I do appreciate all the help and feedback with this!

推荐答案

感谢 @Harikrishnan 和 @Wendy Zang - MSFT 的帮助!Zumero DataGrid 启发我以不同于通常的运动事件处理流程的方式处理运动事件.我基本上为 AbsoluteLayout 创建了以下自定义渲染器

Thanks for the help with this @Harikrishnan and @Wendy Zang - MSFT ! The Zumero DataGrid inspired me to do the motion event handling differently from the usual motion event handling flow. I basically created the following custom renderer for the AbsoluteLayout

using Android.Content;
using Android.Views;
using Test.Droid;
using Test.Views;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using View = Android.Views.View;

[assembly: ExportRenderer(typeof(StatisticsTable), typeof(StatisticsTableRenderer))]
namespace Test.Droid
{
    public class StatisticsTableRenderer : ViewRenderer
    {
        private View _headerScrollView;
        private View _tableScrollView;
        private float _startX;
        public StatisticsTableRenderer(Context context) : base(context)
        {
        }

        public override bool OnInterceptTouchEvent(MotionEvent ev)
        {
            if (_headerScrollView == null || _tableScrollView == null)
            {
                // Completely dependant on the structure of XAML
                _headerScrollView = GetChildAt(0);
                _tableScrollView = GetChildAt(1);
            }
            return true;
        }

        public override bool OnTouchEvent(MotionEvent ev)
        {
            if (ev.Action == MotionEventActions.Down)
            {
                _startX = ev.GetX();
            }

            var headerScroll = false;
            if (_startX > _headerScrollView.GetX())
            {
                headerScroll = _headerScrollView.DispatchTouchEvent(ev);
            }
            var tableScroll = _tableScrollView.DispatchTouchEvent(ev);


            return headerScroll || tableScroll;
        }
    }
}

如您所见,我总是拦截运动事件,然后手动将其分派给孩子们.然而,这还不够.当运动事件未在其中启动时,我不得不阻止 HeaderScrollView 滚动,因为如果运动事件未在其中启动,TableDataScrollView 将不会滚动.我还必须为该表中的所有滚动视图创建自定义渲染器.TableDataScrollView 和 HeaderScrollView 使用相同的自定义渲染器.自定义渲染器唯一实现的是 OnInterceptTouchEvent,如下所示:

As you can see I always intercept the motion event and then manually dispatch it to the children. However that was not enough. I had to prevent HeaderScrollView from scrolling when the motion event didn't start inside of it because the TableDataScrollView wouldn't scroll if the motion event wasn't started inside of it. I also had to create custom renderers for all scrollviews in this table. TableDataScrollView and HeaderScrollView were using the same custom renderer. The only thing that custom renderer implemented was OnInterceptTouchEvent like this:

public override bool OnInterceptTouchEvent(MotionEvent ev)
{
    return false;
}

我不太确定为什么这是必要的,但它似乎对我有用.我想有时 HeaderScrollView 会拦截运动事件,这会导致标题滚动而不滚动表格数据.

I am not quite sure why this is necessary but it seems to have done the trick for me. I suppose that sometimes the HeaderScrollView would intercept the motion event and this caused the header to scroll without scrolling of the table data.

问题的 XAML 中的垂直滚动视图又名 FirstColumnScrollView 必须以不同的方式实现运动事件处理,因为它是 TableDataScrollView 的父级,我们现在以自上而下的方式处理运动事件,而不是默认的 Android 方式底部-到达顶点.这会导致 FirstColumnScrollView 仅处理运动事件而不将其传递给 TableDataScrollView 的问题,这将导致标题和实际表数据彼此不同步.这就是我为它添加以下自定义渲染器的原因

The vertical scrollview aka FirstColumnScrollView in the question's XAML had to implement motion event handling differently because it is the parent of the TableDataScrollView and we are now handling motion events in a top-to-bottom manner instead of the default Android way of bottom-to-top. This caused issues where FirstColumnScrollView would simply handle the motion event and not pass it to TableDataScrollView which would then lead to the header and actual table data to be out of sync with each other. This is why I added the following custom renderer for it

using Android.Content;
using Android.Views;
using Test.Droid;
using Test.Views;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using View = Android.Views.View;

[assembly: ExportRenderer(typeof(ChildFirstScrollView), typeof(ChildFirstScrollViewRenderer))]
namespace Test.Droid
{
    public class ChildFirstScrollViewRenderer : ScrollViewRenderer
    {
        private View _childView;
        public ChildFirstScrollViewRenderer(Context context) : base(context)
        {
        }

        public override bool DispatchTouchEvent(MotionEvent e)
        {
            if (_childView == null)
            {
                _childView = GetChildAt(0);
            }

            _childView.DispatchTouchEvent(e);
            return base.DispatchTouchEvent(e);
        }

        public override bool OnInterceptTouchEvent(MotionEvent ev)
        {
            return true;
        }
    }
}

在这个 ScrollView 中,我们总是拦截/处理运动事件,我们总是在处理运动事件之前先将其发送给子 ScrollView.

In this ScrollView we always intercept/handle the motion event and we always send it to the child ScrollView first before handling the motion event.

我还必须对问题中显示的 XAML 进行一些细微调整.我将 HeaderScrollView 的起始 X 设置为第一列的宽度,因此它实际上并不在第一列的静态标题下.然而,这导致了问题,因为我无法使用 AbsoluteLayout 的宽度(为什么在 XAML 中这么难?)来计算 HeaderScrollView 的正确宽度.现在宽度的设置方式是 HeaderScrollView 的一部分将始终位于视口之外,从而导致永远不会显示最后一个标题.所以我在标题网格中添加了一个PaddingColumn",宽度等于第一列.出于同样的原因,我还必须向 FirstColumnScrollView 网格添加一个PaddingRow".

I also had to do some minor adjustments to the XAML shown in the question. I set the starting X of HeaderScrollView to the width of the first column so it doesn't actually go under the static header of the first column. However this caused issues because I was unable to use width of the AbsoluteLayout (Why is it so hard in XAML?) to calculate the correct width for the HeaderScrollView. Now the width was set in a way that a part of the HeaderScrollView will always be outside of the viewport causing the last header to be never shown. So I added a "PaddingColumn" to the header grid with a width equal to the first column. I also had to add a "PaddingRow" to the FirstColumnScrollView grid for the same reason.

我必须做的另一件事是将 FirstColumnScrollView 内的网格间距设置为 0.没有它,就会有一个很小的间隙,您可以从中启动仅滚动标题而不滚动表格数据的运动事件.

One other thing I had to do was to set the spacing of the grid inside FirstColumnScrollView to 0. Without that, there was this small gap from where you could start motion events that would only scroll the header and not the table data.

目前这只是 Android 解决方案,但如果我能完成,我会回来使用 iOS 解决方案.

This is only the Android solution at the moment but I'll come back with the iOS one if I can accomplish it.

这篇关于如何使用 Xamarin Forms 创建具有垂直粘性标题和水平粘性第一列的表格?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆