Erhan Ballıeker

Xamarin.Forms ConfinstaApp Sample Bölüm 4 – SlideOverKit, Swipable Menu

Selamlar,

Bu serimizin son yazısında yine çok faydalı bir plugin den bahsetmek istiyorum. Hem bu örnek boyunca ilerlemiş olduğumuz Confinsta app i sona erdirecek hem de uygulamalarımızda bizden ekranın farklı yerlerinde farklı şekillerde açılıp kapanan menüler istendiğinde bunu kısa ve hızlı bir şekilde nasıl hallederiz buna bakacağız.

Kullanacak olduğumuz pluginimiz SlideOverKit

Yine solutionımızdaki her projeye bunu ekledikten sonrai plaform spesifik taraflarda gerekli initialize işlemini yapıyoruz.

Detaylı kullanımı için SlideOverKit in kendi github sayfasını ve örneklerini incelemenizi öneririm. Buradan ulaşabilirsiniz.

Bu plugin confinsta app boyunca Customrenderer yazmış olmamı gerekitiren tek şey. Ama aslında yazacağınız custom renderer ın örneklerden alıp copy paste yapmaktan başka neredeyse değiştirmeniz gereken peki bir kısmı kalmıyor(class isimleri hariç) 🙂

Ama arka planda neler yaptığını bilmek isterseniz benim gibi github dan inceleyebilirsiniz.

Önce bir ekranı hatırlayalım, sonra da tek tek kod taraflarına bakalım.

Resimde gördüğünüz gibi, sağ üst köşede ki hamburger buton ikonuna basıldığında ekranın yarısı kadar ene sahip ve tam boyda bir menü sayfası açıyoruz. Tıpkı instagramda olduğu gibi.

Önce sayfanın kendisine bir bakalım.

Sayfada yukarıdaki gibi bir navigation bar kullanmak eskiden custom renderer yazmak gerektirirdi. Ama yeni gelen özellikler ile sayfanın NavigationView ını istediğimiz gibi design edebiliyoruz artık xaml tarafında. Ben sağ üst köşeye bir imaj koydum ve buna bir tapgesture ekledim. Basıldığında da menü yü toggle ediyorum. Bu kadar. Xaml tarafı şu şekilde.

<?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="ConfinstaApp.Views.ProfileView">

    <NavigationPage.TitleView>
        <StackLayout HorizontalOptions="FillAndExpand" Orientation="Horizontal" Padding="10,0,10,0" Spacing="10">
            <Image Source="17.png" HorizontalOptions="StartAndExpand" WidthRequest="25" HeightRequest="25"></Image>
            <Label Text="erhanballieker" FontSize="Medium" FontAttributes="Bold" HorizontalOptions="CenterAndExpand" VerticalOptions="Center" Margin="0,0,25,0"/>
            <Image Source="11.png" HorizontalOptions="End" WidthRequest="25" HeightRequest="25">
                <Image.GestureRecognizers>
                    <TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped"></TapGestureRecognizer>
                </Image.GestureRecognizers>
            </Image>
        </StackLayout>
    </NavigationPage.TitleView>
    
    <ContentPage.Content>
        <StackLayout HorizontalOptions="Center" VerticalOptions="Center">
            <Label FontSize="Medium" FontAttributes="Bold" Text="erhanballieker Profile Page" HorizontalOptions="Center"></Label>
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

Yukarıda gördüğünüz gibi NavigationPage.TitleView içerisinde navigation design ım var. Sayfanın content ine hiçbir şey koymadım (Bir adet label ) dışında çünkü burada odaklandığımız yer sayfa içerisine eklediğimiz açılıp kapanan menü.

Sayfanın backend tarafı ise aşağıdaki gibi. Dikkat etmemiz gereken tek şey, Sayfanın ContentPage dışında bir de IMenuContainerPage interface ini implemente etmiş olması.

Bu interface den bize 3 adet property geliyor.

Bunlar;

  • SlideMenu Tipinde bir SlideMenu Prop u
  • Action tipinde bir ShowMenuAction prop u
  • Action tipinde bir HideMenuAction prop u

bunlar dışında sayfada sadece hamburger butonun gesturetapped event i var.

Bir diğer dikkat edeceğimiz nokta ise sayfanın constructor nda bana interface den gelen SlideMenu propertysine atadığım RightSideMenuContentView. 

	[XamlCompilation(XamlCompilationOptions.Compile)]
	public partial class ProfileView : ContentPage, IMenuContainerPage
        {
           public ProfileView()
	   {
	       InitializeComponent ();

               this.SlideMenu = new RightSideMenuContentView();
            }

        public SlideMenuView SlideMenu { get; set; }
        public Action ShowMenuAction { get; set; }
        public Action HideMenuAction { get; set; }

        private async void TapGestureRecognizer_Tapped(object sender, EventArgs e)
        {
            if (this.SlideMenu.IsShown)
            {
                HideMenuAction?.Invoke();
            }
            else
            {
                ShowMenuAction?.Invoke();
            }
        }
    }

RightSideMenuContentView ın xaml tarafına bir bakalım. Projemize bir adet ContentView ekleyip xaml tarafını aşağıdaki gibi düzenliyoruz.Basit bir StackLayout içerisinde bir kaç label var. Ama dikkat etmemiz gereken şey tüm sayfanın namespacinin SlideOverKit den geliyor olması.

