wtorek, 23 sierpnia 2016

Android 7.0 - migracja projektów w Android Studio i Visual Studio

Dokonajmy krótkiego podsumowania dotychczasowych faktów odnośnie Android 7.0 z punktu widzenia dewelopera.

Screenshot_20160823-005513

Po pierwsze wczoraj została wypuszczona finalna wersja. Nie każdy ją jednak od razu otrzymał. Aby pomóc nieco losowi można było zapisać swoje urządzenie do programu beta (wczoraj przyniosło mi to natychmiastowe pojawienie się pożądanego updatu).

Co nowego dla dev-a?  Najlepiej przeczytać stronkę Android 7.0 for Developers.

Android Studio

Najpierw wczytałem swój projekt z DSP do najnowszego stabilnego Android Studio 2.1.3.  Nie było tak źle, skorzystałem z automatycznego upgrade’u gradle. Zainstalowałem API 24, odpowiednie build tools itp. w SDK managerze:

image

Wcześniej już miałem Javę 8 na maszynie. W zasadzie tylko zmieniłem wersje do kompilacji w pliku build.gradle:

apply plugin: 'com.android.application'

android {
compileSdkVersion 24
buildToolsVersion "24.0.1"

defaultConfig {
applicationId "com.apps.kruszyn.lightorganapp"
minSdkVersion 19
targetSdkVersion 24
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:24.+'
compile 'com.android.support:design:24.+'
compile 'com.android.support:cardview-v7:24.+'
}

W manifeście też dałem targetSdkVersion na 24, choć z podpowiedzi narzędzia wynika nawet że i tak gradle to nadpisuje podczas kompilacji na podstawie swoich ustawień.

Xamarin & Visual Studio

Po pierwsze postąpiłem według Introduction to the Android N Developer Preview. Instaluje się specjalną wersję Xamarina dla VS (nie instalowałem nawet Xamarin Studio czy gtk sharp). W VS w konfiguracji Xamarin ustawiłem ścieżkę do JDK 1.8. W projekcie pozmieniałem ustawienia co do wersji API:

image

Nie ustawiałem wersji minimalnej na 24, a i tak zdaje się wszystko działa. Pakiety nuget jednak automatycznie się nie zaktualizowały, więc je usunąłem i dodałem na nowo. O ile ich wersje nadal pozostają takie same, o tyle teraz są dedykowane dla frameworka 7.0 (przy API 23 dotychczas był to 6.0). 
Deplyment. Tu miałem problem… Dostawałem problem z niemożliwością usunięcia app-ki, mimo że ją wcześniej odinstalowałem na telefonie. Taka przypadłość – jak wyczytałem m.in na stackoverflow – przydarza się czasami w Xamarin. Usuwamy wtedy niby usuniętą już app-kę poprzez linię komend adb:
adb uninstall com.apps.kruszyn.lightorganapp.droid

Czyści to scachowane biblioteki Xamarina. Pomogło. Wgrałem, działa podobnie jak oryginał w Android Studio.

Wsparcia Xamarina wygląda chyba jednak nadal jako preview, designer przestał mi póki co działać:

image

W Android Studio designer działa bez problemu. Niewykluczone, że u mnie w Xamarinie coś przestało działać na skutek instalowania różnych jego wydań (stabilne, beta, Android N Preview, …). Póki co w zasadzie cisza o Android 7 na jego forum, a pewnie w jakiejś stabilnej wersji pojawi się oficjalne wsparcie dla 7.0.

To tyle póki co w temacie siódemki. Aha, jak mamy app-kę targetowaną na API starsze niż 24 (np. 23) to przy dokowaniu app-ki na części ekranu pojawia się info, że aplikacja ta może nie wspierać takiego trybu pracy. Po ztargetowaniu na 24 taka notyfikacja znika. Przedstawione tutaj zmiany trafiły tradycyjnie już jako wbitki do mojego githuba.

niedziela, 21 sierpnia 2016

Xamarin.iOS kontra Swift odc.4 (ustawienia aplikacji, sockety)

Xamarin kontra Swift, witam serdecznie. Dziś upamiętnimy odcinki o iOS z czasów DSP nr. 7 i 8. To sprawadza w zasadzie aplikację w C# do poziomu funkcjonalności w Swift, jaką osiągnąłem na koniec maja (i DSP).

IMG_0049  

Zacznijmy od ustawień. Taki efekt jak na powyższym screenie osiągniemy jak wgramy aplikację skompilowaną jako Release, w Debug Xamarin dodatkowo dodaje swoją sekcję w ustawieniach. W Visual Studio nie ma takiego kreatora do ustawień jak w Xcode, pliki trzeba tworzyć ręcznie w folderze Settings.bundle (ja je skopiowałem z projektu Xcode):

image

W AppDelegate stworzyłem odpowiedniki metod do wczytywania domyślnych wartości:

        private void PopulateRegistrationDomain()
        {
            var appDefaults = LoadDefaultsFromSettingsPage();

            var defaults = NSUserDefaults.StandardUserDefaults;
            defaults.RegisterDefaults(appDefaults);
            defaults.Synchronize();
        }

        private NSDictionary LoadDefaultsFromSettingsPage()
        {
            var settingsDict = new NSDictionary(NSBundle.MainBundle.PathForResource("Settings.bundle/Root.plist", null));

            var prefSpecifierArray = settingsDict["PreferenceSpecifiers"] as NSArray;

            if (prefSpecifierArray == null)
                return null;

            var keyValuePairs = new NSMutableDictionary();

            foreach (var prefItem in NSArray.FromArray<NSDictionary>(prefSpecifierArray))
            {
                var prefItemType = prefItem["Type"] as NSString;
                var prefItemKey = prefItem["Key"] as NSString;
                var prefItemDefaultValue = prefItem["DefaultValue"] as NSString;

                if (prefItemType.ToString() == "PSChildPaneSpecifier")
                {
                   
                }
                else if (prefItemKey != null && prefItemDefaultValue != null)
                {
                    keyValuePairs[prefItemKey] = prefItemDefaultValue;
                }
            }

            return keyValuePairs;
        }

Jest dużo podobieństwa, choć są też pewne różnice np. w metodzie ładującej zasoby.

Największą niespodzianką był jednak fakt, że metoda ViewWillAppear w ViewController nie jest w ogóle wywoływana! (przynajmniej na fizycznym urządzeniu). Patrzyłem w necie i jest całkiem sporo tego typu zgłoszeń, w tym na forum Xamarin. Nasłuchiwanie zmian w konfiguracji umieściłem więc w metodzie ViewDidLoad, podobnie jak w przykładzie od Xamarin, a także innych notyfikacji w mojej aplikacji.

NSObject notificationToken3;

        public override void ViewDidLoad()
        {

              …
              notificationToken3 = notificationCenter.AddObserver(NSUserDefaults.DidChangeNotification, DefaultsChanged);

              …
        }  

        public override void DidReceiveMemoryWarning()
        {
               …

               notificationToken3.Dispose();
        }

        private async void DefaultsChanged(NSNotification notification)
        {
            try
            {
                var defaults = NSUserDefaults.StandardUserDefaults;
                var useRemoteDevice = defaults.BoolForKey("use_remote_device_preference");

                if (remoteController != null)
                    await ReleaseRemoteController();

                if (useRemoteDevice)
                {
                    await CreateNewRemoteController(defaults);                  
                }

             }
            catch (Exception)
            {
               
            }
        }

Kod w ostatniej zaprezentowanej tu metodzie skłania do przejścia do drugiego zagadnienia tego odcinka, a mianowicie socketów. W kontrolerze ViewController wykorzystałem klasę LightsRemoteController z projektu LightOrganApp.Shared używaną już wcześniej przez aplikację w Xamarin.Android. To właśnie jest przykład na siłę i moc Xamarina, jaką jest współdzielenie kodu między platformami. Komunikacja z Raspberry Pi działa bez problemu:

IMG_20160821_120331

I tym sprzętowym akcentem zakończymy ten odcinek Winking smile

piątek, 19 sierpnia 2016

Xamarin.iOS kontra Swift odc.3 (własna kontrolka, designer)

Dobry wieczór, Xamarin.iOS kontra Swift, witam serdecznie. Dziś na warsztat weźmiemy pisanie własnej kontrolki na cześć posta z czasów DSP poświęconemu tej tematyce.

IMG_0048

Jak wygląda w C# kod CircleView? Ano tak:

   [Register("CircleView"), DesignTimeVisible(true)]
   public class CircleView: UIView
   {
       private UIColor circleColor;

       [Export("CircleColor"), Browsable(true)]
       public UIColor CircleColor
       {
           get { return circleColor; }
           set
           {
               circleColor = value;
               SetNeedsDisplay();
           }
       }       

       public CircleView(IntPtr p): base(p)
       {
           Initialize();
       }

       public CircleView()
       {
           Initialize();
       }

       private void Initialize()
       {
           circleColor = UIColor.Red;
           ContentMode = UIViewContentMode.Redraw;

           SetNeedsDisplay();
       }

       public override void Draw(CGRect rect)
       {
           base.Draw(rect);

           DrawCircle(CircleColor);
       }

       private void DrawCircle(UIColor color)
       {
           using (var context = UIGraphics.GetCurrentContext())
           {
               var a = Math.Min(Bounds.Size.Width, Bounds.Size.Height);
               var leftX = Bounds.GetMidX() - a / 2;
               var topY = Bounds.GetMidY() - a / 2;
               var rectangle = new CGRect(leftX, topY, a, a);

               context.SetFillColor(CircleColor.CGColor);
               context.FillEllipseInRect(rectangle);
           }
       }
   }

Jakie odczucia w stosunku do Swift?  Inne atrybuty, inny konstruktor (z IntPtr) dla designera (pachnie trochę Visual Studio i Windows a nie Xcode i OS X). Pojawia się metoda SetNeedsDisplay, której w oryginale nie musiałem używać. Metoda Draw ma nieco inną nazwę. Z kolei operacje takie jak np. GetMidX czy FillEllipseInRect prezentują się znacznie zgrabniej w C#. Co z designerem?  Też działa, to znaczy prawie. W prostszym projekcie “na boku” działa wszystko jak należy:

image

Jednak w projekcie aplikacji widnieją paski zamiast kółek Sad smile I tak dziś jest lepiej, bo wczoraj w nocy nic się w designerze nie pokazywało. Dodam, że nie zmieniałem nic w kodzie…

image

W Xcode taki sam storyboard wyświetlany jest poprawnie z kółkami. Wygląda na to, że jakiś bardziej złożony scenariusz nie został do końca obsłużony w designerze Visual Studio… Może to kwestia auto layoutu i dużej liczby constraint-ów?

Na koniec ciekawostka. Otóż uświadomiłem sobie dopiero przy pisaniu w C#, że UIColor ma gotową metodę do uzyskania koloru z zadaną wartością kanału alfa na podstawie podanego koloru. Metoda SetLight może być taka prosta:

       private void SetLight(CircleView light, float ratio)
       {
              light.CircleColor = light.CircleColor.ColorWithAlpha(ratio);
       }

Yupi! Wszystko co dziś pokazywałem jest oczywiście na githubie.  Dobranoc Państwu Winking smile

środa, 17 sierpnia 2016

Xamarin.iOS kontra Swift odc.2 (UISearchController, UISearchBar, stan aplikacji, stylowanie)

Dobry wieczór, Xamarin.iOS kontra Swift, zaczynamy –Winking smileDziś dzień pamięci [DSP2016] iOS prosto z poligonu odc.3, tak więc będzie o wyszukiwarce, a właściwie o zielonej wyszukiwarce takiej jak ta:

IMG_0046

Wybierzmy co bardziej interesujące kawałki kodu z ostatnich wbitek na github.

Zacznijmy od delegatów. Temat był już poruszany w poprzednim odcinku. O ile ostatnio wdrożyłem podejście silnie typowane, ale okupione tworzeniem dodatkowej klasy TableSource, o tyle tym razem wybrałem drogę na skróty bez silnego typowania, ale bardziej w stosunku 1:1 do oryginału w Swift.

      [Export("searchBarSearchButtonClicked:")]
      public virtual void SearchButtonClicked(UISearchBar searchBar)
      {
          searchBar.ResignFirstResponder();
      }

      private void ConfigureSearchController()
      {
          searchController = new CustomSearchController((UIViewController)null)
          {
              WeakSearchResultsUpdater = this
          };
          searchController.DimsBackgroundDuringPresentation = false;
          searchController.SearchBar.SizeToFit();
          UISearchBar.Appearance.TintColor = UIColor.FromRGB(197, 225, 165);
          searchController.SearchBar.Placeholder = NSBundle.MainBundle.LocalizedString("searchMusic", "Search Music");
         searchController.SearchBar.WeakDelegate = this;
          DefinesPresentationContext = true;
          NavigationItem.TitleView = searchController.SearchBar;
          searchController.HidesNavigationBarDuringPresentation = false;
      }

       [Export("updateSearchResultsForSearchController:")]
       public virtual void UpdateSearchResultsForSearchController(UISearchController searchController)
       {
           FilterContentForSearchText(searchController.SearchBar.Text);
       }

I tak  cała klasa  FileListViewController implementuje protokół UISearchResultsUpdating poprzez oznaczoną atrybutem metodę UpdateSearchResultsForSearchController. W obiekcie klasy CustomSearchController dziedziczącej po UISearchController ustawiamy ogólnie typowaną właściwość WeakSearchResultsUpdater na this. Analogicznie postąpiłem dla UISearchBarDelegate tworząc metodę SearchButtonClicked i ustawiając searchController.SearchBar.WeakDelegate na this.

A jak pokolorować kursor w UISearchBar?  Załatwia to linijka: 

UISearchBar.Appearance.TintColor = UIColor.FromRGB(197, 225, 165);

Wspomnijmy jeszcze o odtwarzaniu stanu. Generalnie jest podobnie jak w Swift. Na poziomie kontrolera są pewne różnice w metodach kodujących i dekodujących parametry. W C# przedstawia się to w następujący sposób:

        public override void EncodeRestorableState(NSCoder coder)
        {
            base.EncodeRestorableState(coder);

            coder.Encode(searchController.Active, RestorationKeys.SearchControllerIsActive.ToString());
            coder.Encode(searchController.SearchBar.IsFirstResponder, RestorationKeys.SearchBarIsFirstResponder.ToString());
            coder.Encode(new NSString(searchController.SearchBar.Text), RestorationKeys.SearchBarText.ToString());
        }

        public override void DecodeRestorableState(NSCoder coder)
        {
            base.DecodeRestorableState(coder);

            restoredState.WasActive = coder.DecodeBool(RestorationKeys.SearchControllerIsActive.ToString());
            restoredState.WasFirstResponder = coder.DecodeBool(RestorationKeys.SearchBarIsFirstResponder.ToString());
            searchController.SearchBar.Text = (NSString)coder.DecodeObject(RestorationKeys.SearchBarText.ToString());
        }

W przypadku kodowania mamy metody Encode z przeładowaniem zamiast np. EncodeBool. Nie ma wariantu dla string, dlatego tworzę odpowiedni obiekt NSString. Przy dekodowaniu jest bardziej podobnie jak w oryginale, mamy osobne metody dla różnych typów, ale o krótszych nazwach (np. zamiast decodeBoolForKey jest po prostu DecodeBool).

Na dziś się żegnamy, dobranoc Państwu Winking smile

środa, 10 sierpnia 2016

Xamarin.iOS kontra Swift odc.1 (narzędzia, składnia, nawigacja, toolbar, Music Library, async, SystemMusicPlayer, lokalizacja, ikony, UITableViewController, stylowanie)

Witam wszystkich serdecznie. Kolekcjonerskich edycji aplikacji Kolorofon z czasów DSP ciąg dalszy. Tym razem powstaje jeszcze bardziej kolekcjonerski klon niż poprzednio, w Xamarinie na system z jabłkiem, zaczynamy!

Obecnie mamy na github stan odpowiadający odcinkom 1, 2 oraz  5, 6 originału pisanego w Xcode i Swift. Poniżej tradycyjnie już screenshoty z działającej nieukończonej jeszcze app-ki (można przeglądać i wybierać pliki do odtwarzania i je odtwarzać), tym razem prosto z iphona:

IMG_0044  IMG_0045

Teraz wypada podzielić się wrażeniami.

 

#1 Narzędzia

Zainstalowałem na Mac mini Xamarina, dołączyłem się do niego z poziomu Visual Studio zgodnie z instrukcją. Co do certyfikatów, to póki co skorzystałem z generowanych za darmo przez Xcode 7 (jak wygeneruje w nim provisioning profile dla jakiejś nazwy, to potem mogę go wybrać w Visual Studio w ustawieniach projektu iOS Bundle Signing, oczywiście wcześniej wybierając tą samą osobę co na Xcode, nazwa identyfikatora w iOS Aplication też powinna się zgadzać). Podłączam telefon do Mac mini i hula deployment z Visual Studio.

Designer. Zdalne wykonywanie powoduje, że przy otwieraniu storyboard mamy przez chwilę pasek postępu, zanim wyświetli się nam zawartość. Designer obsługuje tworzenie przejść między kontrolerami, warunki layoutu czy klasy size, nie wspominam o ustawianiu właściwości obiektu, bo to oczywiste. Do działania nie musimy mieć uruchomionego na Mac-u Xcode, OS X może w ogóle wygasić ekran podczas pracy w Visual Studio i to w sumie chodzi.

vs_storyboard

Jak łączymy XML storyboard/xib z C#?  Nie jest tak jak w Xcode, przypomina to raczej pracę ze starszymi Windows Forms niż nawet XAML… O ile w XAML część partial okna/strony/kontrolki wynikająca z samego XAML nie jest widoczna na pierwszy rzut oka dla dewelopera, o tyle tutaj partial klasę jawnie generowaną i modyfikowaną przez designer mamy w projekcie, tak jak 11 lat temu po premierze VS 2005 i C# 2.0. Ktoś z czytających pamięta?

O designerze można poczytać sobie tutaj.

Odsłońmy kulisy kodu designera. Jak podamy nazwę klasy, a jej nie ma to zostanie wygenerowana z częścia dla nas i dla designera. Jak uzupełnimy pole Name na widgecie, to powstanie nam pole outleta w części designera np:

[Outlet]
[GeneratedCode ("iOS Designer", "1.0")]
UIKit.UIToolbar toolbar { get; set; }

plus kod w metodzie ReleaseDesignerOutlets. Do tego pola można się odwołać w pisanej przez nas części kontrolera. A jak jest ze zdarzeniami?  Można dwa razy kliknąć na przycisk lub skorzystać z zakładki Events na obiekcie. Wygenerowane zostanie metoda partial w części designera:

[Action ("playPausePressed:")]
[GeneratedCode ("iOS Designer", "1.0")]
partial void playPausePressed (UIKit.UIBarButtonItem sender);

oraz w części dewelopera:

partial void playPausePressed(UIKit.UIBarButtonItem sender)
{

}

Udało mi się reużytkować plik .storyboard z oryginalnego projektu w Xcode. Co ciekawe nie musiałem każdego pola czy metody w XML generować w C# osobno. Skasowałem literę w Name jednego z pożądanych w code-behind obiektów, zapisałem, potem dodałem, znów zapisałem. Całość się przegenerowała, dostałem wszystkie potrzebne definicje refencji na pola i oczekiwane metody w C# danego kontrolera.

 

2# Składnia

W Xamarin protokoły Objective-C/Swift są reprezentowane w postaci klas abstrakcyjnych. Dlaczego nie interfejsy? Protokoły mogą mieć opcjonalne elementy, które w klasach abstrakcyjnych są odwzorowane w postaci metod wirtualnych. Przejdźmy do delegatów. W C# możemy odnaleźć odpowiednie zdarzenia, a jeśli są metody z wynikiem to albo implementujemy klasy wewnętrzne dziedziczące po klasach abstrakcyjnych protokołów albo stosujemy podejście mniej type-safe, ale bez tworzenia dodatkowej klasy z opatrzeniem implementowanych metod odpowiednim atrybutem. Można o tym poczytać tutaj.

W moim przypadku źródło danych dla FileListViewController dziedziczącego po UITableViewController wygląda tak:

public partial class FileListViewController : UITableViewController
    {
        static NSString cellId = new NSString("reuseIdentifier");

        List<MPMediaItem> filteredMediaItems;
        …

        public FileListViewController (IntPtr handle) : base (handle)
        {
        }

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

            TableView.Source = new TableSource(this);

            TableView.TableFooterView = new UIView();
            TableView.BackgroundView = new UIView();

            …
        }

        …

        class TableSource : UITableViewSource
        {
            FileListViewController controller;

            public TableSource(FileListViewController controller)
            {
                this.controller = controller;
            }

            public override nint RowsInSection(UITableView tableView, nint section)
            {
                var mediaItems = controller.GetMediaItems();

                if (mediaItems != null)
                    return mediaItems.Count;
                else
                    return 0;               
            }

            public override void WillDisplay(UITableView tableView, UITableViewCell cell, NSIndexPath indexPath)
            {
                cell.TintColor = UIColor.White;

                if (cell.TextLabel != null)
                    cell.TextLabel.TextColor = UIColor.White;

                if (cell.DetailTextLabel != null)
                    cell.DetailTextLabel.TextColor = UIColor.FromRGB(197, 225, 165);
            }

            public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
            {
                var cell = tableView.DequeueReusableCell(cellId, indexPath);
                var row = indexPath.Row;
                var mediaItems = controller.GetMediaItems();

                var item = mediaItems[row];
                if (cell.TextLabel != null)
                    cell.TextLabel.Text = item.Title;

                var artist = NSBundle.MainBundle.LocalizedString("unknownArtist", "Unknown Artist");
                var artistVal = item.Artist;
                if (artistVal != null)
                    artist = artistVal;

                var length = (int)item.PlaybackDuration;

                if (cell.DetailTextLabel != null)
                    cell.DetailTextLabel.Text = $"{artist} {GetDisplayTime(length)}";

                if (controller.selectedMediaItems != null && controller.selectedMediaItems.Contains(item))
                    cell.Accessory = UITableViewCellAccessory.Checkmark;
                else
                    cell.Accessory = UITableViewCellAccessory.None;

                cell.Tag = row;

                return cell;
            }

            …

            public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
            {
                var row = indexPath.Row;
                var mediaItems = controller.GetMediaItems();
                var item = mediaItems[row];

                if (controller.selectedMediaItems == null || !controller.selectedMediaItems.Contains(item))               
                    controller.selectedMediaItems?.Add(item);               
                else               
                    controller.selectedMediaItems.Remove(item);             

                tableView.ReloadData();
            }
        }
    }

