1
votes

Comment créer une table avec un en-tête collant verticalement et une première colonne collante horizontalement à l'aide de Xamarin Forms?

Lors de l'affichage de données tabulaires, je pense que dans certains cas, le fait d'avoir une ligne d'en-tête toujours visible et une première colonne toujours visible peut vraiment améliorer la lisibilité et la convivialité globale d'un tableau, surtout s'il y a beaucoup de données dans le tableau . Le problème se produit lorsque le tableau doit prendre en charge le défilement horizontal et vertical. Un bon exemple d'un tel tableau peut être trouvé à partir de l'application NBA lors de la visualisation du score de la boîte d'un match passé. Voici un exemple d'image de l'application NBA Android: Exemple de tableau de l'application mobile NBA

Comme vous pouvez le voir clairement sur l'image, le la ligne d'en-tête est alignée horizontalement avec les données réelles du tableau et la première colonne est alignée verticalement avec les données du tableau. Je ne sais pas si c'est une décision involontaire ou volontaire d'empêcher le défilement horizontal et vertical avec le même mouvement tactile, mais c'est un détail mineur qui ne me préoccupe pas.

Je ne sais pas savoir comment implémenter cela à l'aide de Xamarin Forms. Je ne suis pas intéressé par une solution fermée / payante car j'aimerais réellement apprendre comment y parvenir par moi-même. Je me rends compte que je dois probablement utiliser des moteurs de rendu personnalisés pour Android et IOS. Mon idée actuelle est d'avoir une mise en page absolue où j'ai les éléments suivants:

  • La première cellule (elle est stationnaire et la seule chose stationnaire)
  • Reste de la ligne d'en-tête dans une vue de défilement horizontale
  • Première colonne dans une vue de liste / stacklayout + scrollview vertical
  • Les données réelles du tableau dans une vue de liste + scrollview / stacklayout horizontal + scrollview horizontal et vertical

Avec cette configuration, je capturerais l'événement tactile et je l'envoyais aux autres vues de liste / vues de défilement, synchronisant ainsi le défilement. En fait, je peux facilement réaliser le défilement synchronisé avec la première colonne et les données réelles du tableau en définissant les données du tableau dans le même scrollview vertical que la première colonne. Mais je ne sais pas comment synchroniser le défilement horizontal avec la ligne d'en-tête et je pense que cela ne peut pas être accompli par une structure de composant intelligente. Jusqu'à présent, je n'ai testé que sur Android que je peux capturer l'événement tactile dans la méthode OnTouchEvent d'un rendu personnalisé par scrollview, mais je ne sais pas comment je pourrais envoyer cela à la ligne d'en-tête scrollview à partir du rendu personnalisé.

Voici un brouillon de XAML illustrant mon approche.

<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>

Comme vous pouvez le voir, le problème est que les événements de défilement horizontal entre HeaderScrollView et TableDataScrollView ne sont pas partagés et je ne sais pas comment pour accomplir cela de la meilleure façon possible ou pas du tout.

J'apprécie toute l'aide et vos commentaires!


2 commentaires

docs.microsoft.com/en-us/xamarin/ xamarin-forms / user-interfac‌ e /…


@FreakyAli Est-ce que TableView est censé être utilisé à cette fin? Par exemple, il manque complètement l'option ItemsSource et tous les exemples montrent une sorte de menu de paramètres.


3 Réponses :


1
votes

Ce que vous recherchez est un composant DataGrid avec une fonction de ligne gelée et de colonne gelée. Certains composants tiers répondraient à vos besoins.

Syncfusion, Telerik et Infragistics DataGrids ont les fonctionnalités que vous recherchez. Reportez-vous aux liens ci-dessous.

Il existe également peu de DataGrid open source. Mais je ne sais pas s'ils disposent des fonctionnalités d'épinglage de lignes et de colonnes. Consultez les liens ci-dessous.


3 commentaires

Merci pour l'aide et je suis désolé d'avoir oublié de préciser que je ne recherche pas de solution source fermée. J'étais conscient que cela peut être réalisé avec ces composants d'interface utilisateur payants, mais je veux savoir comment cela peut être accompli et un composant payant n'aide pas à cela. Je modifierai la question en conséquence. De plus, cette solution open source ne semblait pas prendre en charge l'épinglage de lignes ou de colonnes.


J'ai pu voir que le composant DataGrid ci-dessous est gratuit. Vous pouvez déboguer le code source si vous souhaitez explorer la logique d'épinglage de lignes. Suis essentiellement de l'arrière-plan du développement de composants personnalisés. Tout ce que vous avez à faire est d'appliquer une certaine logique dans la mise en page de vos vues et de jouer un peu avec la liaison des vues qui défilent. github.com/zumero/DataGrid#what-are-its-features