<?xml version="1.0" encoding="utf-8" ?>
<t:SlideMenuView xmlns="http://xamarin.com/schemas/2014/forms" 
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
                 xmlns:t="clr-namespace:SlideOverKit" 
                 x:Class="ConfinstaApp.Views.SlideViews.RightSideMenuContentView">

    <StackLayout Padding="15,50,15,30" Spacing="20" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand">

        <Label HeightRequest="30" Margin="0,0,0,80" Text="Menu" FontSize="20" XAlign="Center" TextDecorations="Underline" />

        <Label HeightRequest="30" Text="Insights" FontSize="20" XAlign="Start" YAlign="Center" TextDecorations="Underline" />
        <Label HeightRequest="30" Text="Your Activity" FontSize="20" XAlign="Start" YAlign="Center" TextDecorations="Underline"/>
        <Label HeightRequest="30" Text="Nametag" FontSize="20" XAlign="Start" YAlign="Center" TextDecorations="Underline"/>
        <Label HeightRequest="30" Text="Saved" FontSize="20" XAlign="Start" YAlign="Center" TextDecorations="Underline"/>
        <Label HeightRequest="30" Text="Discover People" FontSize="20" XAlign="Start" YAlign="Center" TextDecorations="Underline"/>
        <Image HeightRequest="40" Source="nature.png"></Image>
        <Label HeightRequest="30" Text="Settings" FontSize="20" XAlign="Start" YAlign="Center" VerticalOptions="EndAndExpand" TextDecorations="Underline"/>

    </StackLayout>
</t:SlideMenuView>

Slide menünün backend tarafına bakalım. ContentView dan değil de SlideMenuView dan miras alıyor olmamız ilk dikkat çeken nokta. Bu miras ın bize sunduğu bir kaç property ile de nasıl görüneceğini ayarlıyoruz.

IsFullScreen true diyerek tam sayfa açılır olmasını söylüyoruz.

WidthRequest ile istediğimiz genişliği söylüyoruz.

MenuOrienations ı RightToLeft diyerek sağdan sola açılmasını belirtiyoruz.

[XamlCompilation(XamlCompilationOptions.Compile)]
	public partial class RightSideMenuContentView : SlideMenuView
    {
		public RightSideMenuContentView()
		{
			InitializeComponent ();
                        this.IsFullScreen = true;
                        this.WidthRequest = 250;
                        this.MenuOrientations = MenuOrientation.RightToLeft;

                        this.BackgroundColor = Color.White;
                        this.BackgroundViewColor = Color.Transparent;
                }
    }

Daha öncede de söylediğim gibi tüm projenin içerisinde sadece bir adet custom renderer var. o da bu plugin i kullanılabilir kulmak için. iOS Custom Renderer Kısmı aşağıdaki gibi.

[assembly: ExportRenderer(typeof(ProfileView), typeof(ProfileViewRenderer))]

namespace ConfinstaApp.iOS
{
    public class ProfileViewRenderer : PageRenderer, ISlideOverKitPageRendereriOS
    {
        public Action ViewDidAppearEvent { get; set; }

        public Action OnElementChangedEvent { get; set; }

        public Action ViewDidLayoutSubviewsEvent { get; set; }

        public Action ViewDidDisappearEvent { get; set; }

        public Action<CGSize, IUIViewControllerTransitionCoordinator> ViewWillTransitionToSizeEvent { get; set; }

        public ProfileViewRenderer()
        {
            new SlideOverKitiOSHandler().Init(this);
        }

        protected override void OnElementChanged(VisualElementChangedEventArgs e)
        {
            base.OnElementChanged(e);

            OnElementChangedEvent?.Invoke(e);
        }

        public override void ViewDidLayoutSubviews()
        {
            base.ViewDidLayoutSubviews();
            ViewDidLayoutSubviewsEvent?.Invoke();

        }

        public override void ViewDidAppear(bool animated)
        {
            base.ViewDidAppear(animated);
            ViewDidAppearEvent?.Invoke(animated);

        }

        public override void ViewDidDisappear(bool animated)
        {
            base.ViewDidDisappear(animated);
            ViewDidDisappearEvent?.Invoke(animated);
        }

        public override void ViewWillTransitionToSize(CGSize toSize, IUIViewControllerTransitionCoordinator coordinator)
        {
            base.ViewWillTransitionToSize(toSize, coordinator);
            ViewWillTransitionToSizeEvent?.Invoke(toSize, coordinator);
        }
    }
}

Custom renderer kullanmak zorunda olmanın yanında bir iyi haber ise şu. SlideOverKit sample larından aldığınız bir ios yada android rendererlarda sadece class isimlerini düzeltip başka bir customization yapmasanız bile proje çalışır hale gelecektir. Yani aslında tam da custom renderer yazıyor sayılmazsınız.

Android Custom Renderer kısmı

[assembly: ExportRenderer(typeof(ProfileView), typeof(ProfileViewRenderer))]
namespace ConfinstaApp.Droid
{

    public class ProfileViewRenderer : PageRenderer, ISlideOverKitPageRendererDroid
    {
        public Action OnElementChangedEvent { get; set; }

        public Action<bool, int, int, int, int> OnLayoutEvent { get; set; }

        public Action<int, int, int, int> OnSizeChangedEvent { get; set; }

        public ProfileViewRenderer (Context context) : base (context)
        {
            new SlideOverKitDroidHandler ().Init (this, context);
        }

