Xamarin.Forms ConfinstaApp Sample Bölüm 2
Selamlar,
Geçen yazımızda anlatmaya başladığım xamarin.forms ile kısa sürede ortaya çıkarabildiğim ve .NetConf da anlattığım Confinsta App in detaylarına devam ediyoruz.
En son uygulamanın temel navigasyon yapısını Tabbed template i ile nasıl oluşturuğumuzdan ve FeedView.xaml, .cs ve ViewModel tarafarından bahsetmiştik.
Bu yazımızda da xamarin yeni gelen özelliklerinden olan FlexLayout, SwipeGestureRecognizer gibi yeni özellikleri Instagramın Explore sayfasına benzer olarak bir sayfa yapmak için nasıl kullandığımıza bakalım.
Bu sayfada bir navigationbar bulunmuyor. Sayfa temel olarak 3 satırı olan bir Grid den oluşuyor. ilk satırında bir searchbar ve buton mevcut. İkinci satırında ise ana sayfadaki kulanıcı profil resimlerine benzer biraz daha eliptik imajlar üzerinden kategori isimlerinin olduğu bir scrollview var. üçüncü satır ise sadece bir FlexLayout tan oluşuyor. FlexLayout tan önce Native taraflardaki CollectionView görünümüne benzer olarak bir görüntü olutşturmak için GridView dinamil olarak satır ve sütunları kod tarafında oluşturup her bir içeriği onun ilgili satır ve sütununa basmak için uğraştırıcı böir yöntem kullanılıyorduk. FlexLayout da ise bir kaç property ile nasıl davramasını istediğinizi söylüyorsunuz ve içeriğini kod tarafta verdikten sonra gerisini o hallediyor. Yani FlexLayout un en güzel kullanım alanı bence, içeriği dinamil olarak değişecek olan UI larda ekranın yapısını ona bırakmak.
Bu ekranın xaml tarafı aşağıdaki gibi;
<?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:controls="clr-namespace:ImageCircle.Forms.Plugin.Abstractions;assembly=ImageCircle.Forms.Plugin" x:Class="ConfinstaApp.Views.FeedCategoryView"> <Grid Padding="0,20,0,0"> <Grid.RowDefinitions> <RowDefinition Height="50"></RowDefinition> <RowDefinition Height="70"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <StackLayout Orientation="Horizontal" Grid.Row="0" HeightRequest="40" Padding="10" Spacing="10"> <SearchBar Placeholder="Search" HorizontalOptions="FillAndExpand" WidthRequest="300" HeightRequest="30" BackgroundColor="Transparent"></SearchBar> <Image HorizontalOptions="EndAndExpand" Source="3ss.png" WidthRequest="20" HeightRequest="20"></Image> </StackLayout> <ScrollView Orientation="Horizontal" Grid.Row="1" HorizontalScrollBarVisibility="Never"> <StackLayout Orientation="Horizontal"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <controls:CircleImage Grid.Row="0" Grid.RowSpan="2" WidthRequest="100" HeightRequest="80" Source="music.jpg" Aspect="AspectFill"></controls:CircleImage> <Label Grid.Row="1" HorizontalOptions="Center" VerticalTextAlignment="Center" Text="Music" FontSize="Small" FontAttributes="Bold" TextColor="White"></Label> </Grid> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <controls:CircleImage Grid.Row="0" Grid.RowSpan="2" WidthRequest="100" HeightRequest="80" Source="nature.jpg" Aspect="AspectFit"></controls:CircleImage> <Label Grid.Row="1" HorizontalOptions="Center" VerticalTextAlignment="Center" Text="Nature" FontSize="Small" FontAttributes="Bold" TextColor="White"></Label> </Grid> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <controls:CircleImage Grid.Row="0" Grid.RowSpan="2" WidthRequest="100" HeightRequest="80" Source="science.jpg" Aspect="AspectFill"></controls:CircleImage> <Label Grid.Row="1" HorizontalOptions="Center" VerticalTextAlignment="Center" Text="Science" FontAttributes="Bold" FontSize="Small" TextColor="White"></Label> </Grid> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <controls:CircleImage Grid.Row="0" Grid.RowSpan="2" WidthRequest="100" HeightRequest="80" Source="sports.jpg" Aspect="AspectFill"></controls:CircleImage> <Label Grid.Row="1" HorizontalOptions="Center" VerticalTextAlignment="Center" Text="Sports" FontAttributes="Bold" FontSize="Small" TextColor="White"></Label> </Grid> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <controls:CircleImage Grid.Row="0" Grid.RowSpan="2" WidthRequest="100" HeightRequest="80" Source="animals.jpg" Aspect="AspectFill"></controls:CircleImage> <Label Grid.Row="1" HorizontalOptions="Center" VerticalTextAlignment="Center" Text="Animals" FontAttributes="Bold" FontSize="Small" TextColor="White"></Label> </Grid> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <controls:CircleImage Grid.Row="0" Grid.RowSpan="2" WidthRequest="100" HeightRequest="80" Source="fitness.jpg" Aspect="AspectFill"></controls:CircleImage> <Label Grid.Row="1" HorizontalOptions="Center" VerticalTextAlignment="Center" Text="Fitness" FontAttributes="Bold" FontSize="Small" TextColor="White"></Label> </Grid> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <controls:CircleImage Grid.Row="0" Grid.RowSpan="2" WidthRequest="100" HeightRequest="80" Source="comics.jpg" Aspect="AspectFill"></controls:CircleImage> <Label Grid.Row="1" HorizontalOptions="Center" VerticalTextAlignment="Center" Text="Comics" FontAttributes="Bold" FontSize="Small" TextColor="White"></Label> </Grid> </StackLayout> </ScrollView> <ScrollView Grid.Row="2"> <FlexLayout x:Name="flexLayout" Wrap="Wrap" JustifyContent="SpaceEvenly" /> </ScrollView> </Grid> </ContentPage>
Xaml tarafında yukarıda bahsettiğim gibi, dikkat edeceğim en önemli yer Grid View ın son satırına attığımız ScrollView içerisine koyduğumuz FlexLayout.
Sadece iki tane özellikle FlexLayout üzerinde, ekranda eşit aralıklarla dizilmiş bir koleksiyon göstermek istediğimi söylüyoruz.
- Wrap: Wrap olarak set ederek, tek bir satıra sığmayan itemları eşit aralıklarla alt alta dizmesini istediğimizi söylüyoruz. Örneğim iphone 6 ekranında test ettiğimiz de ekrana 3 kolon olacak şekilde diziyor. Telefon yan çevirdiğimiz de ise aralıkları biraz daha arttırarak (ama eşit olacak şekilde) kolon sayısını 5 e çıkartıyor.
- JustifyContent: SpaceEvenly diyerek de yukarıda bahsettiğimiz gibi aralıkların eşit olarak bölünmesini istediğimizi söylüyoruz.
sayfanın c# tarafında neler yaptığımıza bakalım. Burada yaptığımız şey aslında sadece viewmodel i initialize edip sayfanın BindingContext ine atadıktan sonra, model içerisinde olan PhotoList içerisinde dönüp her biri için istediğimiz UI ı kod tarafında oluşturmuş oluyoruz. Bunun için bir content view oluşturup UI orada xaml tarafında da oluşturabilirdik, bu örnekte ben kod tarafında oluşturmayı seçtim. Aslında bu kod tarafından ui oluşturma alışkanlığım doğru mu yanlış mı bilmem ama, native developer lar arasında halen tartışılmakta olan şeyler (ör; native ios da storyboard kullanılmalı mı kullanılmama lı gibi).
UI için sadece bir imaj oluşturduk Width ve Height değerleri fix 120. Buna göre FlexLayout hangi boyutta ekran karşısına gelirse gelsin, yanyana sığdırabildiği kadar sığdırıp gerisini farklı satırlara bölüyor.
Her bir imaja instagram da olduğu gibi uzun basıldığında popup olarak açılma ve yukarı slide edildiğinde ActionSheet gösterip kullanıcıya seçenekler sunma kısmını da yapabilmek için şunu yaptım.
Xamarin forms a yeni gelen SwiperGestureRecognizer ile yukarı parmak hareketini algılayıp resmi azıcık yukarı doğru kaydırıyorum ve ActionSheet açıyorum. Ama buradan sırayla bahsetmek gerekirse adımlar şöyle;
- Xamarin forms da LongTapGestureRecognizer olmadığından dolayı ben burada iki tıkla zorunlu bir TapGestureRecognizer koydum. Çift tap yapıldığında pop içerisinde resmi açıyorum.
- Popup için Rg.Plugins.Popup plugini ni kullandım. Açılan popup tasarımı ve kod behind ı ayrı dosyalarda. Resmi yukarı kaydırma ve popup ı komple animate etme kısmı bu tarafta bulunuyor.
[XamlCompilation(XamlCompilationOptions.Compile)] public partial class FeedCategoryView : ContentPage { FeedCategoryViewModel viewModel; public FeedCategoryView() { InitializeComponent(); NavigationPage.SetHasNavigationBar(this, false); viewModel = new FeedCategoryViewModel(); foreach (var item in viewModel.PhotoList) { var image = new Image() { Source = new UriImageSource { Uri = new Uri(item.PhotoUrl), CacheValidity = TimeSpan.FromHours(1), CachingEnabled = true }, WidthRequest = 120, HeightRequest = 120, Margin = new Thickness { Left = 0, Top = 3, Right = 0, Bottom = 0}, Aspect = Aspect.AspectFill, }; image.GestureRecognizers.Add(new TapGestureRecognizer { NumberOfTapsRequired = 2, Command = new Command(async () => { await Navigation.PushPopupAsync(new CategoryFeedPopupView(item)); }) }); flexLayout.Children.Add(image); } } }
Popup tarafına bakalım.
Eğer bu plugini daha önce hiç kullanmadıysanız hemen denemenizi tavsiye ederim. Popuplar la uğraşmak uygulama içerisindeki en kolay işlemlere dönüyor. Rg.Plugins.Popup nugetten indirip (hem standard hemde ios android projelerine de) uygulama spesifik taraflarda init etmeniz yeterli. Örneğin ios tarafına AppDelegate de aşağıdaki kodu LoadApplication dan önce yazmanız yeterli.
Rg.Plugins.Popup.Popup.Init();
sayfanın xaml tarafı aşağıdaki gibi. Birkaç farklı gelebilecek namespace var. Biri bu sayfanın bir popup sayfasına dair olduğu namespace, biri açılırken ki animasyon için olan diğerleride popup ın davranışlarını belirleyen özellikler
Örneğin;
CloseWhenBackgroundIsClicked=”False” diyerek popup ın arka kısmında ekranda bir yere basıldığına kapanmasın demiş oluyoruz. True dersek kapanır tıkladığımızda.
Tüm ekranı kenarları oval bir şekilde göstermek için Frame içine alıp BorderRadius özelliğini kullandım. Popup ın ekranın ortasında olması ve tüm ekranı kaplamaması içinde frame e VerticalOptions=”Center” dedim.
<?xml version="1.0" encoding="UTF-8"?> <pages:PopupPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:pages="clr-namespace:Rg.Plugins.Popup.Pages;assembly=Rg.Plugins.Popup" x:Class="ConfinstaApp.Views.Popups.CategoryFeedPopupView" xmlns:animations="clr-namespace:Rg.Plugins.Popup.Animations;assembly=Rg.Plugins.Popup" xmlns:controls="clr-namespace:ImageCircle.Forms.Plugin.Abstractions;assembly=ImageCircle.Forms.Plugin" CloseWhenBackgroundIsClicked="False" InputTransparent="False" Padding="20,0,20,0"> <pages:PopupPage.Animation> <animations:ScaleAnimation PositionIn="Center" PositionOut="Center" HasBackgroundAnimation="True" /> </pages:PopupPage.Animation> <Frame BackgroundColor="#fff" VerticalOptions="Center" CornerRadius="15" x:Name="mainFrame"> <Frame.GestureRecognizers> <SwipeGestureRecognizer Direction="Down" Swiped="SwipeGestureRecognizer_Swiped"></SwipeGestureRecognizer> <SwipeGestureRecognizer Direction="Up" Swiped="SwipeGestureRecognizer_Swiped"></SwipeGestureRecognizer> </Frame.GestureRecognizers> <StackLayout IsClippedToBounds="True" Spacing="0"> <StackLayout Orientation="Horizontal" BackgroundColor="White" Padding="10" Spacing="0"> <controls:CircleImage HorizontalOptions="StartAndExpand" Source="https://randomuser.me/api/portraits/women/72.jpg" Aspect="AspectFill" WidthRequest="50" HeightRequest="50"></controls:CircleImage> <Label FontSize="Large" TextColor="Black" FontAttributes="Bold" Text="Jojo Mayer" VerticalOptions="Center" HorizontalOptions="StartAndExpand"></Label> </StackLayout> <Image Source="{Binding PhotoUrl}" WidthRequest="300" HeightRequest="250"></Image> </StackLayout> </Frame> </pages:PopupPage>
Dikkat ederseniz frame e xaml tarafında atanmış iki tane swipegesture var.
Up kısmında popup açıcaz, Down olarak set ettiğimizde ise popup ı kapayacağız.
Kod tarafı aşağıdaki gibi.İlk dikkat edeceğimiz şey sayfanın bir ContentPage sınıfından değil de PopupPage den türüyor olması. Sayfaya dışardan bir adet Photo sınıfı geçiyoruz bunu da BindingContext e set ediyoruz. Resim ve ismi bu şekilde xaml tarafına bind ediyoruz.
İkinci dikkat etmemiz gereken yer de, SwipeGesture ların aynı eventhandler ları kullanıyor olması. event e gelen EventArgument lardan Direction propertysi ile hangi gesture ile uğraşttığımızı anlayabiliyoruz.
Down ise sadece popup ı kapatıyoruz. Bu arada bu popup plugin inin Navigation üzerine yazılmış extension metodları mevcut. Yani normal xamarin.forms daki sayfa geçişlerinde Navigation sınıfını kullandığımız gibi popup lar içinde kullanabiliriz.
Örneğin popup ı kapatmak için;
await Navigation.PopPopupAsync(); diyebiliriz.
[XamlCompilation(XamlCompilationOptions.Compile)] public partial class CategoryFeedPopupView : PopupPage { Photos selecedPhoto; public CategoryFeedPopupView(Photos selectedPhoto) { InitializeComponent (); this.selecedPhoto = selectedPhoto; BindingContext = selecedPhoto; } private async void SwipeGestureRecognizer_Swiped(object sender, SwipedEventArgs e) { if (e.Direction == SwipeDirection.Down) { await Navigation.PopPopupAsync(); } else if(e.Direction == SwipeDirection.Up) { mainFrame.TranslateTo(0, -140, 300, Easing.CubicOut); var res = await DisplayActionSheet(string.Empty, "Cancel", "Share", "Like"); if (!string.IsNullOrEmpty(res)) { mainFrame.TranslateTo(0, 0, 150, Easing.CubicOut); await Navigation.PopPopupAsync(); } } } }
Up olarak slide ettiğimiz de ise en dıştaki Frame imize verdiğimiz name ile ona kod tarafından ulaşıp,
mainFrame.TranslateTo(0, 0, 150, Easing.CubicOut);
diyerek yukarı kaydırıyoruz.
Genel olarak sayfamıza dönecek olursak, sayfada kullandığımız viewmodel ise aşağıdaki gibi. Hiçbir numara yok, sadece dummy bir Photo sınıfı listesi oluşturuyoruz ObservableCollection olarak.
public class FeedCategoryViewModel { public FeedCategoryViewModel() { PhotoList = new ObservableCollection(); LoadDummData(); } private void LoadDummData() { for (int i = 0; i < 10; i++) { PhotoList.Add(new Photos { PhotoUrl = "https://images.unsplash.com/photo-1539578741486-e0d3a45e16c2?ixlib=rb-0.3.5&s=05b39cf560d156b0e5f7408c0ca05ccb&auto=format&fit=crop&w=1900&q=80" }); PhotoList.Add(new Photos { PhotoUrl = "https://images.unsplash.com/photo-1539578903083-0d9bb0ed39d4?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=044382ada2f12060a3b4f28c120365a2&auto=format&fit=crop&w=634&q=80" }); PhotoList.Add(new Photos { PhotoUrl = "https://images.unsplash.com/photo-1539604880233-d282d9bac272?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=902aa0705d67ac390c0170c68aa4907f&auto=format&fit=crop&w=1051&q=80" }); PhotoList.Add(new Photos { PhotoUrl = "https://images.unsplash.com/photo-1539578839907-f463d05d7ad9?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=d7a43a0fdf7e74bb78e3413341a2598e&auto=format&fit=crop&w=634&q=80" }); PhotoList.Add(new Photos { PhotoUrl = "https://images.unsplash.com/photo-1539572408025-26bebcbb750e?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=b3f3a1e1037df2e7adcf65ba2816d3db&auto=format&fit=crop&w=1050&q=80" }); PhotoList.Add(new Photos { PhotoUrl = "https://images.unsplash.com/photo-1539609413529-1166774c3954?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=2c7b7da55abdaf0759143fbfc8e0b59b&auto=format&fit=crop&w=634&q=80" }); PhotoList.Add(new Photos { PhotoUrl = "https://images.unsplash.com/photo-1539593758884-2e9d7b18e451?ixlib=rb-0.3.5&s=6e3b06927d46acb4071f7de4c8ba1a4c&auto=format&fit=crop&w=634&q=80" }); PhotoList.Add(new Photos { PhotoUrl = "https://images.unsplash.com/photo-1539553281713-4f86b514e86f?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=33a4f691badb8c4b237727ef96c31414&auto=format&fit=crop&w=500&q=60" }); PhotoList.Add(new Photos { PhotoUrl = "https://images.unsplash.com/photo-1539535879069-3c148518f8c9?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=0e707059e2e9d16f94a682a189be8e42&auto=format&fit=crop&w=500&q=60" }); PhotoList.Add(new Photos { PhotoUrl = "https://images.unsplash.com/photo-1539553139747-e2ae5159d2e5?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=89e76f2a8cf3a81c8dba40f5dd01bb0f&auto=format&fit=crop&w=500&q=60" }); PhotoList.Add(new Photos { PhotoUrl = "https://images.unsplash.com/photo-1539576282236-40272d2dbe7e?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=9cee2739772eb498885974c2f2542a83&auto=format&fit=crop&w=500&q=60" }); PhotoList.Add(new Photos { PhotoUrl = "https://images.unsplash.com/photo-1539541364455-3a385c64f82d?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=503bce372f4b0071c629e5620a929bc1&auto=format&fit=crop&w=500&q=60" }); PhotoList.Add(new Photos { PhotoUrl = "https://images.unsplash.com/photo-1539601001507-887d25d5449e?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=4ee2cba1be085263937478f5f71803fc&auto=format&fit=crop&w=500&q=60" }); PhotoList.Add(new Photos { PhotoUrl = "https://images.unsplash.com/photo-1539609301259-1dd126206e5e?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=dcec9da5f439e073559530f679a1f23f&auto=format&fit=crop&w=500&q=60" }); PhotoList.Add(new Photos { PhotoUrl = "https://images.unsplash.com/photo-1539597583595-2069afd2b107?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=64b66dfa88a1b0f2ebffd20197857ed2&auto=format&fit=crop&w=500&q=60" }); PhotoList.Add(new Photos { PhotoUrl = "https://images.unsplash.com/photo-1539544048267-9744e3b84996?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=16959568ee70ac013296240aa5f7ced4&auto=format&fit=crop&w=500&q=60" }); PhotoList.Add(new Photos { PhotoUrl = "https://images.unsplash.com/photo-1539603584498-db314ea45182?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=a62e0d1be0a73a5030616caabd8b7cc7&auto=format&fit=crop&w=500&q=60" }); PhotoList.Add(new Photos { PhotoUrl = "https://images.unsplash.com/photo-1539598735229-e5918f5408b5?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=3a526577df19d1ec423b2220532475e7&auto=format&fit=crop&w=500&q=60" }); PhotoList.Add(new Photos { PhotoUrl = "https://images.unsplash.com/photo-1539553521736-053bd7e14cf5?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjEyMDd9&s=debb88414bb2ee774ce6229a72adac17&auto=format&fit=crop&w=500&q=60" }); } } public ObservableCollection PhotoList { get; set; } }
bu sayfanın da sonuna geldik, birçok şeyin kullanım şeklini görmşü olduk, diğer detaylarla Confinsta App ve xamarin forms yazılarına devam edeceğim.
Bir sonraki yazımda görüşmek üzere.