Przechodząc ze Swift na C# brakuje mi w tym ostatnim skrótowego zapisywania enum-ów (znów piszę UITableViewCellAccessory.Checkmark zamiast .Checkmark) oraz rozbudowanej składni w if-ach czy guardów (znów muszę zapisywać najpierw coś do zmiennej, by potem korzystać z niej w warunku i dalszym kodzie zamiast zdefiniować ją wewnątrz if-a).

Na koniec punktu #2 dodatkowa ogólna uwaga odnośnie składni przy migracji Objective-C/Swift na C#.  Więcej mamy drobnych różnic niż podczas migracji z Javy na Android. Wynika to pewnie z większych różnic pomiędzy Objective-C/Swift a C# niż pomiędzy C# a Javą. Twórcy jednak zrobili wydaje się co mogli, by otrzymać jak najczystszą i prostą formę, czasami decydując się na pewne kompromisy. Warto przeczytać o API Design.

Plusem pisania w C# jest async. Dziś odciążyłem wątek UI podobnie jak kiedyś w Swift od ładowania utworów z Media Library:

        private async void LoadMediaItemsForMediaTypeAsync(MPMediaType mediaType)
        {
            await Task.Run(() =>
            {
                var query = new MPMediaQuery();
                var mediaTypeNumber = NSNumber.FromInt32((int)mediaType);
                var predicate = MPMediaPropertyPredicate.PredicateWithValue(mediaTypeNumber, MPMediaItem.MediaTypeProperty);

                query.AddFilterPredicate(predicate);

                allMediaItems = query.Items.ToList();
            });

            TableView.ReloadData();                      
        }

