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