XamarinとXamarin.Formsはチョコレートサボテンです。 パート2

ごく最近、人気のあるXamarinモバイルフレームワークの機能と問題に関する記事を公開しました。 今日もストーリーを続け、Xamarin.Formsライブラリのニュアンスに焦点を当てます。 カットの下で、クロスプラットフォームUIを作成することを決めた人を待っているレーキについてのストーリーがあります。



基本的な問題



まず、レイアウトはコードとXAML形式の両方で準備できます。 残念ながら、この機能はネイティブ開発ツールで利用できますが、インターフェースのプレビューをリアルタイムで見ることはできません。 そのため、コードからインターフェイスを開発することにしました。 少しかさばりますが、全体的には便利です。



public class LoginViewController: ContentPage { public LoginViewController() { Content = new StackLayout { Orientation = StackOrientation.Vertical, Children = { new Entry { Placeholder = ". ", Keyboard = Keyboard.Email, }, new Entry { Placeholder = "", IsPassword = true, }, new Button { Text = "" }, new ActivityIndicator { IsRunning = true, IsVisible = false, } } }; } }
      
      





さらに、Xamarin.Forms のコンポーネントセットはそれほど大きくありません。 カスタムコンテンツ用の「カルーセル」など、そのような一見当たり障りのないものは十分ではありません。 フルスクリーンのカルーセルコントローラーがありますが、画面の一部のみを占める同様のコンポーネントが必要でした。 私はサードパーティ製のバイクを少し曲げなければなりませんでした。



多くの場合、利用可能なコンポーネントには、iOSまたはAndroidで利用可能な機能やイベントがありません。 プレースホルダーのフォントやカーソルの色を変更したり、テキストフィールドの最大長を設定したりすることなどができない場合があります。これらを自分で追加する必要があります。 2015年11月中旬にリリースされたXamarin.Forms 2.0バージョンでは、これらのプロパティの一部が追加されましたが、ネイティブプラットフォームのすべての機能の最大100%のカバレッジはまだ遠いです。



すべてのコンポーネント(パディングとマージン)にパディングを設定できないことも不満です-コンテナにのみパディングがあります。 ボタンまたは入力フィールドをインデントしますか? コンテナで包みます:



 new ContentView { Padding = new Thickness { Top = Sizes.StandartTopPadding Left = Sizes.StandartLeftPadding }, Content = new Label { Text ="  " } }
      
      





ただし、階層が深すぎるとレンダリングプロセスが遅くなり、Formsは原則としてネイティブコンポーネントよりも多少遅くなります。 リストでは特にスローダウンが顕著ですが、一部のアプリケーションでは単純に複雑なフォームが深刻な問題になる場合があります



可能性の一部が正しく実装されていないこともまた喜ばしいことであり、これは「機能」です。 たとえば、Androidで標準ナビゲーションを使用している場合、コントローラーは新しい画面に切り替えてもライフサイクルイベントを取得しません。 ナビゲーションは実際の画面やフラグメントではなく、単一の物理画面内のビューの通常の変更によって行われます。





また、コンポーネントにはしばしばバグがあります。 たとえば、 ScrollViewには問題がありました-キーボードが表示されたときに、コンテンツのない領域に必要以上にスクロールをスクロールすることができました。