Wygląda to nieco zgrabniej niż kolejki iOS nawet i w Swift, choć do ideału brakuje asynchronicznych metod w samym systemowym API, a co jest na porządku dziennym w Windows Runtime API. O wątkach można poczytać tutaj.

W miarę rozwoju Xamarin też ewoluuje w mapowaniu pewnych rzeczy. Weźmy np. obsługę notyfikacji. W Swift piszemy wywołania AddObserver i RemoveObserver. W C# kiedyś też, ale od jakiejś wersji można nieco krócej:

NSObject notificationToken1;

notificationToken1 = NSNotificationCenter.DefaultCenter.AddObserver(MPMusicPlayerController.NowPlayingItemDidChangeNotification, NowPlayingItemChanged, player);

notificationToken1.Dispose();

Wołamy Dispose na tokenie zamiast długiego w zapisie wywołania RemoveObserver.

 

#3 Lokalizacja

Najlepiej przeczytać sobie jej opis i posiłkować się samplem. Xamarin trochę porządkuje pewną anarchię projektu Xcode, który trzyma pewne zasoby poza folderem całego projektu. W Xamarin możemy umieścić je w folderze Resources, a potem trafią w odpowiednie dla nich miejsce.

image

W dodatku zasoby trzech rodzajów: napisy w kodzie (Localizable.strings), napisy pakietu całej aplikacji np. jej nazwa (InfoPlist.strings) oraz napisy danego storyboard/xib (u mnie jest to Main.strings do Main.storyboard) są umieszczane w Xamarinie w tym samym folderze danego języka. Takiego porządku nie ma w projekcie Xcode. Co do samych plików to wziąłem je wprost z projektu Xcode, zmian co do ich formatu nie ma. W kodzie odczytujemy napis w następujący sposób:

