czwartek, 29 stycznia 2015

Kulisy Xamarina - odc. 2: nawigacja, selektory, protokoły

Wchodzimy na kolejny level Xamarina (nie ostatni) dotyczący: różnych rodzajów nawigacji, obsługi list i selektorów oraz protokołów w iOS, które są czymś więcej niż interfejsami w C#. Ta ostatnia rzecz jest przykładem, że o ile Xamarin stara się dość wiernie mapować natywne API, o tyle nie robi tego za wszelką cenę i czasami idzie na pewien kompromis proponując własne bardziej wygodne w C# odpowiedniki.

 

Nawigacja Swipe w Android

ViewPager + fragmenty z zawartością stron + adapter (bazujący na FragmentStatePagerAdapter)

Tworzymy:

  • plik .axml dla fragmentu
  • klasę C# dla fragmentu (szablon Fragment)

public class AlbumFragment: Fragment

{

        TextView textTitle;

        …

        public override void OnCreate(Bundle savedInstanceState)

        {

                  base.OnCreate(savedInstanceState);

                  …

        }

        public override View OnCreateView(LayoutInflater inflater, ViewGroup container,

        Bundle savedInstanceState)

        {

                  View rootView = inflater.Inflate(Resource.Layout.AlbumFragment, container, false);

 

                  textTitle = rootView.FindViewById<TextView>(Resource.Id.textTitle);

                  …

 

                  return rootView;

        }

}

Z Android.Support.V4.App potrzebujemy klas FragmentStatePagerAdapter i ViewPager, a także FragmentActivity i Fragment (nawet jeśli urządzenie wspiera natywnie fragmenty). W Xamarin nie dodajemy referencji do Mono.Android.Support.v4. Zamiast tego w folderze Components wybieramy podręczne polecenie Get More Components…, otwiera się nam strona Web ze sklepem Xamarin(darmowe lub płatne dodatki), z której możemy pobrać Android Support Library v4 (w referencjach pojawia się referencja do Xamarin.Android.Support.v4). W podobny sposób dodajemy też Android Support Library v7 AppCompat.

class AlbumPagerAdapter: FragmentStatePagerAdapter

{

         …

         public override Android.Support.V4.App.Fragment GetItem(int position)

         {

                  …

                  AlbumFragment albumFragment = new AlbumFragment();

                  …

         }

          public override int Count

          {

          }

}

W pliku z definicją fragmentu zmieniamy namespace z Android.App na Android.Support.V4.App.

Tworzymy nowy layout (szablon Android Layout):

<?xml version=”1.0” encoding=”utf-8”?>

<android.support.v4.view.ViewPager xmlns:android=”…”

   android.id = “@+id/albumPager”

   android:layout_width=”fill_parent”

   android:layout_height=”fill_parent”/>

a potem aktywność zastępującą poprzednią główną aktywność:

[Activity(Label = “Albums”, MainLauncher = true, Icon = “@drawable/icon”)]

public class AlbumActivity: FragmentActivity

{

           …

           AlbumPagerAdapter albumPagerAdapter;

           ViewPager viewPager;

           protected override void OnCreate(Bundle bundle)

           {

                     base.OnCreate(bundle);

                     SetContentView(Resource.Layout.CourseActivity);

                     …

                     albumPagerAdapter = new AlbumPagerAdapter(SupportFragmentManager, …);

                     viewPager = FindViewById<ViewPager>(Resource.Id.albumPager);

                     viewPager.Adapter = albumPagerAdapter;

           }

}

 

Nawigacja Swipe w iOS

UIPageViewController + instancje UIViewController do poszczególnych stron