        protected override void OnElementChanged (ElementChangedEventArgs e)
        {
            base.OnElementChanged (e);
            OnElementChangedEvent?.Invoke (e);
        }

        protected override void OnLayout (bool changed, int l, int t, int r, int b)
        {
            base.OnLayout (changed, l, t, r, b);
            OnLayoutEvent?.Invoke (changed, l, t, r, b);
        }

        protected override void OnSizeChanged (int w, int h, int oldw, int oldh)
        {
            base.OnSizeChanged (w, h, oldw, oldh);
            OnSizeChangedEvent?.Invoke (w, h, oldw, oldh);
        }
    }
}

Evet tüm hikaye bu kadar. .NetConf 2018 de sunumunu yapmış olduğum ve yaklaşık 1 iş günümü almış olan (sadece ui olduğu için :)) bir app in, xamarin forms ile ve xamarin forms a yeni gelen özellikler ile de nasıl bir şey ortaya çıkarabildiğini gördük.

Ekranları bir hatırlayalım.

Xamarin.Forms özelinde faydalı örneklerle dolu olduğunu düşündüğüm bu Confinsta app in proje haline buradan ulaşabilirsiniz. Aynı şekilde aynı gün yapmış olduğum diğer sunum dosyaları ve projeler burada mevcut.

Bir sonraki yazımda görüşmek üzere.

Xamarin.Forms ConfinstaApp Sample Bölüm 1

Selamlar,

Bir önceki yazımda paylaştığım geçtiğimiz haftalarda .Net Conf 2018 Istabul event inde Xamarin.Forms un gücünü gösterme amaçlı yapmış olduğum uygulamanın kod taraflarına bakacağımızı söylemiştim. Bunun içinde bayağı talep geldi doğrusu.

Şimdi tek tek bu sayfaların ve kodlarının üzerinden geçelim. Son yazımda eklediğim sırayla ekranların üzerinden geçelim.

Öncelikle şunu belirteyim, uygulama Tabbed template i üzerine kuruldu.

Capture

public partial class MainPage : TabbedPage
    {
        public MainPage()
        {
            InitializeComponent();
        } 
    }

kod behind tarafında pek birşey yok gördüğünüz üzere. Xaml tarafına baktığımız da durum şöyle;

 
<?xml version="1.0" encoding="utf-8" ?>
<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
            xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
            xmlns:views="clr-namespace:ConfinstaApp.Views"
            x:Class="ConfinstaApp.Views.MainPage">
    <TabbedPage.Children>
        <NavigationPage>
            <NavigationPage.Icon>
                <OnPlatform x:TypeArguments="FileImageSource">
                    <On Platform="iOS" Value="6.png"/>
                </OnPlatform>
            </NavigationPage.Icon>
            <NavigationPage.Title>
                <OnPlatform x:TypeArguments="x:String">
                    <On Platform="Android" Value="Main"></On>
                </OnPlatform>
            </NavigationPage.Title>
            <x:Arguments>
                <views:FeedView />
            </x:Arguments>
        </NavigationPage>
        
        <NavigationPage>
            <NavigationPage.Icon>
                <OnPlatform x:TypeArguments="FileImageSource">
                    <On Platform="iOS" Value="7.png"/>
                </OnPlatform>
            </NavigationPage.Icon>
            <NavigationPage.Title>
                <OnPlatform x:TypeArguments="x:String">
                    <On Platform="Android" Value="Main"></On>
                </OnPlatform>
            </NavigationPage.Title>
            <x:Arguments>
                <views:FeedCategoryView />
            </x:Arguments>
        </NavigationPage>
        <NavigationPage>
            <NavigationPage.Icon>
                <OnPlatform x:TypeArguments="FileImageSource">
                    <On Platform="iOS" Value="8.png"/>
                </OnPlatform>
            </NavigationPage.Icon>
            <NavigationPage.Title>
                <OnPlatform x:TypeArguments="x:String">
                    <On Platform="Android" Value="Main"></On>
                </OnPlatform>
            </NavigationPage.Title>
            <x:Arguments>
                <views:FeedView />
            </x:Arguments>
        </NavigationPage>
        <NavigationPage>
            <NavigationPage.Icon>
                <OnPlatform x:TypeArguments="FileImageSource">
                    <On Platform="iOS" Value="2.png"/>
                </OnPlatform>
            </NavigationPage.Icon>
            <NavigationPage.Title>
                <OnPlatform x:TypeArguments="x:String">
                    <On Platform="Android" Value="Main"></On>
                </OnPlatform>
            </NavigationPage.Title>
            <x:Arguments>
                <views:FeedActivityView />
            </x:Arguments>
        </NavigationPage>
        <NavigationPage>
            <NavigationPage.Icon>
                <OnPlatform x:TypeArguments="FileImageSource">
                    <On Platform="iOS" Value="12.png"/>
                </OnPlatform>
            </NavigationPage.Icon>
            <NavigationPage.Title>
                <OnPlatform x:TypeArguments="x:String">
                    <On Platform="Android" Value="Main"></On>
                </OnPlatform>
            </NavigationPage.Title>
            <x:Arguments>
                <views:ProfileView />
            </x:Arguments>
        </NavigationPage>
    </TabbedPage.Children>
</TabbedPage>