var artist = NSBundle.MainBundle.LocalizedString("unknownArtist", "Unknown Artist");

Nieco inaczej, ale nazwa metody i jej parametry takie same jak w oryginale.

 

#4 Ikony

O ikonach dobrze rzucić sobie okiem tu oraz tu. Mamy ładne wsparcie dla asset-ów w Visual Studio:

image

Opis ikon nieco inny, ale dokładnie odpowiadają temu, co jest w Xcode.

 

#5 Stylowanie

Teraz sobie styluję w tonacji zielonej, potrzebowałem więc kilka rzeczy ostylować po raz pierwszy zamiast zadowolić się ich domyślną kolorystyką.

Tło paska nazwigacji i toolbara:  Bar Tint

Kolor ikony/napisu przycisku: Tint

Kolor Accessory w komórce UITableView:  Tint (u mnie checkmark)

Nieprzezroczysty toolbar: domyślnie jest przezroczysty, programowo trzeba ustawić:   toolbar.Translucent = false; 

W ostatnim przypadku designer we właściwościach nie zawsze podaje prawdę, bo przezroczystość była  na dzień dobry odznaczona, a domyślnie jak wiemy jest. Podobnie nie wiem czemu po ustawieniu niestandardowego koloru we właściwościach Visual Studio pokazywane są później inne wartości z jakiegoś predefiniowanego koloru…

 

