При отображении табличных данных я думаю, что в некоторых случаях наличие всегда видимой строки заголовка и всегда видимого первого столбца действительно может улучшить читаемость и общее удобство использования таблицы, особенно если в таблице много данных. Проблема возникает, когда таблица должна поддерживать как горизонтальную, так и вертикальную прокрутку. Хороший пример такой таблицы можно найти в приложении NBA при просмотре очков в прошлой игре. Вот пример изображения из приложения NBA для Android: Пример таблицы из мобильного приложения NBA
Как вы можете ясно видеть из изображения, строка заголовка выровнена по горизонтали с фактическими данными таблицы, а первый столбец выровнен по вертикали с данными таблицы. Я не знаю, является ли это непроизвольным или добровольным решением предотвратить прокрутку как по горизонтали, так и по вертикали одним движением касания, но это мелкая деталь, которая меня не волнует.
Я не знаю, как это реализовать с помощью Xamarin Forms. Я не интересуюсь закрытым исходным кодом / платным решением, так как я действительно хотел бы узнать, как это сделать самостоятельно. Я понимаю, что мне, скорее всего, придется использовать собственные средства визуализации как для Android, так и для IOS. Моя текущая идея заключается в том, что у меня есть абсолютный макет, в котором есть следующие элементы:
С помощью этой настройки я бы захватил событие касания и отправил его в другие списки / прокрутки, таким образом синхронизируя прокрутку. Фактически, я могу легко добиться синхронизированной прокрутки с первым столбцом и фактическими данными таблицы, установив данные таблицы внутри того же вертикального прокрутки, что и первый столбец. Но я не знаю, как синхронизировать горизонтальную прокрутку со строкой заголовка, и я считаю, что это не может быть достигнуто с помощью продуманной компонентной структуры. До сих пор я тестировал только на 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 не используются совместно, и я не знаю, как это сделать наилучшим образом или вообще.
Я ценю всю помощь и отзывы по этому поводу!
Спасибо за помощь с этим @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, если смогу его выполнить.
Для открытого исходного кода вы можете использоватьZumero DataGrid
для Xamarin.Forms. Он поддерживает прокрутку, как горизонтальную, так и вертикальную, необязательную верхнюю закрепленную строку заголовка, необязательный левый закрепленный столбец и т. Д. Вы можете скачать образец кода по ссылке ниже.
Zumero DataGrid для Xamarin.Forms: https://github.com/zumero/DataGrid/tree/8caf4895e2cc4362da3dbdd4735b5c6eb1d2dec4
Для примера кода, если вы получите ошибку ниже, можно запустить от имени администратора.
Build action 'EmbeddedResource' is not supported by one or more of the project's targets
Вам нужен компонент DataGrid с функцией замороженной строки и замороженного столбца. Есть некоторые сторонние компоненты, которые могут удовлетворить ваши требования.
Syncfusion, Telerik и Infragistics DataGrids имеют функции, которые вам нужны. См. Ссылки ниже.
Есть также несколько доступных DataGrid с открытым исходным кодом. Но не уверен, есть ли у них функции закрепления строк и столбцов. Проверьте ссылки ниже.