Yukarıda gördüğünüz gibi her 5 adet NavigationPage var. Her birinin Icon, Title ve argument yani göstereceği sayfa olarak da proje içerisinde oluşturmuş olduğum ContentPage lerin isimleri mevcut. Bu isimleri views:FeedView şeklinde göstermemizi sağlayan da yukarıda tanımladığımız namespecimiz :

xmlns:views=”clr-namespace:ConfinstaApp.Views”

Bunların dışında bu uygulamanın ana navigasyonunu belirlediğimiz bu sayfada daha da farklı şeyler yok.

Uygulamamızın App.xaml tarafında açılış sayfası olarak bu TabbedPage i miz set edilmiş durumda. İlk açılacağı sayfada xaml tarafına eklediğiniz ilk NavigationPage oluyor.

 public App()
        {
            InitializeComponent();
            MainPage = new MainPage();
        }

Uygulamanın ana çatısını gördükten sonra gelelim sayfalarımıza.

İlk sayfamız FeedView sayfamız.  Bu sayfanın xaml tarafını bakalım ve üzerinden konuşalım. Ekrandaki tüm akış aslında tek bir Grid içerisinde. 2 satır lı bu grid in içerisinde en üstte kullanıcıların yuvarlak profil imajlarının olduğu horizontal bir scrollView diğer satırda da bir ListView var. List view ın içerisinde DataTemplate  olarak 6 satırlık bir Grid var. Burada ki tek trik olan satır 2. satır. Çünkü burada farklı durumlara göre farklı içerikler basıyorum. Bunları ne şekilde yönettiğimi hemen aşağıda codebehind tarafına ve viewmodel tarafına geçince açıklayacağım. Ama aslında özetle bu satır da üst üste konulmuş,

  • ImageView
  • CarouselView
  • VideoView

var. Bunların hangisinin gösterilip hangisin gizleceneği de bu ListView Datasource olarak  verdiğim CardItem ismindeki sınıfın propertylerine bağlı. Bu kadar.

