4gus71n
4gus71n

Reputation: 4073

Xamarin.Forms - ListView change height at runtime

I'm developing a cross-platform app using Xamarin.Forms. In one of the pages I've an avatar image, some info, and then a ListView. The thing is that I've all this wrapped in a ScrollView, so I'm able to scroll all over the content. The issue with this is that I'm losing the ListView scroll behaviour. This're some screenshots:

enter image description here

enter image description here

As you can see the XAML view is basically, the avatar Image (the firetruck image), then the "Last check step" info, and then we've the ListView. I hardcoded a HeightRequest in the ListView so It has a bigger height. But the thing is that when I scroll to the bottom of the page I can't keep scrolling through the ListView because the ScrollView messes with that behaviour. I need to use a ListView because the list of check reports that I'm showing there is populated from a webservice, and I update that list when I enter and exit the page. This is the XAML code:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:checkReport="clr-namespace:HalliganTL.View;assembly=HalliganTL"
              xmlns:controls="clr-namespace:ImageCircle.Forms.Plugin.Abstractions;assembly=ImageCircle.Forms.Plugin.Abstractions"
             x:Class="HalliganTL.View.ApparatusDetailPage" BackgroundColor="#FFFFFFFF">
  <StackLayout Orientation="Vertical" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
    <ScrollView x:Name="GeneralScrollView"  
                Orientation="Vertical" VerticalOptions="FillAndExpand"
                HorizontalOptions="FillAndExpand">
      <Grid HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
        <Grid.RowDefinitions>
          <RowDefinition Height="180"></RowDefinition>
          <RowDefinition Height="40"></RowDefinition>
          <RowDefinition Height="80"></RowDefinition>
          <RowDefinition Height="40"></RowDefinition>
          <RowDefinition Height="360"></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>

        <!-- Top Image -->
        <ContentView Grid.Column="0" Grid.Row="0" Padding="15">
          <controls:CircleImage x:Name="ApparatusImage"
            HeightRequest="150" WidthRequest="150"
            VerticalOptions="Center"
            BorderColor="#3B5685"
            BorderThickness="2"
            HorizontalOptions="Center"
            Aspect="AspectFill">
          </controls:CircleImage>
        </ContentView>

        <!-- Last Check Separator -->
        <AbsoluteLayout Grid.Column="0" Grid.Row="1" x:Name="LastCheckContainerTitle" BackgroundColor="#FFE4E4E9" Padding="5,15,15,5" VerticalOptions="End" >
          <Label Text="LAST CHECK" Style="{StaticResource separatorLabel}" />
        </AbsoluteLayout>
        <!-- Last Check Detail -->
        <checkReport:CheckReportViewPage Grid.Column="0" Grid.Row="2" x:Name="LastCheckReportView">
          <checkReport:CheckReportViewPage.GestureRecognizers>
            <TapGestureRecognizer Tapped="OnLastCheckReportClicked" NumberOfTapsRequired="1" />
          </checkReport:CheckReportViewPage.GestureRecognizers>
        </checkReport:CheckReportViewPage>

        <AbsoluteLayout  Grid.Column="0" Grid.Row="3"  x:Name="CheckHistoryTitle" BackgroundColor="#FFE4E4E9" Padding="5,15,15,5" >
          <Label Text="CHECK HISTORY" Style="{StaticResource separatorLabel}" FontAttributes="None" VerticalOptions="End" />
        </AbsoluteLayout>

        <!-- Apparatus check history -->
        <ListView x:Name="CheckHistoryListView"
                  HeightRequest="380"
                   Grid.Column="0" Grid.Row="4"
                  HorizontalOptions="FillAndExpand"
                  HasUnevenRows="True"
                  IsPullToRefreshEnabled="False"
                  VerticalOptions="FillAndExpand" ItemTapped="OnItemTapped">
          <ListView.ItemTemplate>
            <DataTemplate>
              <ViewCell>
                <checkReport:CheckReportViewPage/>
              </ViewCell>
            </DataTemplate>
          </ListView.ItemTemplate>
        </ListView>
      </Grid>
    </ScrollView>

    <Label Text="Start Check" HeightRequest="60"  VerticalOptions="End" BackgroundColor="#3B5685" VerticalTextAlignment="Center" HorizontalTextAlignment="Center" FontSize="18" TextColor="White">
      <Label.GestureRecognizers>
        <TapGestureRecognizer
                Tapped="OnStartCheckClicked"
                NumberOfTapsRequired="1" />
      </Label.GestureRecognizers>
    </Label>

  </StackLayout>



</ContentPage>