Na tym dziś kończymy. W najbliższym czasie zamierzam przenieść wyszukiwarkę.

środa, 3 sierpnia 2016

Xamarin.Android kontra Java odc.4 (rozwiązanie problemu ze słabym mruganiem świateł w C#)

Hurra! Zagadka #9 Zagadka z wizualizacji z odcinka 2 rozwiązana! Zielona Xamarinowa edycja app-ki mruga już tak samo jak oryginał w Javie. Poprawka wbita na githubie.

WP_20160803_22_35_05_Pro

Co nie grało? Czemu w Javie dokładnie ten sam kod inaczej działał niż w C#?  Zrobiłem sobie logowanie tablicy bajtów z FFT dostarczanych do mojej klasy LightOrganProcessor przez obiekt klasy Visualizer z Android API w projekcie w Javie oraz w C#.

Java:

08-03 19:23:35.166 19561-19561/com.apps.kruszyn.lightorganapp I/sample_LightOrganProce: processFftData 1470245015166: [-8, 0, -5, -1, -8, 0, -14, 2, -13, 17, 24, -13, 11, 10, 13, -3, 4, -8, -1, -4, 3, 3, 4, -3, 0, -5, -1, -2, 1, 0, 3, -2, -2, -4, -2, 0, 0, 1, 2, -1, -1, -3, -2, -2, -1, 0, 1, -1, 0, -3, -3, -1, -1, 0, 0, -1, 0, -2, -2, -1, 0, 0, 0, -1, -1, -2, -2, -1, 0, 0, 0, -1, 0, -1, -1, 0, -1, 0, 1, -1, 0, -2, -1, -1, 0, 0, 0, -1, 0, -1, -1, 0, -1, 0, 0, -1, 0, -1, -1, -1, 0, -1, 0, -1, -1, -1, -1, -1, 0, 0, 0, -1, 0, -2, -1, -1, 0, 0, 0, -1, -1, -2, -2, -2, -3, 0, -1, 0, -2, 2, 0, 0, 0, 0, 0, 1, 0, -1, -1, 0, 0, 0, 1, -1, 0, -1, -1, -1, -1, 0, 0, -1, -1, -1, -1, 0, 0, -1, 0, -2, -3, -2, -2, -3, -1, 1, 0, 1, 0, -1, 0, -2, 0, -1, 0, -1, -1, -1, -2, -1, 0, -1, 0, 0, 0, -1, -1, -1, -1, 0, -2, 0, -1, 0, 0, 0, -1, 2, 0, -5, -2, -2, -2, 0, 0, 1, 0, -1, -1, 0, -2, -1, 1, -1, 1, -1, 0, -2, -2, 1, -1, 0, 1, -1, 2, -2, -1, -3, -2, 0, -1, 0, -1, -2, 1, -3, -2, -1, -3, -2, -1, 2, -1, -2, -3, 0, 2, 0, 0, 1, -1, -1, 0, 0, 1, 0, 1, -2, 0, -2, -2, -1, 0, -4, -2, -2, 0, -3, -2, -1, -4, -1, -2, 1, 0, -1, -1, 0, -1, 0, -1, -1, -1, 1, -3, 2, 0, 2, 3, 0, 0, 0, 0, -1, 0, 1, -1, -2, 1, -3, -2, -2, 1, 0, -1, -1, -2, -2, -2, -1, -2, -1, -2, 1, -2, -1, 1, -1, 0, -1, -1, -1, -2, 0, -1, -2, -1, 0, 1, 0, 0, -2, -3, 0, -2, 0, -1, 0, 0, 0, -1, -2, 0, -3, -1, -3, -3, -1, -2, 0, -1, 1, 0, 1, 1, 0, -2, 1, 0, 1, 0, -1, 0, -2, 0, -1, -4, -3, -2, 1, -1, 1, 0, 2, -2, 1, -1, -1, 0, -2, -2, 0, -1, 0, -1, -1, -1, -1, -2, -1, -2, -1, 0, 0, -1, 0, -1, 0, -4, 0, -1, 1, -1, 2, 0, 1, 0, 0, -1, 0, -1, 1, 0, -1, -1, 0, 0, 0, -1, -1, -3, -1, 0, 2, -1, 1, 0, -1, -1, 0, 0, 0, 0, 1, -1, -1, -1, -1, -2, 2, 0, 0, 0, -2, -2, -1, -2, 1, 0, -1, -1, -1, -2, -1, 1, 0, -1, 0, -1, 0, 0, 0, -1, 1, -2, 0, -1, 1, 0, 2, -1, 0, -1, 1, 0, -1, -1, -1, -2, 1, 0, 1, 0, 1, 1, 0, -1, 1, 0, -1, -1, -2, -1, 0, -1, 0, -1, 0, 0, 1, 2, 1, -2, 2, 2, -2, 0, -2, -2, -1, -1, 0, 0, -1, 0, 0, 0, -1, 0, -1, -2, 1, 0, -1, 0, 0, 1, 0, -1, 1, -2, 0, -1, 0, -1, 2, 1, 0, -1, -1, -1, 0, 1, 0, 1, 0, 0, -1, -1, -1, -1, 0, -1, 1, 0, 1, 1, 1, 2, 0, 0, 0, -1, -1, 1, 0, 1, -1, 0, -2, -1, -2, -2, 0, -1, 2, 1, 1, 2, -1, 0, -2, -3, -1, -1, -1, 0, 0, 0, 0, 0, 0, 1, 1, 1, -1, 0, -1, 0, 0, -2, -1, -1, 0, 0, 0, -1, 0, -1, 1, 1, 0, 1, 0, 1, -1, 0, -1, 0, -1, 0, -1, 0, -1, 0, 0, 0, 0, 1, -1, -1, -1, 0, 0, -1, 0, -1, 0, 0, 0, 1, -1, 0, -2, -2, -2, 0, 1, -1, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, -1, 1, -1, -2, -2, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, -1, 0, -2, 0, 1, 0, -1, -1, 0, 0, 0, 0, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, -1, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, 0, -1, 0, 0, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, 0, -1, -1, -1, 0, 0, 0, -1, -1, -1, -1, -1, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, -1, 0, 0, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, -1, 0, 0, 0, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, -1, 0, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, 0, -1, -1, -1, -1, 0, 0, -1, 0, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, 0, 0, 0, 0, -1, 0, -1, -1, 0, -1, 0, 0, 0, -1, -1, -1, -1, -1, 0, 0, -1, 0, -1, -1, -1, -1, 0]

C#:

08-03 19:47:04.625 I/sample_LightOrganProce(22658): processFftData 1470246424618: [2, 0, 0, 251, 249, 1, 1, 15, 19, 14, 241, 215, 12, 242, 254, 241, 250, 249, 253, 0, 2, 249, 0, 252, 255, 254, 1, 255, 2, 254, 2, 255, 0, 250, 254, 253, 0, 254, 0, 254, 0, 253, 255, 253, 255, 254, 255, 254, 255, 255, 0, 255, 0, 255, 0, 254, 255, 254, 255, 255, 0, 255, 0, 254, 0, 254, 255, 254, 255, 255, 255, 254, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 254, 1, 254, 0, 255, 0, 254, 255, 255, 0, 254, 255, 255, 255, 255, 0, 255, 0, 255, 255, 255, 255, 255, 255, 255, 255, 254, 254, 255, 255, 0, 255, 0, 255, 0, 0, 255, 254, 0, 255, 1, 255, 2, 1, 254, 1, 0, 0, 255, 1, 0, 1, 0, 1, 255, 0, 254, 0, 255, 0, 255, 0, 0, 255, 0, 1, 255, 0, 1, 0, 0, 3, 255, 0, 253, 255, 0, 253, 255, 1, 2, 0, 255, 0, 255, 3, 0, 255, 1, 1, 255, 0, 254, 254, 0, 0, 255, 255, 0, 1, 2, 3, 1, 2, 253, 2, 254, 255, 255, 1, 255, 0, 255, 254, 250, 0, 254, 255, 254, 2, 254, 254, 253, 252, 2, 2, 2, 1, 254, 0, 1, 1, 0, 1, 254, 1, 0, 1, 253, 0, 0, 0, 2, 4, 250, 0, 253, 254, 254, 2, 255, 0, 253, 255, 252, 254, 252, 251, 253, 254, 8, 255, 255, 255, 1, 255, 0, 4, 254, 2, 253, 255, 252, 252, 253, 253, 254, 0, 255, 0, 0, 254, 252, 252, 0, 0, 3, 2, 0, 0, 0, 1, 255, 3, 2, 3, 255, 2, 254, 254, 254, 0, 254, 255, 254, 255, 2, 253, 255, 254, 253, 2, 0, 2, 0, 0, 1, 1, 255, 1, 0, 255, 252, 254, 254, 0, 255, 0, 253, 253, 254, 254, 0, 255, 0, 255, 0, 254, 255, 252, 255, 254, 255, 0, 0, 253, 1, 255, 0, 1, 255, 0, 255, 254, 0, 254, 1, 4, 1, 1, 0, 0, 1, 1, 254, 255, 253, 253, 254, 253, 2, 1, 5, 5, 1, 2, 254, 1, 252, 0, 1, 2, 253, 255, 1, 0, 1, 0, 255, 3, 255, 2, 1, 1, 0, 1, 0, 2, 2, 3, 254, 1, 252, 0, 254, 1, 254, 2, 251, 255, 254, 254, 253, 253, 255, 254, 4, 1, 255, 0, 255, 255, 255, 2, 254, 255, 254, 0, 0, 0, 0, 0, 3, 1, 254, 1, 0, 253, 255, 255, 0, 255, 0, 254, 255, 1, 0, 0, 1, 2, 255, 1, 0, 255, 255, 254, 1, 253, 1, 0, 3, 2, 2, 1, 255, 255, 254, 255, 0, 255, 2, 2, 253, 2, 253, 0, 254, 255, 255, 254, 254, 255, 254, 2, 1, 0, 255, 254, 255, 254, 0, 255, 1, 1, 0, 1, 255, 0, 3, 1, 255, 255, 255, 254, 255, 254, 0, 2, 254, 0, 255, 255, 0, 0, 1, 1, 0, 255, 1, 2, 254, 0, 254, 253, 253, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 254, 0, 0, 0, 255, 0, 253, 0, 2, 3, 255, 254, 1, 253, 1, 0, 0, 1, 1, 1, 1, 255, 255, 255, 0, 1, 1, 0, 2, 255, 0, 253, 255, 254, 255, 0, 255, 255, 0, 255, 255, 255, 0, 255, 0, 254, 1, 2, 0, 254, 254, 0, 0, 0, 255, 1, 2, 1, 2, 255, 0, 253, 254, 253, 253, 255, 255, 1, 1, 0, 2, 254, 1, 253, 253, 0, 2, 255, 0, 254, 255, 1, 0, 1, 1, 0, 1, 255, 1, 254, 0, 254, 255, 255, 255, 2, 1, 1, 3, 255, 255, 254, 254, 255, 255, 0, 1, 255, 0, 253, 255, 255, 0, 2, 1, 2, 1, 255, 2, 254, 0, 0, 0, 255, 1, 255, 253, 254, 254, 255, 255, 0, 1, 1, 2, 255, 0, 253, 0, 253, 255, 255, 255, 254, 0, 255, 0, 254, 254, 0, 1, 254, 255, 255, 0, 0, 0, 254, 255, 255, 253, 1, 254, 1, 0, 1, 1, 0, 0, 255, 255, 255, 255, 255, 0, 255, 0, 255, 255, 255, 255, 255, 0, 255, 0, 255, 255, 255, 0, 255, 0, 255, 0, 255, 255, 255, 255, 255, 0, 255, 0, 255, 0, 255, 255, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 255, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 255, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 0, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 0, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 255, 0, 255, 0, 0, 0, 255, 0, 255, 0, 255, 0, 0, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 0, 255, 0, 255, 0, 0, 0, 0, 0, 255, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 255, 0, 0, 0, 255, 0, 255, 0, 255, 0, 0, 0, 255, 0, 255, 0, 255, 0, 255, 0, 0, 0, 0, 0, 255, 0, 255

Podczas portowania wkradła się szkolna pułapka (kiedyś na studiach gdy Java rozmawiała z C++ przez sockety stało się podobnie….). Pułapką była zbytnia wierność kodu w C# w stosunku do kodu w Javie, w sumie to mapowanie API w Xamarinie z Javy na C# ma trochę za uszami. Przecież byte w Javie to NIE TO SAMO co byte w C#, po dwakroć, po trzykroć!  W Javie byte jest signed, podczas gdy w C# jest unsigned.

image

http://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html

image

https://msdn.microsoft.com/en-us/library/5bdb6693.aspx

W C# bajtem unsigned jest sbyte.

image

https://msdn.microsoft.com/en-us/library/d86he86x.aspx

Xamarin zwraca nam z Android API tablicę byte[] zamiast sbyte[]. Jak dokonam rzutowania bajtu w C# (powstałego z bajtu w Javie) na sbyte, to powrócę do tego, co było w Javie.

Po zastosowaniu poprawki:

    public class LightOrganProcessor
    {
        …

        public void ProcessFftData(Visualizer visualizer, byte[] fft, int samplingRate)
        {

            …
             //bass

            …
            while (nextFrequency < LowFrequency)
            {
                energySum += (int)GetAmplitude((sbyte)fft[k], (sbyte)fft[k + 1]);
                k += 2;
                nextFrequency = ((k / 2) * sampleRate) / (captureSize);
            }
            …


            //mid
            …

            while (nextFrequency < MidFreguency)
            {
                energySum += (int)GetAmplitude((sbyte)fft[k], (sbyte)fft[k + 1]);
                k += 2;
                nextFrequency = ((k / 2) * sampleRate) / (captureSize);
            }
            …


            //treble
            …

            while ((nextFrequency < HighFrequency) && (k < fft.Length))
            {
                energySum += (int)GetAmplitude((sbyte)fft[k], (sbyte)fft[k + 1]);
                k += 2;
                nextFrequency = ((k / 2) * sampleRate) / (captureSize);
            }
           …
        }

        private static double GetAmplitude(sbyte r, sbyte i)
        {
            return Math.Sqrt(r * r + i * i);
        }

       …

    }

mruganie świateł wraca w Xamarinie do normy Winking smile

wtorek, 2 sierpnia 2016

Xamarin.Android kontra Java odc.3 (ustawienia i ich stylowanie, sockety)

W nawiązaniu do odcinka 10 app-ki w Javie na Android z czasów legendarnego już DSP na github w “limitowanej zielonej” gałęzi Xamarin przybył - odpowiadający mu w miarę możliwości - kod w C#. Oto screenshoty prosto z mojego telefonu:

Screenshot_20160802-200250  Screenshot_20160802-200318

Poruszę dziś trzy techniczne zagadnienia. Nie wszystko wyszło jak planowałem. Nastawcie się na zapiskę bez retuszu, mówiącą po prostu “jak jest” –Winking smile

 

1# Ustawienia - własna kontrolka

Napisałem w C# podobnie jak Javie własną kontrolkę NumberPickerPreference do wprowadzania ustawień liczbowych. Posiłkowałem się samplami Xamarina na githubie, powinno być OK. Uwagę zwraca zobrazowanie konstrukcji CREATOR w C# (potrzebny specjalny atrybut ExportField + referencja do assembly Mono.Android.Export), a także miejscami jawna potrzeba użycia obiektów Javy. Klasa BaseSavedState w C# nie jest dostępna w postaci generycznej. W porównaniu z oryginałem w Javie wygląda to trochę mniej zgrabnie, choć oczywiście w powstałym kodzie klasy znajdziemy i bardziej zgrabne drobniejsze elementy C# w porównaniu do Javy.

   [Register("com.apps.kruszyn.lightorganapp.droid.NumberPickerPreference")]
   public class NumberPickerPreference: DialogPreference
   {
       private const int DefaultValue = 0;

       private NumberPicker numberPicker;
       private int? currentValue;

       public NumberPickerPreference(Context context, IAttributeSet attrs): base(context, attrs)
       {
           DialogLayoutResource = Resource.Layout.numberpicker_dialog;
           SetPositiveButtonText(Android.Resource.String.Ok);
           SetNegativeButtonText(Android.Resource.String.Cancel);

           DialogIcon = null;
       }

       …       
       protected override void OnSetInitialValue(bool restorePersistedValue, Java.Lang.Object defaultValue)
       {
           if (restorePersistedValue)
           {
               currentValue = GetPersistedInt(DefaultValue);
           }
           else
           {
               currentValue = (int)defaultValue;
               PersistInt(currentValue.Value);
           }
       }

       protected override Java.Lang.Object OnGetDefaultValue(TypedArray a, int index)
       {
           return a.GetInteger(index, DefaultValue);
       }

       protected override IParcelable OnSaveInstanceState()
       {
           var superState = base.OnSaveInstanceState();

           if (Persistent)
           {
               return superState;
           }

           var myState = new SavedState(superState);
           myState.Value = numberPicker.Value;
           return myState;
       }

       protected override void OnRestoreInstanceState(IParcelable state)
       {
           if (state == null || state.GetType() != typeof(SavedState)) {
               base.OnRestoreInstanceState(state);
               return;
           }

           var myState = (SavedState)state;
           base.OnRestoreInstanceState(myState.SuperState);

           numberPicker.Value = myState.Value;
       }

       public class SavedState: BaseSavedState
       {
           public int Value { get; set; }

           public SavedState(IParcelable superState): base(superState)
           {               
           }

           public SavedState(Parcel source): base(source)
           {               
               Value = source.ReadInt();
           }

           public override void WriteToParcel(Parcel dest, ParcelableWriteFlags flags)
           {
               base.WriteToParcel(dest, flags);
               dest.WriteInt(Value);
           }

           [ExportField("CREATOR")]
           static SavedStateCreator InitializeCreator()
           {
               return new SavedStateCreator();
           }

           class SavedStateCreator : Java.Lang.Object, IParcelableCreator
           {
               public Java.Lang.Object CreateFromParcel(Parcel source)
               {
                   return new SavedState(source);
               }

               public Java.Lang.Object[] NewArray(int size)
               {
                   return new SavedState[size];
               }
           }
       }         
   }

Niestety aplikacja w Xamarin uparcie wyrzuca w runtime wyjątek:

System.NotSupportedException: Unable to activate instance of type LightOrganApp.Droid.UI.NumberPickerPreference from native handle 0x7fe6652874 (key_handle 0x63ae23e).

przy próbie jakiejkolwiek czynności wymagającej załadowania XML dla fragmentu ustawień:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">

    …
    <com.apps.kruszyn.lightorganapp.droid.NumberPickerPreference
        android:key="pref_remote_device_port"
        android:dependency="pref_use_remote_device"
        android:defaultValue="8181"
        android:title="@string/pref_title_remote_device_port" />   

</PreferenceScreen>

Przy okazji mogę pokazać jak można w Xamarin powiedzieć, że jakaś klasa .NET jest z jakiegoś pakietu ala Java. Służy do tego atrybut Register nad pisaną przez nas klasą. Niestety to nie pomogło rozwiązać problemu. Wcześniej miałem wersję podobną do kodu z poprzedniego postu (osadzałem tam kilka swoich elementów w XML), czyli bez atrybutu Register, a w XML po prostu LightOrganApp.Droid.UI.NumberPickerPreference. W każdym przypadku dostaję ten sam wyjątek dotyczący LightOrganApp.Droid.UI.NumberPickerPreference, co pokazuje równoważność obu podejść –Winking smileNiestety na forum Xamarin też nie znalazłem rozwiązania podobnych problemów:

Coś jest na rzeczy, bo nawet w samplu Xamarina o nazwie ApiDemo plik ustawień xml/advanced_preferences.xml z własnym widokiem nie wydaje się używany. Może nie jest to zbyt często używany ficzer i jest jakiś bug albo jednak coś robię podobnie jak parę innych osób inaczej niż ktoś zamierzył…

Aby zielona Xamarinowa edycja mojej app-ki działała, póki co zamiast niestandardowej kontrolki zastosowałem mniej efektowny standardowy EditTextPreference z android:inputType="number":

<EditTextPreference
        android:key="pref_remote_device_port"
        android:dependency="pref_use_remote_device"
        android:defaultValue="8181"
        android:inputType="number"
        android:maxLines="1"
        android:singleLine="true"
        android:title="@string/pref_title_remote_device_port" />

Nie zauważyłem, by przy takim inpucie można wprowadzić jakieś głupoty do tego pola (poza pustym stringiem), zresztą mój kod jest na nie odporny. Odwołuję się do pola tekstowego, którego wartość konwertuję w razie potrzeby (i możliwości) na liczbę.

 

#2 Ustawienia - stylowanie okien dialogowych

Zielone tło całego fragmentu z ustawieniami dostałem na dzień dobry w wyniku mojego wcześniejszego stylowania. Okna dialogowe np. do EditTextPreference nadal pozostawały niezielone…  Wyszła tutaj spora trudność w ostylowaniu tego elementu w Androidzie. Posiłkując się stackoverflow ustaliłem, że trzeba zastosować sztuczkę i ostylować  Dialog.Alert w używanej themie. W swojej aplikacji używam Theme.AppCompat, by wspierać Android starszy niż 5.0. Dostawałem wtedy co prawda ładne zielone tło, ale okno dialogowe … się nieładnie skurczyło w stosunku do wersji nieostylowanej. Długo zajęło mi zanim dotarłem do posta https://www.reddit.com/r/androiddev/comments/2qt9kd/overriding_alertdialogtheme_and_material_themes/, który wyjaśnił mi wszystko. Ponieważ jest to limitowana edycja mojej app-ki postanowiłem, że odpuszczę systemy starsze niż 5.0 i po zastosowaniu zdefiniowaniu stylu dziedziczącego z systemowego stylu android:Theme.Material.Dialog.Alert otrzymałem zielone okienko dialogowe w postaci nieskurczonej:

     <style name="AppTheme" parent="Theme.AppCompat">        
        …       
        <item name="android:alertDialogTheme">@style/AlertDialogStyle</item>       
    </style> 

    <style name="AlertDialogStyle" parent="android:Theme.Material.Dialog.Alert">     
      <item name="android:background">#ff1b5e20</item>       
    </style>

Nielicho się narobiłem…. wymyślając te kilka linijek w XML-u. Na obecny moment nie walczyłem, by działało to na Android 4.x…

 

#3 Sockety

Tak jak w przypadku oryginału w Javie postanowiłem użyć socketów, by rozmawiać z diodami LED na Raspberry Pi 2.  Generalnie mogę powiedzieć, że komunikacja mi działa. Mam jednak kilka uwag.

Po pierwsze najpierw myślałem, że sobie przekleję kod z testowej klienckiej app-ki UWP z czasów DSP. No tak, ale na UWP dla Windows 10 mamy platformę WinRT i .NET Core, a Xamarin używa Mono. Skutkuje to potrzebą pisania innego kodu przy użyciu klas TcpClient i NetworkStream zamiast nowocześniejszych StreamSocket i DataWriter. Dobrze chociaż, że w klasach Mono są metody zwracające obiekty klasy Task, na które można poczekać await-em. No tak, postulat Scotta Hanselmana z BUILD 2016, by zrobić porządek z .NET (pełny .NET, .NET Core, Mono) i stworzyć wspólną podstawę pokazuje swoją zasadność. Tym bardziej, że Microsoft póki co nie oferuje dll-ki portable dla socketów. Są oczywiście komponenty trzecie np. Sockets Plugin (dostępny także w postaci pakietu nuget), ale lepiej byłoby by .NET sam sobie z tym radził.

Przejrzałem sobie kod ostatnio wspomnianego tu komponentu, okazał się dość prosty, więc na razie postanowiłem skorzystać bezpośrednio z klas Mono. Nie jest możliwe stworzenie portable library tylko dla Android i iOS (zaznaczają się zawsze checki z .NET, Windows 8.x, UWP), więc stworzyłem sobie projekt LightOrganApp.Shared - jak nazwa wskazuje - typu Shared. Posłuży mi teraz do zielonego klona w Xamarin.Android, a później także do Xamarinowego klona na iOS. W stosunku do oryginału w Javie przy pomocy async powstał bardziej zgrabny kod w C#, którego esencję zamknąłem w klasie LightsRemoteController:

    public class LightsRemoteController
    {
        TcpClient tcpClient;
        NetworkStream writeStream;       

        public async Task ConnectAsync(string host, int port)
        {
            try
            {
                tcpClient = new TcpClient();

                await tcpClient.ConnectAsync(host, port);
                writeStream = tcpClient.GetStream();
            }
            catch(Exception ex)
            {
                Debug.Write(ex.StackTrace);
            }          
        }

        public async Task SendCommandAsync(byte[] bytes)
        {
            try
            {
                if (writeStream != null)
                {
                    writeStream.Write(bytes, 0, bytes.Length);
                    await writeStream.FlushAsync();
                }
            }
            catch(Exception ex)
            {
                Debug.Write(ex.StackTrace);
            }
        }       

        public async Task CloseAsync()
        {
            try
            {
                var bytes = new byte[3] { 13, 13, 13 };
                await SendCommandAsync(bytes);

                if (writeStream != null)
                    writeStream.Close();

                if (tcpClient != null)
                    tcpClient.Close();
            }
            catch(Exception ex)
            {
                Debug.Write(ex.StackTrace);
            }

            writeStream = null;
            tcpClient = null;
        }
    }

Korzysta z niej MusicService. Aplikacja zachowuje się w każdym przypadku jak powinna, ale łapanie wyjątków się w niej przydaje. Przy podawaniu błędnego hosta lub portu albo przy włączaniu przesyłu przy błędnej konfiguracji czy też przy powrocie do prawidłowych ustawień sypie się więcej wyjątków niż sypało się w takich sytuacjach w Javie… Jakby czasami pewien kod po zaistnieniu błędu wywoływał się kilka razy.  To jest wstępna implementacja, być może mogę jakoś ją udoskonalić. Z drugiej strony niewykluczone, że sockety na Mono w Xamarin w połączeniu z async same w sobie wymagają pewnych poprawek. Całkiem niedawno były poprawiane do nich bugi (np. Bug 41616 ), pewne  zgłoszenia są otwarte (np. Bug 42617).

 

W tym wszystkim nie chodzi tu o czarny pijar, nadal uważam, że Xamarin jest całkiem dobrą platformą, a C# z Visual Studio w pewnych - zwłaszcza biznesowych - aplikacjach może lepiej się sprawdzić niż tworzenie kodu w zupełnie innym języku i narzędziach. Obrałem sobie jednak dziś bardziej wysublimowane zagadnienia i pewne rzeczy w platformie mogą wymagać udoskonaleń (punkty 1# i 3#, dzisiejszy punkt 2# dotyczy Androida w ogóle). Dzisiejsze problemy nie są jednak bardzo dokuczliwe. Implementację kolorofonu na Xamarin.Android mogę uznać za zakończoną i w miarę udaną, jeśli uda mi się wyjaśnić sprawę z punktu #9 Zagadka z wizualizacji z poprzedniego posta. Zapowiada się ciekawe śledztwo.