0

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.

3 Answers 3

3

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Quick question, is there any way to show the Header when the ListView has no items? Because, currently if my ListView has no items, the Header isn't displayed.
1

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

3 Comments

Thank you for taking your time an answer me. I'm very aware that isn't correct placing a scrolleable control inside another scrolleable control, but can you see what I'm trying to point out here? Can you suggest me some kind of XAML organization?
Maybe I am misunderstanding what you are looking for. Are you wanting your avatar plus the static info at the top to remain visible as the list is scrolled?
No, I want that info to scroll up as I scroll through the ListView, but don't worry putting that info within ListView.Header solved the problem
0

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.