Tworzymy AlbumPagerViewController (szablon UIViewController class):

   [Register("AlbumViewController")]
   public class AlbumViewController : UIViewController
   {

       UIPageViewController pageViewController;

       …


       public AlbumViewController()
       {
       }

       public override void DidReceiveMemoryWarning()
       {
           base.DidReceiveMemoryWarning();

       }

       public override void ViewDidLoad()
       {          

           base.ViewDidLoad();

           pageViewController = new UIPageViewController(

                UIPageViewControllerTransitionStyle.Scroll,

                UIPageViewControllerNavigationOrientation.Horizontal);

           pageViewController.View.Frame = View.Bounds;

           this.View.AddSubview(pageViewController.View);

           var firstAlbumViewController = CreateAlbumViewController();

           pageViewController.SetViewControllers(

                 new UIViewController[] { firstAlbumViewController },

                 UIPageViewControllerNavigationDirection.Forward, false, null);

            pageViewController.GetNextViewController = GetNextViewController;

            pageViewController.GetPreviousViewController = GetPreviousViewController;

        }

        AlbumViewController CreateAlbumViewController()

        {

                 var albumViewController = new AlbumViewController();

                 …

        }

        public UIViewController GetNextViewController(

                 UIPageViewController pageViewController,

                 UIViewController referenceViewController)

        {

                 …

        }

        public UIViewController GetPreviousViewController(

                 UIPageViewController pageViewController,

                 UIViewController referenceViewController)

        {

                 …

        }

       
   }

W AppDelegate.cs zmieniamy główny kontroler (lub robimy to w bardziej nowoczesny sposób).

Przygotowujemy kontroler, który ma prezentować zawartość stron.

Animacja przewijania strony zamiast swipe:

pageViewController = new UIPageViewController(

        UIPageViewControllerTransitionStyle.PageCurl,

        UIPageViewControllerNavigationOrientation.Horizontal,

        UIPageViewControllerSpineLocation.Min);

 

Używanie protokołów z iOS

Xamarin czasami ukrywa używanie protokołów

  • czasami oferuje alternatywy do jawnego używania protokołów
    • może dodawać składowe (propercje delegata z nieco innymi nazwami) do klasy (by ukryć korzystanie z protokołu)
    • przykład: UIPageViewController (ukrywa użycie protokołu i nadal wspiera jego używanie)

Protokoły Objective-C mają właściwości niedostępne w interfejsach C#

  • opcjonalne składowe

Wyzwanie

  • implementacja typów realizujących kontrakt z protokołu
  • nie możemy użyć tutaj interfejsów

Implementacja protokołów w Xamarin

  • abstrakcyjne klasy
    • wymagane składowe są oznaczane jako abstract
    • opcjonalne składowe są oznaczane jako virtual

Implementujemy klasę przez dziedziczenie i dołączamy jako referencję (klasa używająca protokołu ma już klasę bazową)

class AlbumPagerViewControllerDataSource: UIPageViewControllerDataSource

{

         …

         public override UIViewController GetNextViewController(UIPageViewController pageViewController)

         {

         }

         public override UIViewController GetPreviousViewController(UIPageViewController pageViewController)

         {

         }

}

   [Register("AlbumViewController")]
   public class AlbumViewController : UIViewController
   {

       UIPageViewController pageViewController;

       …  

       public override void ViewDidLoad()
       {   

               …

               var dataSource = new AlbumPagerViewControllerDataSource(…);

               pageViewController.DataSource = dataSource;

               …                     

       }        
   }

 

Nawigacja master-detail w Android

Każda aktywność wymaga atrybutu [Activity(Label = “opis”)]

Lista:

[Activity(Label = “AlbumCategories”, MainLauncher = true, Icon = “@drawable/icon”)]

public class CategoryActivity: ListActivity

{

          protected override void OnCreate(Bundle bundle)

          {

                   base.OnCreate(bundle);

                   var categories = { “Rock”, “Classic”, “Jazz” };

                   ListAdapter = new ArrayAdapter<String>(this,  Android.Resource.Layout.ListItem, categories);

          }

}

Niestandardowy adapter:

class AlbumCategoryAdapter: BaseAdapter<AlbumCategory>

{

           Context context;

           int layoutResourceId;

           …

           public AlbumCategoryAdapter(Context context, int layoutResourceId, …)

           {

                   this.context = context;

                   this.layoutResourceId = layoutResourceId;

                   …

           }

           public override AlbumCategory this[int position]

           {

                    get { … }

           }

           public override int Count

           {

                    get { … }

           }

           public override long GetItemId(int position)

           {

                    return position;

           }

           public override View GetView(int position, View convertView, ViewGroup parent)

