mefiX
mefiX

Reputation: 1307

Xamarin.Forms ListView inside StackLayout

I've got a fairly simple XAML form in my Xamarin project, which is supposed to show two views packed in a vertical StackLayout, a CameraView at the top and a ListView at the bottom. The CameraView shall use up most of the space. The ListView shall not exceed 4-5 items, but will change during runtime. Here's the XAML I've got so far:

<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
         xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
         x:Class="Foo.CamPage"
         xmlns:ctrl="clr-namespace:Foo.Controls">
<ContentPage.Content>
    <StackLayout Orientation="Vertical">

        <ctrl:CameraView HeightRequest="2000"/>

        <StackLayout VerticalOptions="FillAndExpand">
            <ListView>
                  <ListView.ItemsSource>
                      <x:Array Type="{x:Type x:String}">
                        <x:String>Entry 1</x:String>
                        <x:String>Entry 2</x:String>
                      </x:Array>
                  </ListView.ItemsSource>
            </ListView>
        </StackLayout>

    </StackLayout>
</ContentPage.Content>

As you can see, my first idea was to put the ListView into another nested StackLayout. I also played with the HeightRequest of the CameraView However, I don't want to set a specific height of the camera view, I want the ListView to take up as much space as needed and the CameraView to adapt accordingly. Is it possible to define percentages in the sizes?

If it helps, here's the CameraView:

namespace Foo.Controls
{
    public class CameraView : View
    {
        public static readonly BindableProperty CameraProperty =
            BindableProperty.Create(
                propertyName: "Camera",
                returnType: typeof(CameraOptions),
                declaringType: typeof(CameraView),
                defaultValue: CameraOptions.Rear);

        public CameraOptions Camera
        {
            get { return (CameraOptions)GetValue(CameraProperty); }
            set { SetValue(CameraProperty, value); }
        }

        public Func<Task<ImageSource>> TakePhotoAsync { set; get; }

        public Func<CameraOptions, bool> SwitchCamera { set; get; }
    }
}

And this is the associated iOS renderer:

[assembly: ExportRenderer(typeof(CameraView), typeof(CamPreviewRenderer))]
namespace Foo.iOS.Renderers
{
    public class CamPreviewRenderer : ViewRenderer<CameraView, UICamPreview>
    {
        UICamPreview uiCameraPreview;

        protected override void OnElementChanged(ElementChangedEventArgs<CameraView> e)
        {
            base.OnElementChanged(e);

            if (Control == null)
            {
                uiCameraPreview = new UICamPreview(e.NewElement.Camera);
                SetNativeControl(uiCameraPreview);
            }
            if (e.OldElement != null)
            {
                // Unsubscribe
                uiCameraPreview.Tapped -= OnCameraPreviewTapped;
            }
            if (e.NewElement != null)
            {
                // Subscribe
                uiCameraPreview.Tapped += OnCameraPreviewTapped;
            }
        }

        void OnCameraPreviewTapped(object sender, EventArgs e)
        {
            if (uiCameraPreview.IsPreviewing)
            {
                uiCameraPreview.CaptureSession.StopRunning();
                uiCameraPreview.IsPreviewing = false;
            }
            else
            {
                uiCameraPreview.CaptureSession.StartRunning();
                uiCameraPreview.IsPreviewing = true;
            }
        }
    }
}

And this is the UICamPreview:

namespace Foo.iOS.Renderers
{
    public class UICamPreview : UIView
    {
        AVCaptureVideoPreviewLayer previewLayer;
        CameraOptions cameraOptions;

        public event EventHandler<EventArgs> Tapped;

        public AVCaptureSession CaptureSession { get; private set; }

        public bool IsPreviewing { get; set; }

        public UICamPreview(CameraOptions options)
        {
            cameraOptions = options;
            IsPreviewing = false;
            Initialize();
        }

        public override void Draw(CGRect rect)
        {
            base.Draw(rect);
            previewLayer.Frame = rect;
        }

        public override void TouchesBegan(NSSet touches, UIEvent evt)
        {
            base.TouchesBegan(touches, evt);
            OnTapped();
        }

        protected virtual void OnTapped()
        {
            var eventHandler = Tapped;
            if (eventHandler != null)
            {
                eventHandler(this, new EventArgs());
            }
        }

        void Initialize()
        {
            CaptureSession = new AVCaptureSession();
            previewLayer = new AVCaptureVideoPreviewLayer(CaptureSession)
            {
                Frame = Bounds,
                VideoGravity = AVLayerVideoGravity.ResizeAspectFill
            };

            var videoDevices = AVCaptureDevice.DevicesWithMediaType(AVMediaType.Video);
            var cameraPosition = (cameraOptions == CameraOptions.Front) ? AVCaptureDevicePosition.Front : AVCaptureDevicePosition.Back;
            var device = videoDevices.FirstOrDefault(d => d.Position == cameraPosition);

            if (device == null)
            {
                return;
            }

            NSError error;
            var input = new AVCaptureDeviceInput(device, out error);
            CaptureSession.AddInput(input);
            Layer.AddSublayer(previewLayer);
            CaptureSession.StartRunning();
            IsPreviewing = true;
        }
    }
}

Any hint appreciated.

Upvotes: 2

Views: 1460

Answers (2)

Uraitz
Uraitz

Reputation: 496

Have you tried overriding OnLayout, usually code is like this.

        protected override void OnLayout(bool changed, int l, int t, int r, int b)
        {
            try
            {
                base.OnLayout(changed, l, t, r, b);



                if (!changed)
                    return;



                int msw = 50;
                int msh = 50;

                int layoutWidth = r - l;
                int layoutHeight = b - t;


                msw = MeasureSpec.MakeMeasureSpec(layoutWidth, MeasureSpecMode.Exactly);
                msh = MeasureSpec.MakeMeasureSpec(layoutHeight, MeasureSpecMode.Exactly);

                view.Measure(msw, msh);
                view.Layout(0, 0, layoutWidth, layoutHeight);


            }
            catch (System.Exception ex)
            {
               Logger.WriteException(ex);
            }
            
      }

Upvotes: 0

Woj
Woj

Reputation: 833

Put those views into Grid with 2 rows. The one that you want to use as much space as you require set Height to Auto and the other view that suppose to take up the remaining space set Height to *. Something like this

<ContentPage.Content>
<StackLayout Orientation="Vertical">
 <Grid>
   <Grid.RowDefinitions>
   <RowDefinition Height="Auto"/>
   <RowDefinition Height="*" />
   </Grid.RowDefinitions>
    <ctrl:CameraView Grid.Row="0"/>

    <StackLayout VerticalOptions="FillAndExpand" Grid.Row="1">
        <ListView>
              <ListView.ItemsSource>
                  <x:Array Type="{x:Type x:String}">
                    <x:String>Entry 1</x:String>
                    <x:String>Entry 2</x:String>
                  </x:Array>
              </ListView.ItemsSource>
        </ListView>
    </Grid>
    </StackLayout>
   </StackLayout>
  </ContentPage.Content>

Please bear in mind that ListView is a very buggy when it comes to layout. You might have to play around with setting Hard coded values for Height anyway.

Upvotes: 1

Related Questions