問題の原因は、ScrollViewのコンテンツの高さがコンテナよりも小さいことです。 コンテンツをスクロールする領域のサイズは、次のコードによって決定されます。



 protected override void LayoutChildren(double x, double y, double width, double height) { //[...] ContentSize=new Size(width, Math.Max(height, Content.Bounds.Bottom + Padding.Bottom)); }
      
      





その結果、問題をどれほど迅速に(そして汚いほど)解決できるかというアイデアが浮かび上がりました。目的のメソッドが重複するScrollViewの子孫を作成します。



 protected override void LayoutChildren(double x, double y, double width, double height) { //[...] // Max,       ContentSize = new Size(width, Content.Bounds.Bottom + Padding.Bottom); }
      
      





ただ? いずれにしても、ContentSizeプロパティにはプライベートセッターがあり、継承者の値は変更されません。 しかし、カーブパスに沿って行ったので、いつでもヘルプリフレクションを呼び出して、プロパティの値を変更できます。



 public class ScrollViewCopycat : ScrollView { private readonly Action<Size> setContentSize; public ScrollViewCopycat() { var methodInfo = typeof(ScrollViewCopycat) .GetProperty("ContentSize", BindingFlags.Instance | BindingFlags.Public) .GetSetMethod(true); setContentSize = value => methodInfo.Invoke(this, new object[] { value }); } protected override void LayoutChildren(double x, double y, double width, double height) { //[...] setContentSize(new Size(width, Content.Bounds.Bottom + Padding.Bottom)); } }
      
      





ある時点で、最終的に次のバグを解決しました:一連のコントロールの可視性プロパティの値を変更すると(画面のいくつかのフィールドにIsVisibleプロパティを設定し、一方をFalseに、もう一方をTrueに )、要素が画面に表示されないことがあります! 同時に、彼は階層の代わりになりました(フォームのスクリーンに穴が現れました)が、実際には彼は隠れていました。 問題は私たちだけでなく、Xamarinフォーラムでいくつかの議論を見つけることができます-ここに例が1回2回あります。



バグはフローティングであることが判明し、Xamarin.Forms 1.3.3.6323以降で発生しました。フォーム自体の内部の競合状態が原因で問題が発生しました。 したがって、しばらくの間、古いバージョンのままでしたが、バグバージョン1.3.1.6296はありませんでした 。 残念なことに、このバージョンには独自のバグが後のバージョンで修正されていました。



そのため、最終的にこの決定に至りました。





詳細なコード
 public class Batch { private readonly ILayoutController visualElement; public Batch(ILayoutController visualElement) { this.visualElement = visualElement; } public IDisposable Begin() { var animatables = GatherAnimatables(visualElement).ToArray(); foreach (var animatable in animatables) animatable.BatchBegin(); return new ActionDisposable(() => { foreach (var animatable in animatables) animatable.BatchCommit(); }); } private static IEnumerable<IAnimatable> GatherAnimatables(ILayoutController root) { return root.Children.OfType<IAnimatable>() .Concat(root.Children.OfType<ILayoutController>().SelectMany(GatherAnimatables)); } }
      
      





このコードは、前述の問題を解決するだけでなく、コンポーネントの複数のプロパティを一度に変更する場合にも推奨されます。 コードが次のように書かれているとしましょう:



 if (alert) { errorlabel.IsVibislbe = true; errorlabel.TextColor = Colors.Red; errorlabel.Text = AlertText; }
      
      





そのコンポーネントは、プロパティが変更されるたびに3回再描画されます。 ただし、BatchBegin / BatchCommitでラップすると、再描画(およびサイズの再計算)が1回だけ行われ、速度にプラスの影響があります。



他にもバグがあります。たとえば、 TextViewはコンテナーのサイズに影響を与える可能性がありますが、「全幅に拡大」オプションがあります。







これは、垂直コンテナが水平方向の別のコンテナにある場合に発生します。



問題につながるコード。
 Content=new StackLayout { Orientation = Orientation.Horizontal, BackgroundColor = Color.Green, Children = { new StackLayout { Orientation = StackOrientation.Vertical, VerticalOptions = LayoutOptions.FillAndExpand, HorizontalOptions = LayoutOptions.FillAndExpand, Children = { new Label { BackgroundColor = Color.Red, HorizontalOptions = LayoutOptions.FillAndExpand, } } } } }
      
      







通信モデルとUIコンポーネント(バインディング)



モデルとビューの間の双方向バインディングの組み込みサポートも、私たちを満足させませんでした。 リンクを示す最初のオプションは次のとおりです。



 public class Model1 { public string Text { get; private set; } public Model1 (string text) { Text = text; } } var label1 = new Label { BindingContext = new Model1("Hello, problems!") } label1.SetBinding(Label.TextProperty, "Text");
      
      





間違えた場合、「テキスト」の代わりに別の名前を書くと、コンパイル段階でもランタイムでも何も爆発しません。 ラベルだけがテキストなしで表示されます。



もちろん、接続のセットアップは少し改善されています。



 label1.SetBinding<Model1>(Label.TextProperty, source => source.Text);
      
      





しかし、別のオブジェクトがラベルに配置される状況から私たちを救いません:



 var label1 = new Label { BindingContext = new Model2(), };
      
      





この場合も、実行中に何も落ちません。



しかし、それだけではありません。 モデルで相互接続されたフィールドが必要な場合(別の変更が1つ変更される場合) -UIが機能するためには、かなり退屈なコードを追加する必要があります-INotifyPropertyChangedインターフェイスを実装し、変更されたフィールドのリストを自分で報告します:



 public class Model : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged([CallerMemberName]string propertyName = null) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } private int value1; public int Value1 { get { return value1; } set { value1 = value; OnPropertyChanged(); OnPropertyChanged("Value2"); } } public int Value2 { get { return Value1*2; } } }
      
      





これらの理由から、モデルとコントローラーの間に独自のバインディングを作成しました-フィールドタイプの対応をチェックし、関連するフィールドを自動的に更新するなど。



リスト



まあ、別の頭痛-リスト。 ささいなことから始めましょう:リストにはヘッダーとフッター( footerheader )があり、通常の行に沿ってスクロールするユニークなセルがあります。 これはいいです。 ただし、タイトルのコンテンツを置換する場合、新しいタイトルが前のタイトルよりも大きいまたは小さい場合、その高さは再カウントされず、テーブル行の高さは固定されます。 手動でやらなければならない



 public interface IHeader { Layout GetView(); double GetHeight(); } public void SetHeaderForm(IHeader value) { value.GetView().Layout(new Rectangle(value.GetView().X, value.GetView().Y, Width, value.GetHeight())); list.Header = value; }
      
      





ネイティブiOSコンポーネントで作成する場合、このような問題は発生せず、サイズは自動的に再計算されます。



もう1つの不快な瞬間は、「 コンテキストアクション 」です。 このメニューは通常、長いタップでAndroidを呼び出し、iOSで-セルをスワイプして呼び出します。 この状況での問題は、Xamarin.Formsのこれらのコンテキストアクションに対してMenuItemオブジェクトが使用されることです。これには、とりわけIconプロパティがあります。 ただし、これらのメニューにはアイコンは表示されません。 そして、 これは機能です



そのため、アイコンを表示するために、Object-CライブラリMGSwipeTableCellを使用し、その周りに独自のラッパーを作成しました。 確かに、結果として、リスト内のセルのサイズを自動的に変更する機能が失われました-セルの正しい複雑なカスタムレンダリングの記述は見た目ほど単純ではないため、すべてのセルは厳密に同じ高さになります。



最後に、リストはデータソースとしてIEnumerableとして受け入れられますが、デフォルトでは「スクロールしながらロード」はありません。ソースを決定する時点で、コンポーネントはデータを最後まで読み取ります。 「箱から出してすぐに」iOSまたはAndroidには無限のリストがないため、このような動作を期待していましたが、まだいくつかの希望がありました。 残念ながら、Xamarin.Formsのコンポーネントは、生計を立てる機会のみを実装しています。他のすべては、私たち自身で完了する必要があります。



結論



Xamarin.Formsを使用する価値があるかどうか-次のステップでは、Android用にすでに作成されたJavaプロジェクトをFormsに転送します。 しかし、Xamarin.Formsは最も単純なUIにのみ使用する必要があると言えます。 特定のプラットフォームのすべての単一チップを使用したり、設計上の巧妙な決定を行う場合、Xamarin.Formsは支援よりも邪魔になります。 このオプションでは、ビジネスロジック専用にXamarinを使用し、各プラットフォームのレイアウトをネイティブにすることをお勧めします。



ご質問やご意見がありましたら、コメントでお答えします。



All Articles