           {

                    View view = convertView;

                    if (view == null)

                    {

                            var inflater = context.GetSystemService(Context.LayoutInflaterService) as LayoutInflater;

                            view = inflater.Inflate(layoutResourceId, null);

                    }

                    var textView = view.FindViewById<TextView>(Android.Resource.Id.Text1);

                    textView.Text = this[position].Title;

                    return view;

           }

}

 

[Activity(Label = “AlbumCategories”, MainLauncher = true, Icon = “@drawable/icon”)]

public class CategoryActivity: ListActivity

{

          …

          protected override void OnCreate(Bundle bundle)

          {

                   base.OnCreate(bundle);

                   …

                   ListAdapter =

                   new AlbumCategoryAdapter(this, Android.Resource.Layout.ListItem, …);

          }

          protected override void OnListItemClick(ListView l, View v, int position, long id)

          {

                   var intent = new Intent(this, typeof(AlbumActivity));

                   …

                   intent.PutExtra(“extra name”, categoryName);

                   StartActivity(intent);

          }

}

 

[Activity(Label = “Albums”)]

public class AlbumActivity: FragmentActivity

{

           …

           protected override void OnCreate(Bundle bundle)

           {

                     …

                     var startupIntent = this.Intent;

                     if (startupIntent != null)

                     {

                              string categoryName = startupIntent.GetStringExtra(“extra name”);

                              …

                     }

                     …

            }

}

 

Android Navigation Drawer

<?xml version=”1.0” encoding=”utf-8” ?>

<android.support.v4.widget.DrawerLayout  xmlns:android=”…”

   android.id=”@+id/drawerLayout”

   android:layout_width=”match_parent”

   android:layout_height=“match_parent”>

   <android.support.v4.view.ViewPager

           android.id=”@+id/albumPager”

           android:layout_width=”fill_parent”

           android:layout_height=”fill_parent” />

    <ListView

           android:id=”@+id/categoryDrawerListView”

           android:layout_height=”match_parent”

           android:layout_width=”240p”

           android:layout_gravity=”start”

           android:choiceMode=”singleChoice”

           android:divider=”@android:color/transparent”

           android:dividerHeight=”0dp”

           android:background=”#888” />

</android.support.v4.widget.DrawerLayout>

 

[Activity(Label = “Albums”, MainLauncher = true, Icon = “@drawable/icon”)]

public class AlbumActivity: FragmentActivity

{

           …

           DrawerLayout drawerLayout;

           ListView categoryDrawerListView;

 

           protected override void OnCreate(Bundle bundle)

           {

                     …

                     drawerLayout = FindViewById<DrawerLayout>(Resource.Id.drawerLayout);

                     categoryDrawerListView = FindViewById<ListView>(Resource.Id.categoryDrawerListView);

                     categoryDrawerListView.Adapter =

                     new AlbumCategoryAdapter(this, Android.Resource.Layout.ListItem, …);

                     categoryDrawerListView.SetItemChecked(0, true);

                     categoryDrawerListView.ItemClick += categoryDrawerListView_ItemClick;

            }

            void categoryDrawerListView_ItemClick(object sender, Adapter.ItemClickEventArgs e)

            {

                     drawerLayout.CloseDrawer(categoryDrawerListView);

                     //e.Position  - przeładowanie danych

                     …

                     viewPager.CurrentItem = 0;

            }

}

 

Właściwości elementów w designerze:

@android:id/text1

?android:attr/activatedBackgroundIndicator

 

class AlbumPagerAdapter: FragmentStatePagerAdapter

{

         …

         public override int GetItemPosition(Java.Lang.Object @object)

         {

                 return PositionNone;   //wymuszenie ponownego tworzenia elementów

         }

         //odświeżenie danych: NotifyDataSetChanged();

}

 

Nawigacja master-detail w iOS

[Register("CategoryViewController")] 
public class CategoryViewController : UITableViewController 
{

        …

        public CategoryViewController()

        {

        }

        public override void DidReceiveMemoryWarning()
        {
                base.DidReceiveMemoryWarning();

        }

        public override void ViewDidLoad()
        {  

                 base.ViewDidLoad(); 

                 this.Title = “Kategorie”;

                 …

                 UITableView tableView = this.View as UITableView; 

                 tableViewSource.Source = new CategoryViewSource(…);  

        }

}

 