@kapseli: J'espère que cela aide. Si tel est le cas, n'oubliez pas de marquer ma réponse comme réponse, merci :)



1
votes

Pour l'open source, vous pouvez utiliser Zumero DataGrid pour Xamarin.Forms. Il prend en charge le défilement horizontal et vertical, la ligne d'en-tête gelée supérieure facultative, la colonne gelée gauche facultative, etc. Vous pouvez télécharger l'exemple de code à partir du lien ci-dessous.

Zumero DataGrid for Xamarin.Forms: : //github.com/zumero/DataGrid/tree/8caf4895e2cc4362da3dbdd4735b5c6eb1d2dec4

 entrez la description de l'image ici

Pour l'exemple de code, si vous obtenez l'erreur ci-dessous, exécutez en tant qu'administrateur serait correct.

Build action 'EmbeddedResource' is not supported by one or more of the project's targets


0 commentaires

0
votes

Merci pour l'aide avec ce @Harikrishnan et @Wendy Zang - MSFT! Le Zumero DataGrid m'a inspiré pour faire la gestion des événements de mouvement différemment du flux habituel de gestion des événements de mouvement. J'ai essentiellement créé le moteur de rendu personnalisé suivant pour 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(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;
        }
    }
}

Comme vous pouvez le voir, j'intercepte toujours l'événement de mouvement, puis je l'envoie manuellement aux enfants. Cependant, cela ne suffisait pas. J'ai dû empêcher HeaderScrollView de défiler lorsque l'événement de mouvement ne commençait pas à l'intérieur, car le TableDataScrollView ne défilerait pas si l'événement de mouvement n'était pas démarré à l'intérieur. J'ai également dû créer des moteurs de rendu personnalisés pour toutes les vues de défilement de ce tableau. TableDataScrollView et HeaderScrollView utilisaient le même moteur de rendu personnalisé. La seule chose que le moteur de rendu personnalisé a implémenté était OnInterceptTouchEvent comme ceci:

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

Je ne sais pas trop pourquoi cela est nécessaire mais il semble avoir fait l'affaire pour moi. Je suppose que parfois le HeaderScrollView intercepterait l'événement de mouvement et cela faisait défiler l'en-tête sans faire défiler les données de la table.

Le scrollview vertical aka FirstColumnScrollView dans le XAML de la question devait implémenter la gestion des événements de mouvement différemment car il est le parent de TableDataScrollView et nous gérons maintenant les événements de mouvement de haut en bas au lieu de la méthode Android par défaut de bas en haut. Cela provoquait des problèmes où FirstColumnScrollView gérait simplement l'événement de mouvement et ne le transmettrait pas à TableDataScrollView, ce qui conduirait alors à la désynchronisation des données d'en-tête et de table réelle. C'est pourquoi j'ai ajouté le moteur de rendu personnalisé suivant

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;
        }
    }
}

Dans ce ScrollView, nous interceptons / gérons toujours l'événement de mouvement et nous l'envoyons toujours à l'enfant ScrollView avant de gérer le événement de mouvement.

J'ai également dû faire quelques ajustements mineurs au XAML indiqué dans la question. J'ai défini le X de départ de HeaderScrollView sur la largeur de la première colonne afin qu'il ne passe pas sous l'en-tête statique de la première colonne. Cependant, cela a causé des problèmes car je ne pouvais pas utiliser la largeur de AbsoluteLayout (pourquoi est-ce si difficile en XAML?) Pour calculer la largeur correcte pour HeaderScrollView. Désormais, la largeur a été définie de manière à ce qu'une partie de HeaderScrollView soit toujours en dehors de la fenêtre, ce qui fait que le dernier en-tête n'est jamais affiché. J'ai donc ajouté un "PaddingColumn" à la grille d'en-tête avec une largeur égale à la première colonne. J'ai également dû ajouter un "PaddingRow" à la grille FirstColumnScrollView pour la même raison.

Une autre chose que je devais faire était de définir l'espacement de la grille à l'intérieur de FirstColumnScrollView sur 0. Sans cela, il y avait ce petit écart à partir duquel vous pourriez démarrer des événements de mouvement qui ne feraient que faire défiler l'en-tête et non les données du tableau.

Ce n'est que la solution Android pour le moment mais je reviendrai avec celle d'iOS si je peut l'accomplir.


0 commentaires