<NavigationPage.TitleView>
        <StackLayout HorizontalOptions="FillAndExpand" Orientation="Horizontal" Padding="10,0,10,0" Spacing="10">
            <Image Source="1ss.png" HorizontalOptions="StartAndExpand" WidthRequest="25" HeightRequest="25"></Image>
            <Label Text="Confinsta" FontSize="Medium"  TextDecorations="Underline" FontAttributes="Bold" HorizontalOptions="CenterAndExpand" VerticalOptions="Center" Margin="0,0,25,0"/>
            <Image Source="17.png" HorizontalOptions="End" WidthRequest="25" HeightRequest="25"></Image>
            <Image Source="3ss.png" HorizontalOptions="End" WidthRequest="25" HeightRequest="25"></Image>
        </StackLayout>
    </NavigationPage.TitleView>

    <ContentPage.Content>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="80"></RowDefinition>
                <RowDefinition Height="*"></RowDefinition>
            </Grid.RowDefinitions>

            <ScrollView Orientation="Horizontal" Grid.Row="0" HorizontalScrollBarVisibility="Never">
                <StackLayout Orientation="Horizontal" Spacing="10" Padding="10">
                    <StackLayout>
                        <controls:CircleImage BorderThickness="2" BorderColor="#ab423f" Source="https://randomuser.me/api/portraits/women/72.jpg" Aspect="AspectFill"></controls:CircleImage>
                        <Label Grid.Row="5"
                                       Margin="10,0,10,15"
                                       Text="portraits" 
                                       FontSize="Micro" 
                                       TextColor="Black"></Label>
                    </StackLayout>
                    <StackLayout>
                        <controls:CircleImage BorderThickness="2" BorderColor="#ab423f" Source="https://randomuser.me/api/portraits/women/1.jpg" Aspect="AspectFill"></controls:CircleImage>
                        <Label Grid.Row="5"
                                       Margin="10,0,10,15"
                                       Text="women" 
                                       FontSize="Micro" 
                                       TextColor="Black"></Label>
                    </StackLayout>
                    <controls:CircleImage BorderThickness="2" BorderColor="#ab423f" Source="https://randomuser.me/api/portraits/women/44.jpg" Aspect="AspectFill"></controls:CircleImage>
                    <controls:CircleImage BorderThickness="2" BorderColor="#ab423f" Source="https://randomuser.me/api/portraits/women/25.jpg" Aspect="AspectFill"></controls:CircleImage>
                    <controls:CircleImage BorderThickness="2" BorderColor="#ab423f" Source="https://randomuser.me/api/portraits/women/48.jpg" Aspect="AspectFill"></controls:CircleImage>
                    <controls:CircleImage BorderThickness="2" BorderColor="#ab423f" Source="https://randomuser.me/api/portraits/women/86.jpg" Aspect="AspectFill"></controls:CircleImage>
                    <controls:CircleImage BorderThickness="2" BorderColor="#ab423f" Source="https://randomuser.me/api/portraits/women/29.jpg" Aspect="AspectFill"></controls:CircleImage>
                    <controls:CircleImage BorderThickness="2" BorderColor="#ab423f" Source="https://randomuser.me/api/portraits/women/72.jpg" Aspect="AspectFill"></controls:CircleImage>
                    <controls:CircleImage BorderThickness="2" BorderColor="#ab423f" Source="https://randomuser.me/api/portraits/women/1.jpg" Aspect="AspectFill"></controls:CircleImage>
                    <controls:CircleImage BorderThickness="2" BorderColor="#ab423f" Source="https://randomuser.me/api/portraits/women/44.jpg" Aspect="AspectFill"></controls:CircleImage>
                    <controls:CircleImage BorderThickness="2" BorderColor="#ab423f" Source="https://randomuser.me/api/portraits/women/25.jpg" Aspect="AspectFill"></controls:CircleImage>
                    <controls:CircleImage BorderThickness="2" BorderColor="#ab423f" Source="https://randomuser.me/api/portraits/women/48.jpg" Aspect="AspectFill"></controls:CircleImage>
                    <controls:CircleImage BorderThickness="2" BorderColor="#ab423f" Source="https://randomuser.me/api/portraits/women/86.jpg" Aspect="AspectFill"></controls:CircleImage>
                    <controls:CircleImage BorderThickness="2" BorderColor="#ab423f" Source="https://randomuser.me/api/portraits/women/29.jpg" Aspect="AspectFill"></controls:CircleImage>
                </StackLayout>
            </ScrollView>

            <ListView x:Name="ItemsListView" 
                      Grid.Row="1"
                      ItemsSource="{Binding CardItems}"
                      SelectionMode="None"
                      SeparatorVisibility="None"
                      HasUnevenRows="true"
                      RefreshCommand="{Binding LoadCardItemsCommand}"
                      IsPullToRefreshEnabled="true"
                      IsRefreshing="{Binding IsBusy, Mode=OneWay}"
                      CachingStrategy="RecycleElement">
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <ViewCell Appearing="ViewCell_Appearing">
                            <Grid RowSpacing="5">
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="50"></RowDefinition>
                                    <RowDefinition Height="320"></RowDefinition>
                                    <RowDefinition Height="Auto"></RowDefinition>
                                    <RowDefinition Height="Auto"></RowDefinition>
                                    <RowDefinition Height="Auto"></RowDefinition>
                                    <RowDefinition Height="Auto"></RowDefinition>
                                </Grid.RowDefinitions>

                                <!--ROW 1-->
                                <StackLayout Grid.Row="0" Orientation="Horizontal" Spacing="10" Padding="10,5,10,5" HorizontalOptions="FillAndExpand">
                                    <controls:CircleImage BorderThickness="2" 
                                                      BorderColor="#33ccff" 
                                                      Source="{Binding ProfileImageUrl}" 
                                                      Aspect="AspectFill"></controls:CircleImage>

                                    <Label Text="{Binding Name}" 
                                           FontSize="Small" 
                                           TextColor="Black"
                                           VerticalOptions="Center"></Label>

                                    <Image HorizontalOptions="EndAndExpand" WidthRequest="22" HeightRequest="20" Aspect="AspectFit" Source="10.png">
                                        <Image.GestureRecognizers>
                                            <TapGestureRecognizer Tapped="MenuTapped"></TapGestureRecognizer>
                                        </Image.GestureRecognizers>
                                    </Image>
                                </StackLayout>
                                
                                <!--ROW 2 Image, Slide or Video (indicator and error label for video errors and loadings.)-->
                                <Image Grid.Row="1"
                                       Source="{Binding ImageUrl}"
                                       IsVisible="{Binding IsSinglePhotoPost}"
                                       Aspect="AspectFill"
                                       BackgroundColor="Black">
                                    <Image.GestureRecognizers>
                                        <TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped"></TapGestureRecognizer>
                                    </Image.GestureRecognizers>
                                </Image>

                                <forms:VideoView x:Name="videoView"
                                                 Grid.Row="1" 
                                                 IsVisible="{Binding IsVideoPost}"
                                                 HorizontalOptions="FillAndExpand"
                                                 VerticalOptions="FillAndExpand"
                                                 AspectMode="AspectFill">
                                    <forms:VideoView.GestureRecognizers>
                                        <TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped"></TapGestureRecognizer>    
                                    </forms:VideoView.GestureRecognizers>
                                </forms:VideoView>

                                <carousel:CarouselViewControl x:Name="carouselView"
                                                              Orientation="Horizontal" 
                                                              InterPageSpacing="0" 
                                                              ShowIndicators="True"
                                                              IndicatorsShape="Circle"
                                                              IndicatorsTintColor="White"
                                                              CurrentPageIndicatorTintColor="Black"
                                                              Grid.Row="1" 
                                                              IsVisible="{Binding IsMultiPhotoPost}"
                                                              ItemsSource="{Binding PhotosItemsSource}">
                                    <carousel:CarouselViewControl.ItemTemplate>
                                        <DataTemplate>
                                            <Image 
                                                   Source="{Binding PhotoUrl}"
                                                   Aspect="AspectFill"
                                                   BackgroundColor="Black"></Image>
                                        </DataTemplate>
                                    </carousel:CarouselViewControl.ItemTemplate>
                                    <carousel:CarouselViewControl.GestureRecognizers>
                                        <TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped"></TapGestureRecognizer>
                                    </carousel:CarouselViewControl.GestureRecognizers>
                                </carousel:CarouselViewControl>

                                <ActivityIndicator Grid.Row="1"
                                                   VerticalOptions="Center"
                                                   Color="Black"
                                                   HorizontalOptions="Center"
                                                   IsVisible="{Binding IsVideoBuffering}"
                                                   IsRunning="{Binding IsVideoBuffering}"></ActivityIndicator>

                                <Label Text="There is an error occured while loading the video" 
                                       Grid.Row="1"
                                       IsVisible="{Binding ErrorOnVideoLoadBindable}"
                                       FontSize="Small" 
                                       TextColor="Gray"
                                       VerticalOptions="Center"
                                       HorizontalOptions="Center"></Label>

                                <!--ROW 3 function buttons-->
                                <StackLayout Grid.Row="2" Orientation="Horizontal" Spacing="10" Padding="10,0,10,0">
                                    <Image WidthRequest="23" HeightRequest="23" Aspect="AspectFit" Source="2.png"></Image>
                                    <Image WidthRequest="23" HeightRequest="23" Aspect="AspectFit" Source="5.png"></Image>
                                    <Image WidthRequest="23" HeightRequest="23" Aspect="AspectFit" Source="3.png"></Image>

                                    <Image HorizontalOptions="EndAndExpand" WidthRequest="23" HeightRequest="23" Aspect="AspectFit" Source="4.png"></Image>
                                </StackLayout>

                                <!--ROW 4 like count label-->
                                <Label Grid.Row="3"
                                       Text="{Binding LikeCount, StringFormat='{0} likes'}" 
                                       IsVisible="{Binding HaveLike}" 
                                       FontSize="Micro" 
                                       Margin="10,0,10,0"
                                       TextColor="Black"></Label>

                                <!--ROW 5 Showcase command-->
                                <StackLayout Grid.Row="4" IsVisible="{Binding HaveComment}" Spacing="10" Padding="10,0,10,0" Orientation="Horizontal">
                                    <Label
                                       Text="{Binding ShowcaseComment.Name}" 
                                        MinimumWidthRequest="100"
                                       FontSize="Micro" 
                                       FontAttributes="Bold"
                                       TextColor="Black"></Label>

                                    <Label
                                       Text="{Binding ShowcaseComment.Comment}" 
                                       FontSize="Micro" 
                                       LineBreakMode="TailTruncation"
                                        MaxLines="2"
                                        TextColor="Black"></Label>
                                </StackLayout>

                                <!--ROW 6 More Comment Selection-->
                                <Label Grid.Row="5"
                                       Margin="10,0,10,15"
                                       Text="{Binding CommentCount, StringFormat='View all {0} comments'}" 
                                       IsVisible="{Binding HaveMoreComment}" 
                                       FontSize="Micro" 
                                       TextColor="Gray"></Label>

                            </Grid>
                        </ViewCell>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
        </Grid>

Uygulamanın FeedView sayfasında herşey aslında aşağıdaki 3 model üzerinden dönüyor. INotifyPropertyChanged imlplemente etmiş bir CardItem ve buna bağlı CardComment ve Photos sınıfları mevcut. FeedView gösterilmek üzere örneğin kolaylıktan çıkmaması için 3 farklı post tipi düşündüm.

Bir Card yani FeedView da listelenecek herbir görsel şu 3 tiptten biri olabilir.

  • Tek fotoğraflı içerik
  • Birden çok fotoğralı içerik
  • Video lu içerik

Bu üçü şuan kod tarafında hardcoded gömülmüş durumda. Bu tabii ki daha doğru bir yazılım pattern i uygulanarak çözülmeli gerçek hayat örneğinde çünkü her yeni gelecek olan içerik tipi için benim xaml ve .cs taraflarına çok d okunmyor olmam lazım. Ama dediğim gibi bu Confinsta App in amacı tamamen önyüz de nelerin ne kadar hızlı yapılabileceğini göstermek olduğundan buralara takılmayınız.

    public class CardItem : INotifyPropertyChanged
    {
        public string ProfileImageUrl { get; set; }
        public string ImageUrl { get; set; }
        public string VideoUrl { get; set; }

        private bool isVideoBuffering;
        public bool IsVideoBuffering
        {
            get => isVideoBuffering;
            set
            {
                isVideoBuffering = value;
                OnPropertyChanged();
            }
        }

        private bool errorOnVideoLoad;

        public bool ErrorOnVideoLoad
        {
            get { return errorOnVideoLoad; }
            set
            {
                errorOnVideoLoad = value;
                OnPropertyChanged();
            }
        }


        public string Name { get; set; }
        public int LikeCount { get; set; }
        public List<CardComment> Comments { get; set; }
        public List<Photos> PhotosItemsSource { get; set; }
        public bool IsSinglePhotoPost { get; set; }
        public bool IsVideoPost { get; set; }
        public bool IsMultiPhotoPost { get; set; }
        public bool ErrorOnVideoLoadBindable => ErrorOnVideoLoad && IsVideoPost;

        public CardComment ShowcaseComment => Comments?.FirstOrDefault() ?? new CardComment();
        public bool HaveComment => Comments?.Count > 0;
        public bool HaveMoreComment => Comments?.Count > 1;
        public bool HaveLike => LikeCount > 0;
        public int CommentCount => Comments != null ? Comments.Count : 0;

        public event PropertyChangedEventHandler PropertyChanged;

        public void OnPropertyChanged([CallerMemberName] string propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

    }

    public class CardComment
    {
        public string Name { get; set; }
        public string Comment { get; set; }
    }

    public class Photos
    {
        public string PhotoUrl { get; set; }
        public string Title { get; set; }
    }

Yukarıdaki 3 modelin ve FeedView sayfasının arasında bir FeedViewModel ilimiz olacak. Hemen aşağıda onu da paylaşacağım ama bundan önce size FeedView un .cs tarafında neler bunları göstereyim.

Video oynatma işlemi için MediaManager pluginini kullandığımı söylemiştim. Genel olarak .cs tarafında 3 field ım var. Biri ViewModel in kendisi. Initialize edip, BindingContext e atıyorum, xaml tarafında Bindingleri buradan yöneteceğim. Biri IPlaybackController MediaManager a ait bir interface ki aslında CrossMediaManager.Current.PlaybackController olarak atanmış.

Bir diğeri de ListView içerisinde her bir item gösterilirken sakladığım o an ki CardItem. Bunu dışarıda tanımlayıp aşağıda ViewCell_Appearing event inde her bir yeni item geldiğinde değiştiriyor olmamın tek sebebi, PlayBackControllerın status_changed inde o anki statuse göre bazı propertylerini değiştirmek. Mesela video load olurken bir hata olduğunda, uygulama üzerinde video bastığım yerde “Yüklenirken bir hata oluştu” demek için bir label var ve bu label ın görünüp görünmemesi CardItem daki ErrorOnVideoLoadBindable propertysine bağlı, yada aynı şekilde video yüklenirken tam ortada bir ActivityIndicator göstereceğim, video nun load olduğunu göstermek için, bunun için de CardItem IsVideoBuffering properysinin değerini bu statuschanged anında değiştirip, önyüzde activity indicator ın görünüp görünmeyeceğini kontrol ediyorum. VideoPlayer ve ViewCellAppearing eventlari dışında 2 event daha var. Bunlarda video oynarken oradaki item a tıklandığında videonun sesini kapatmak için, bir diğeri de 3 noktalı imaja basıldığında bir ActionSheet göstermek için.

        FeedViewModel viewModel;
        private IPlaybackController PlaybackController => CrossMediaManager.Current.PlaybackController;
        private CardItem CurrentItem;
        public FeedView()
        {
            InitializeComponent();

            BindingContext = viewModel = new FeedViewModel();
        }

        protected override void OnAppearing()
        {
            base.OnAppearing();
            CrossMediaManager.Current.StatusChanged += Current_StatusChanged;
        }

        private void Current_StatusChanged(object sender, Plugin.MediaManager.Abstractions.EventArguments.StatusChangedEventArgs e)
        {
            switch (e.Status)
            {
                case MediaPlayerStatus.Stopped:
                    CurrentItem.IsVideoBuffering = false;
                    break;
                case MediaPlayerStatus.Paused:
                    CurrentItem.IsVideoBuffering = false;
                    break;
                case MediaPlayerStatus.Playing:
                    CurrentItem.IsVideoBuffering = false;
                    break;
                case MediaPlayerStatus.Loading:
                    CurrentItem.IsVideoBuffering = true;
                    break;
                case MediaPlayerStatus.Buffering:
                    CurrentItem.IsVideoBuffering = true;
                    break;
                case MediaPlayerStatus.Failed:
                    CurrentItem.IsVideoBuffering = false;
                    CurrentItem.ErrorOnVideoLoad = true;
                    break;
                default:
                    break;
            }
        }

        private async  void ViewCell_Appearing(object sender, EventArgs e)
        {
            if (sender is ViewCell cell)
            {
                CurrentItem = cell.BindingContext as CardItem;

                var videoView = cell.View.FindByName("videoView");
                bool IsVideoContent = (cell.BindingContext as CardItem).IsVideoPost;

                if (videoView != null && IsVideoContent)
                {
                    videoView.Source = CurrentItem.VideoUrl;
                    await CrossMediaManager.Current.Play(CurrentItem.VideoUrl, MediaFileType.Audio, ResourceAvailability.Remote);
                }
                else
                {
                    if (CrossMediaManager.Current.Status == MediaPlayerStatus.Loading || 
                        CrossMediaManager.Current.Status == MediaPlayerStatus.Buffering ||
                        CrossMediaManager.Current.Status == MediaPlayerStatus.Playing)
                    {
                        await CrossMediaManager.Current.Stop();
                        CurrentItem.ErrorOnVideoLoad = false;
                    }
                }
            }
        }

        private async void TapGestureRecognizer_Tapped(object sender, EventArgs e)
        {
            CrossMediaManager.Current.VolumeManager.Mute = !CrossMediaManager.Current.VolumeManager.Mute;           
        }

        private async void MenuTapped(object sender, EventArgs e)
        {
            await DisplayActionSheet(string.Empty, "Cancel", "Unfollow", "Share to Facebook", "Share to Whatsapp", "Copy Link", "Turn On Post Notifications", "Report");
        }
    }