class CategoryViewSource: UITableViewSource

// zamiast: UITableViewDataSource i UITableViewDelegate

{

         const string cellId = “Cell”;

         …

         public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)

         {

                  UITableViewCell cell = tableView.DequeueReusableCell(cellId);

                  if (cell == null)

                         cell = new UITableViewCell(UITableViewCellStyle.Default, cellId);

                  //indexPath.Row

                  cell.TextLabel.Text = …

                  return cell;

         }

         public override int RowsInSection(UITableView tableView, int section)

         {

                  …

         }

         //przy nawigacji za pomocą kodu

         public override void RowSelected(UITableView tableView, NSIndexPath indexPath)

         {

                  var albumPagerViewController = new AlbumPagerViewController(…);  //indexPath.Row

                  var appDelegate = UIApplication.SharedApplication.Delegate as AppDelegate;

                  appDelegate.RootNavigationController.PushViewController(albumPagerViewController, true);

         }

}

 

//przy nawigacji za pomocą kodu

public partial class AppDelegate: UIApplicationDelegate

{

          …

          public UINavigationController RootNavigationController

          {

                   get;

                   private set;

          }

          public override bool FinishedLaunching(UIApplication app, NSDictionary options)

          {

                   …

                   RootNavigationController = new UINavigationController();

                   var viewController = new CategoryViewController();

                   RootNavigationController.PushViewController(viewController, false);

                   window.RootViewController = viewController;

                   …

                   return true;

          }

}

 

Uzupełnienia

Android

Combo - podłączenie

  • kontrolka Spinner

spinner.Adapter = new ArrayAdapter<string>(this, Android.Resource.Layout.ListItem, array);

spinner.ItemSelected += (object sender, AdapterView.ItemSelectedEventArgs e) =>

{

        //e.Position - wybrany indeks

};

Notyfikacja

Toast.MakeText(this, “XXX”, ToastLength.Short).Show();

 

iOS

UIPickerView - podłączenie

public abstract class GenericPickerModel<T>:  UIPickerViewModel

{

         public T SelectedItem { get; private set; }

         public event EventHandler ItemSelected;

 

         IList<T> _items;

         public IList<T> Items

         {

                  get { return _items; }

                  set { _items = value;  Selected(null, 0, 0); }

         }

         public GenericPickerModel()

         {

         }

         public GenericPickerModel(IList<T> items)

         {

                 Items = items;

         }

         public override int GetRowsInComponent(UIPickerView picker, int component)

         {

                  if (NoItem())

                         return 1;

                  return Items.Count;

         }

         public override string GetTitle(UIPickerView picker,  int row,  int component)

         {

                  if (NoItem(row))

                        return “”;

                  var item = Items[row];

                  return GetTitleForItem(item);

         }

         public override void Selected(UIPickerView picker, int row, int component)

         {

                  if (NoItem(row))

                         SelectedItem = default(T)

                  else

                         SelectedItem = Items[row];

                  if (ItemSelected != null)

                         ItemSelected(this, null);

         }

         public override int GetComponentCount(UIPickerView picker)

         {

                  return 1;

         }

         public virtual string GetTitleForItem(T item)

         {

                  return item.ToString();

         }

         bool NoItem(int row = 0)

         {

                  return Items == null || row >= Items.Count;

         }

}

 

public class DocumentPickerModel: GenericPickerModel<Document>

{

         public DocumentPickerModel(IList<Document> documents): base(documents) 

         {

         }

}

 

//w kontrolerze odnoszącym się do widoku zawierającym picker

var model = new DocumentPickerModel (documents);

documentPicker.Model = model;

//odczyt

var documentModel = documentPicker.Model as DocumentPickerModel;

//documentModel.SelectedItem

//zdarzenie wyboru

model.ItemSelected += (object sender, EventArgs e) => {

}

Programowe ukrywanie klawiatury

var tap = new UITapGestureRecognizer();

tap.AddTarget(() => {

        View.EndEditing(true);

});

View.AddGestureRecognizer(tap);

RestSharp - prosty klient Http w Xamarin Components

Brak komentarzy: