Как создать таблицу с вертикально закрепленным заголовком и горизонтально закрепленным первым столбцом с помощью Xamarin Forms?

При отображении табличных данных я думаю, что в некоторых случаях наличие всегда видимой строки заголовка и всегда видимого первого столбца действительно может улучшить читаемость и общее удобство использования таблицы, особенно если в таблице много данных. Проблема возникает, когда таблица должна поддерживать как горизонтальную, так и вертикальную прокрутку. Хороший пример такой таблицы можно найти в приложении NBA при просмотре очков в прошлой игре. Вот пример изображения из приложения NBA для Android: Пример таблицы из мобильного приложения NBA

Как вы можете ясно видеть из изображения, строка заголовка выровнена по горизонтали с фактическими данными таблицы, а первый столбец выровнен по вертикали с данными таблицы. Я не знаю, является ли это непроизвольным или добровольным решением предотвратить прокрутку как по горизонтали, так и по вертикали одним движением касания, но это мелкая деталь, которая меня не волнует.

Я не знаю, как это реализовать с помощью Xamarin Forms. Я не интересуюсь закрытым исходным кодом / платным решением, так как я действительно хотел бы узнать, как это сделать самостоятельно. Я понимаю, что мне, скорее всего, придется использовать собственные средства визуализации как для Android, так и для IOS. Моя текущая идея заключается в том, что у меня есть абсолютный макет, в котором есть следующие элементы:

  • Первая ячейка (она стационарная и единственная стационарная)
  • Остальная часть строки заголовка внутри горизонтальной прокрутки
  • Первый столбец внутри listview / stacklayout + вертикальная прокрутка
  • Фактические данные таблицы внутри списка + горизонтальная прокрутка / stacklayout + горизонтальная и вертикальная прокрутка

С помощью этой настройки я бы захватил событие касания и отправил его в другие списки / прокрутки, таким образом синхронизируя прокрутку. Фактически, я могу легко добиться синхронизированной прокрутки с первым столбцом и фактическими данными таблицы, установив данные таблицы внутри того же вертикального прокрутки, что и первый столбец. Но я не знаю, как синхронизировать горизонтальную прокрутку со строкой заголовка, и я считаю, что это не может быть достигнуто с помощью продуманной компонентной структуры. До сих пор я тестировал только на Android, что могу зафиксировать событие касания в методе OnTouchEvent настраиваемого средства визуализации scrollview, но я не знаю, как отправить его в строку заголовка scrollview из настраиваемого средства визуализации.

Вот черновик XAML, иллюстрирующий мой подход.

<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 не используются совместно, и я не знаю, как это сделать наилучшим образом или вообще.

Я ценю всю помощь и отзывы по этому поводу!

# xamarin xamarin.forms scrollview tabular
Источник
  • 0
    @FreakyAli Предполагается, что TableView будет использоваться для таких целей? Например, в нем вообще отсутствует опция ItemsSource, и все примеры показывают какое-то меню настроек.
Codelisting
за 0 против
Лучший ответ

Спасибо за помощь с этим @Harikrishnan и @Wendy Zang - MSFT! Zumero DataGrid вдохновил меня на то, чтобы обрабатывать события движения иначе, чем в обычном потоке обработки событий движения. Я в основном создал следующий пользовательский рендерер для 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, подобный этому:

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

Я не совсем уверен, почему это необходимо, но, похоже, это помогло мне. Я предполагаю, что иногда HeaderScrollView перехватывает событие движения, и это заставляет заголовок прокручиваться без прокрутки данных таблицы.

Вертикальная прокрутка, известная как FirstColumnScrollView в XAML вопроса, должна была реализовать обработку событий движения по-другому, потому что это родительский элемент для TableDataScrollView, и теперь мы обрабатываем события движения сверху вниз вместо стандартного способа Android по умолчанию снизу-к- Топ. Это вызывало проблемы, когда FirstColumnScrollView просто обрабатывал событие движения, а не передавал его в TableDataScrollView, что приводило бы к рассинхронизации заголовка и фактических данных таблицы друг с другом. Вот почему я добавил для него следующий пользовательский рендерер

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 перед обработкой события движения.

Мне также пришлось внести некоторые незначительные изменения в XAML, показанный в вопросе. Я установил начальный X HeaderScrollView равным ширине первого столбца, чтобы он фактически не проходил под статическим заголовком первого столбца. Однако это вызвало проблемы, потому что мне не удалось использовать ширину AbsoluteLayout (почему это так сложно в XAML?) Для расчета правильной ширины для HeaderScrollView. Теперь ширина была установлена таким образом, что часть HeaderScrollView всегда будет за пределами области просмотра, в результате чего последний заголовок никогда не будет отображаться. Поэтому я добавил «PaddingColumn» в сетку заголовка с шириной, равной ширине первого столбца. Мне также пришлось добавить «PaddingRow» к сетке FirstColumnScrollView по той же причине.

Еще мне пришлось сделать шаг сетки внутри FirstColumnScrollView равным 0. Без этого был небольшой промежуток, откуда вы могли запускать события движения, которые прокручивали бы только заголовок, а не данные таблицы.

На данный момент это только решение для Android, но я вернусь с решением для iOS, если смогу его выполнить.

за 1 против

Для открытого исходного кода вы можете использоватьZumero DataGrid для Xamarin.Forms. Он поддерживает прокрутку, как горизонтальную, так и вертикальную, необязательную верхнюю закрепленную строку заголовка, необязательный левый закрепленный столбец и т. Д. Вы можете скачать образец кода по ссылке ниже.

Zumero DataGrid для Xamarin.Forms: https://github.com/zumero/DataGrid/tree/8caf4895e2cc4362da3dbdd4735b5c6eb1d2dec4

Изображение 334953

Для примера кода, если вы получите ошибку ниже, можно запустить от имени администратора.

Build action 'EmbeddedResource' is not supported by one or more of the project's targets
за 1 против

Вам нужен компонент DataGrid с функцией замороженной строки и замороженного столбца. Есть некоторые сторонние компоненты, которые могут удовлетворить ваши требования.

Syncfusion, Telerik и Infragistics DataGrids имеют функции, которые вам нужны. См. Ссылки ниже.

Есть также несколько доступных DataGrid с открытым исходным кодом. Но не уверен, есть ли у них функции закрепления строк и столбцов. Проверьте ссылки ниже.

  • 0
    Спасибо за помощь, и мне очень жаль, что я забыл указать, что я не ищу решение с закрытым исходным кодом. Я знал, что этого можно достичь с помощью этих платных компонентов пользовательского интерфейса, но я хочу узнать, как это можно сделать, а платный компонент в этом не помогает. Отредактирую вопрос соответственно. Кроме того, это решение с открытым исходным кодом, похоже, не поддерживает закрепление строк или столбцов.
  • 0
    Я мог видеть, что нижеприведенный компонент DataGrid бесплатен. Вы можете отладить исходный код, если хотите изучить логику закрепления строк. Я в основном из фона разработки пользовательских компонентов. Все, что вам нужно сделать, это применить некоторую логику в компоновке ваших представлений и немного поиграть с привязкой прокручиваемых представлений. github.com/zumero/DataGrid#what-are-its-features
  • 0
    @kapseli: Надеюсь, это поможет. Если да, не забудьте отметить мой ответ как ответ, спасибо :)
Codelisting
Популярные категории
На заметку программисту