ViewModel aşağıdaki gibi. Sadece dummy data ile bir ObservableCollection tipindeki CardItem listemi doldurduğum bir LoadCardItems metodu. bir de ListView ın RefreshCommand ine bind ettiğim bir ExecuteLoadCardItemsCommand command i mevcut. Bir de sadece IsBusy kontrolü yaptığım bir BaseViewModel im var.

public class FeedViewModel : BaseViewModel
    {
        public ObservableCollection CardItems { get; set; }
        public Command LoadCardItemsCommand { get; set; }

        public FeedViewModel()
        {
            CardItems = new ObservableCollection();

            LoadCardItems();

            LoadCardItemsCommand = new Command(() => ExecuteLoadCardItemsCommand());
        }

        private void LoadCardItems()
        {
            CardItems.Add(new CardItem
            {
                ProfileImageUrl = "https://randomuser.me/api/portraits/women/29.jpg",
                Name = "Ashley Cole",
                ImageUrl = "https://images.unsplash.com/photo-1539604880233-d282d9bac272?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=902aa0705d67ac390c0170c68aa4907f&auto=format&fit=crop&w=1051&q=80",
                Comments = new List {
                    new CardComment { Name = "name orlk", Comment = "orduk plsd durol solre lorem ipsum doler ot durol solre ler amet durol solre lorem ipsum orduk plsd durol solre lorem ipsum doler"},
                    new CardComment { Name = "suhlt sooa", Comment = "orduk plsd durol solre lorem ipsum doler amet durol solre lorem ipsum orduk plsd durol solre lorem ipsum doler amet durol solre " }
                },
                LikeCount = 13,
                IsSinglePhotoPost = true
            });

            CardItems.Add(new CardItem
            {
                ProfileImageUrl = "https://randomuser.me/api/portraits/women/51.jpg",
                Name = "Kevin Spacey",
                ImageUrl = "https://images.unsplash.com/photo-1539608170043-f55d83afe1c9?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=89a54fa339be3dde93fa137a213655c7&auto=format&fit=crop&w=1189&q=80",
                Comments = new List {
                    new CardComment { Name = "brad", Comment = "lorem ipsum doler amet"},
                    new CardComment { Name = "suhlt sooa", Comment = "orduk plsd durol solre" },
                    new CardComment { Name = "suhlt sooa", Comment = "orduk plsd durol solre" },
                    new CardComment { Name = "suhlt sooa", Comment = "orduk plsd durol solre" },
                    new CardComment { Name = "suhlt sooa", Comment = "orduk plsd durol solre" },
                    new CardComment { Name = "suhlt sooa", Comment = "orduk plsd durol solre" }
                },
                LikeCount = 99,
                IsSinglePhotoPost = true
            });

            CardItems.Add(new CardItem
            {
                ProfileImageUrl = "https://randomuser.me/api/portraits/women/91.jpg",
                Name = "Brad Pitt",
                ImageUrl = "https://images.unsplash.com/photo-1539593608687-ccae798ff3ba?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=abbf25cb0a9e3f706263ff5fa81bf9d9&auto=format&fit=crop&w=1051&q=80",
                LikeCount = 4,
                IsSinglePhotoPost = true
            });

            CardItems.Add(new CardItem
            {
                ProfileImageUrl = "https://randomuser.me/api/portraits/women/84.jpg",
                Name = "Anniston Jeniffer",
                ImageUrl = "https://images.unsplash.com/photo-1539576282236-40272d2dbe7e?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=9cee2739772eb498885974c2f2542a83&auto=format&fit=crop&w=1050&q=80",
                Comments = new List {
                    new CardComment { Name = "jeniffer", Comment = "lorem ipsum doler amet"},
                    new CardComment { Name = "suhlt sooa", Comment = "orduk plsd durol solre" }
                },
                LikeCount = 12,
                IsVideoPost = true,
                VideoUrl = "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"
            });

            CardItems.Add(new CardItem
            {
                ProfileImageUrl = "https://randomuser.me/api/portraits/women/84.jpg",
                Name = "Chris Yang",
                ImageUrl = "https://images.unsplash.com/photo-1539603094258-e61b393cbd52?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=72d48ccd425134d86f1c5f82e0103e38&auto=format&fit=crop&w=1050&q=80",
                Comments = new List {
                    new CardComment { Name = "Anniston", Comment = "lorem ipsum doler amet"},
                },
                LikeCount = 1,
                IsMultiPhotoPost = true,
                PhotosItemsSource = new List {
                    new Photos { Title = "", PhotoUrl = "https://images.unsplash.com/photo-1539576282236-40272d2dbe7e?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=9cee2739772eb498885974c2f2542a83&auto=format&fit=crop&w=1050&q=80"},
                    new Photos { Title = "", PhotoUrl = "https://images.unsplash.com/photo-1539603094258-e61b393cbd52?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=72d48ccd425134d86f1c5f82e0103e38&auto=format&fit=crop&w=1050&q=80"},
                    new Photos { Title = "", PhotoUrl = "https://images.unsplash.com/photo-1539593608687-ccae798ff3ba?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=abbf25cb0a9e3f706263ff5fa81bf9d9&auto=format&fit=crop&w=1051&q=80"},
                    new Photos { Title = "", PhotoUrl = "https://images.unsplash.com/photo-1539608170043-f55d83afe1c9?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=89a54fa339be3dde93fa137a213655c7&auto=format&fit=crop&w=1189&q=80"}
                }
            });
        }

        void ExecuteLoadCardItemsCommand()
        {
            if (IsBusy)
                return;

            IsBusy = true;

            LoadCardItems();

            IsBusy = false;
        }

    }

BaseViewModel de şu şekilde. Sadece INotify… dan imlemente olan bir sınıf. Encapsulate edilmiş bir adet isBusy field ve propery si var.

   public class BaseViewModel : INotifyPropertyChanged
    {

        bool isBusy = false;
        public bool IsBusy
        {
            get => isBusy;
            set
            {
                isBusy = value;
                OnPropertyChanged();
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
        

Uygulamanın navigation yapısını ve FeedView dediğimiz ana sayfasını görmüş olduk. Bir sonraki post um da Confinsta uygulamasının detaylarına devam edeceğim.

Bir sonraki yazımda görüşmek üzere.