So basically what I'm asking is how can achieve something like a parallax effect, scrolling down and then delegate the scroll behaviour to the ListView that way I can scroll through every item in the ListView. Currently I'm just able to see the items that fit in the ListView HeightRequest dimension. In the example screenshot that I'm shoing for example, there are several other items in that ListView but I can't scroll through them because the ScrollView is messing with the ListView scroll behaviour.

Upvotes: 0

Views: 2420

Answers (3)

Adam
Adam

Reputation: 16199

The recommended approach for putting content on a page with a ListView is to use the Footer and Header properties of the ListView.

Have a look at ListView Headers and Footers

<ListView x:Name="listView">
  <ListView.Footer> 
    <Label Text="Footer" />
  </ListView.Footer>
</ListView>

This way you don't need to wrap in another scroll.

Upvotes: 3

George Papadakis
George Papadakis

Reputation: 1368

I believe that you need a RepeaterView , a control that repeats items but is not scrolable and you can bind it to an ObservableCollection.

It just works

using System;
    using System.Collections;
    using System.Collections.Specialized;
    using Xamarin.Forms;

    public delegate void RepeaterViewItemAddedEventHandler(object sender, RepeaterViewItemAddedEventArgs args);

    // in lieu of an actual Xamarin Forms ItemsControl, this is a heavily modified version of code from https://forums.xamarin.com/discussion/21635/xforms-needs-an-itemscontrol
    public class RepeaterView : StackLayout
    {
        public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create<RepeaterView, IEnumerable>(
            p => p.ItemsSource,
            null,
            BindingMode.OneWay,
            propertyChanged: ItemsChanged);

        public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create<RepeaterView, DataTemplate>(
            p => p.ItemTemplate,
            default(DataTemplate));

        public event RepeaterViewItemAddedEventHandler ItemCreated;

        public IEnumerable ItemsSource
        {
            get { return (IEnumerable)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }

        public DataTemplate ItemTemplate
        {
            get { return (DataTemplate)GetValue(ItemTemplateProperty); }
            set { SetValue(ItemTemplateProperty, value); }
        }

        private static void ItemsChanged(BindableObject bindable, IEnumerable oldValue, IEnumerable newValue)
        {
            var control = (RepeaterView)bindable;
            var oldObservableCollection = oldValue as INotifyCollectionChanged;

            if (oldObservableCollection != null)
            {
                oldObservableCollection.CollectionChanged -= control.OnItemsSourceCollectionChanged;
            }

            var newObservableCollection = newValue as INotifyCollectionChanged;

            if (newObservableCollection != null)
            {
                newObservableCollection.CollectionChanged += control.OnItemsSourceCollectionChanged;
            }

            control.Children.Clear();

            if (newValue != null)
            {
                foreach (var item in newValue)
                {
                    var view = control.CreateChildViewFor(item);
                    control.Children.Add(view);
                    control.OnItemCreated(view);
                }
            }

            control.UpdateChildrenLayout();
            control.InvalidateLayout();
        }

        protected virtual void OnItemCreated(View view) =>
            this.ItemCreated?.Invoke(this, new RepeaterViewItemAddedEventArgs(view, view.BindingContext));

        private void OnItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            var invalidate = false;

            if (e.OldItems != null)
            {
                this.Children.RemoveAt(e.OldStartingIndex);
                invalidate = true;
            }

            if (e.NewItems != null)
            {
                for (var i = 0; i < e.NewItems.Count; ++i)
                {
                    var item = e.NewItems[i];
                    var view = this.CreateChildViewFor(item);

                    this.Children.Insert(i + e.NewStartingIndex, view);
                    OnItemCreated(view);
                }

                invalidate = true;
            }

            if (invalidate)
            {
                this.UpdateChildrenLayout();
                this.InvalidateLayout();
            }
        }

        private View CreateChildViewFor(object item)
        {
            this.ItemTemplate.SetValue(BindableObject.BindingContextProperty, item);
            return (View)this.ItemTemplate.CreateContent();
        }
    }

    public class RepeaterViewItemAddedEventArgs : EventArgs
    {
        private readonly View view;
        private readonly object model;

        public RepeaterViewItemAddedEventArgs(View view, object model)
        {
            this.view = view;
            this.model = model;
        }

        public View View => this.view;

        public object Model => this.model;
    }

Read this thread in Xamarin Forms Forums

Upvotes: 0

Keith Rome
Keith Rome

Reputation: 3228

ListView should never be placed inside of a scrolling container. When you do this, you lose all benefits of the ListView (virtualization, scrolling, etc).

If you need to implement a parallax effect by scrolling some other content in sync with the ListView position, then you can always overlay the list on top of another scrolling view (but not as a child of that scrolling view).

Upvotes: 1

Related Questions