Xamarin.FormsでSnackBarを表示してみた
はじめに
今回はAndroidのSnackBarをXamarin.FormsでAndroid/iOSで実装してみました。
完成動画はこちら
— ゆ~か (@yu_ka1984) 2017年5月13日
コードはこちら
SnackBarはAndroidのマテリアルデザインライブラリに含まれる表現で、画面下からちょこっと出てきて通知を行ったりToastと違いタップによって動作を行ったりすることができます。
今回はXamairn.FormsでSnackBarをAndroid/iOSで使用してみたいと思います。
共通部分解説
まずは共通ライブラリで使用するインターフェース
public interface ISnackBar { void Show(string text, int duration, string actionText, Action action); }
本来ならもっとオプション化が必要になりますが、今回はそれは主旨ではないので省きます。
Android実装開設
XamarinForms_SnackBar/AndroidSnackBar.cs at master · yuka1984/XamarinForms_SnackBar · GitHub
AndroidはSnackBarを直接呼び出しているだけなので特に特殊なことはないです。
iOS実装開設
XamarinForms_SnackBar/TouchSnackBar.cs at master · yuka1984/XamarinForms_SnackBar · GitHub
考え方としてはUIViewでSnackBarの見た目を構築してUIView.Animateにて表示アニメーション、時間経過後およびAction実行時に閉じるアニメーションを行っています。
KeyWindowにAddSubViewしているのでXamarin.Formsで画面遷移してもSnackBarは表示されたままになります。
これを例えば表示中のViewに表示したいようなケースの場合にはwindowsのPresentatedViewControllerにAddSubViewすればよいです。
How to acces the current view UIViewController from an external service — Xamarin Forums
SnackBarのレイアウトはLaytouAnchorを使用しています。
iOS 9で追加されたNSLayoutAnchor使うと簡単にわかりやすく間違えずにNSLayoutConstraint(制約)が作れます【Auto Layout】 - Qiita
UIView.Animateでは定義したTopのLayoutAnchorに対してConstantプロパティを変更してAnimationを行っています。
UIView.Animate(OpenDuration, () => { initialTop.Constant -= BoxHeighy; window.LayoutIfNeeded(); });
AndroidのSnackBarは重ねて表示するということがなくSnackBar表示中に別のSnackBarを表示した場合には、まず現在表示中のSnackBarが閉じて新しいSnackBarが開くという動作になるため、iOSでもそのように実装しました。
_addedsnacks にUIViewと表示待機ノ為のTask.Delayに設定したCancellationTokenSourceを保持させておきShowメソッドが呼び出された際にはClearSnackにてDelayをキャンセルさせて表示中SnackBarを閉じ、その後新しいSnackBarを表示します。
private async Task ClearSnack() { var count = _addedsnacks.Count; foreach (var added in _addedsnacks) added.Item1.Cancel(); await Task.Delay((int) (CloseDuration * 1000 * count)); }
Actionの実行に関しては引数で受け取ったActionクラスを使って表示待機をキャンセル後Actionを実行しています。
button.TouchUpInside += (sender, e) => { cancel.Cancel(); action(); };
SnackBarの利用
めんどくさがってすいません。
もう少しちゃんと実装するならDependencyService使うなりContainer使うなりしてください。
public partial class SnackBarSamplePage : ContentPage { public SnackBarSamplePage() { InitializeComponent(); SnackButton.Clicked += ButtonOnClicked; #if __IOS__ snackbar = new SnackBarSample.iOS.TouchSnackBar(); #else snackbar = new SnackBarSample.Droid.AndroidSnackBar(); #endif } private ISnackBar snackbar = null; private void ButtonOnClicked(object sender, EventArgs eventArgs) { snackbar.Show("Description" + DateTime.Now.ToString(), 2500, "Click", () => { DisplayAlert("alert", "click", "close"); }); } }
おわりに
いかがでしょうか?
こんな感じで割と簡単にアニメーション付きの特殊動作などを実行することができます。
ではでは( `ー´)ノ
Xamarin.FormsでCustomRendererでContentPageをカスタマイズしてみる
はじめに
今回はContentPageのRendererを継承したクラスを作成してContentPageをカスタマイズしてみたいと思います。
完成品はこちら
コードはこちら
IsBusyプロパティに連動したローディング表現を実装してみます。
以前はに書いた記事ではNavigationPageでIsBusyがTrueになった時に単純にインジケータを動かすだけでしたが
IsBusyがTrueになった時にインジケータの表示に加えてページをスライドアウトさせて非表示にしIsBusyがFalseになった時にはページがスライドインしてくるような動作をAndroid/iOSで書いてみました。
更にIsBusyがTrueになってから1秒のDelay後にローディングが行われるようにしました。
ページの実装
XamarinFormsLoadingPage/MainPage.xaml.cs at master · yuka1984/XamarinFormsLoadingPage · GitHub
Pageでの実装は今回もボタンをクリックするとIsBusyが変化するだけのものです。
ボタンをクリックするとIsBusyがTrueになります。
2500msec後にIsBusyはfalseになります。
その途中にボタンをクリックした場合にはIsBusyはfalseになります。
Androidの実装解説
XamarinFormsLoadingPage/LoadingPageRenderer.cs at master · yuka1984/XamarinFormsLoadingPage · GitHub
PageRendererクラスを継承して実装を行います。
AndroidのPageRendererはVisualElementRenderer
部品(View or ViewGroup)を組み合わせて画面を構成できます。
セットアップ
まずはクラスセットアップとしてOnElementChangedにてAddViewを使ってAndroid.Widget.ProgressBarを追加します。
_progress = new AProgressBar(Context, null, Android.Resource.Attribute.ProgressBarStyleSmall) { Indeterminate = true }; AddView(_progress); _progress.Visibility = ViewStates.Invisible;
次にValueAnimatorの設定を行っています。
ValueAnimatorは
AndroidでもiPhoneに負けないようなアニメーションを実装してみよう - Yahoo! JAPAN Tech Blog
アニメーションを実行するために、アニメーション中の値を計算して、それらの値をターゲットオブジェクトに設定するための、簡単なタイミングエンジンを提供します。
というものでこれを使用してIsBusyが変化したときのアニメーション動作を作成ています。
if (animator == null) { animator = ValueAnimator.OfFloat(0f, 1f); animator.SetDuration(300); animator.Update += (s, a) => { var view = GetChildAt(1); var width = view.Width; var height = view.Height; var c = (float) a.Animation.AnimatedValue; view.Left = (int) (width * c); view.Right = view.Left + width; _progress.Alpha = c; System.Diagnostics.Debug.WriteLine(c); ; }; }
0fから1fまで300msecで変化して変化のたびにUpdateイベントを発行してくれるような感じです。
値を使用して画面のスライドアウトとプログレスバーのAlphaを同時に変更しています。
レイアウト
OnLayoutのoverrideにてプログレスバーの位置を調整しています。
真ん中よりちょっと上くらいに表示させます。
var width = r - l; var woffset = (width - 100) / 2; var hoffset = (b - t) / 10; _progress.Layout(l + woffset, t + hoffset, r - woffset, t + hoffset * 4);
IsBusy
OnElementPropertyChangedのoverrideにてIsBusyが変化したときの動作を実装しています。
IsBusyがTrueになった場合ValueAnimatorにStartDelayを設定しStartしています。
IsBusyがFalseになった場合にはValueAnimatorでStartDelayを0にしてReverseしています。
Reverseすることで1f -> 0fにアニメーションしていくのでIsBusyがTrueになった時の逆のアニメーションが行われます。
Pause Resuemeを使用することでアニメーション中にIsBusyの変化が起きた場合に対応します。
var view = GetChildAt(1); if (view != _progress) { if (animator.IsStarted) animator.Pause(); if (page.IsBusy) { _progress.Alpha = 0; if (!animator.IsPaused) animator.StartDelay = 1000; else animator.Resume(); animator.Start(); } else { animator.StartDelay = 0; if (animator.IsPaused) animator.Resume(); animator.Reverse(); } }
iOSの実装解説
XamarinFormsLoadingPage/LoadingPageRenderer.cs at master · yuka1984/XamarinFormsLoadingPage · GitHub
PageRendererクラスを継承して実装します。
iOSのPageRendererクラスはUIViewControllerクラスを継承して実装されているためカスタマイズする際にはUIViewControllerでできる事は全てできます。
UIViewControllreの位置づけというのはFormsのPageと近いものがあると思います。
ただXamarin.FormsでのUIViewControllerの使われ方という側面でいうと、本来のUIViewControllerの使われ方とは少し異なる感じですので注意が必要です。
セットアップ
UIActivityIndicatorViewをViewにAddSubViewで追加します。
if (_indicator == null) { _indicator = new UIActivityIndicatorView(UIActivityIndicatorViewStyle.Gray); View.AddSubview(_indicator); Indicatorconstraint(_indicator); }
レイアウトはLayoutAucherを使って設定します。
iOS 9で追加されたNSLayoutAnchor使うと簡単にわかりやすく間違えずにNSLayoutConstraint(制約)が作れます【Auto Layout】 - Qiita
Xmarin.FormsでいうRelativeLayoutに近い感じで設定できます。
私自身はこちらの方が書きやすくてよいです。
CreateAnchorした後に.Activeプロパティをtrueにしないと制約が確定しませんので注意が必要です。
よく付け忘れて制約が適用されずに悩む事をしてしまうことが多いです。
CenterX CenterYを使うことで中央より少し上くらいに表示します。
TranslatesAutoresizingMaskIntoConstraintsをfalseにしないと制約が適用されません。
UserInteractionEnable = falseとするとタップしても反応せずに背面に透過します。
protected virtual void Indicatorconstraint(UIActivityIndicatorView indicatorView) { indicatorView.CenterXAnchor.ConstraintEqualTo(View.CenterXAnchor).Active = true; indicatorView.CenterYAnchor.ConstraintEqualTo(View.CenterYAnchor, -30).Active = true; indicatorView.TranslatesAutoresizingMaskIntoConstraints = false; indicatorView.UserInteractionEnabled = false; }
IsBusy
今回はUIView.Animateを使用してみました。
ただこういうケースでは本来はUIView.Transitionを使うのが良いとは思うのですがレイアウト面がまだ使いこなせていないのでAnimateとしました。
iOSアプリ開発でアニメーションするなら押さえておきたい基礎 - Qiita
こちらの記事で学習できます。
UIView.Animateを使用して
- インジケータのAlphaを0から1fへ変更
- Pageの中身のView(PageのXAMLをiOSのUIViewに変換された様なもの)のAlphaを1fから0fへ変更
- Pageの中身のviewの位置を0fから1fの値の変化に応じて移動
というアニメーション動作を設定しています。
戻す際には逆動作となります。
if (e.PropertyName == "IsBusy") { var isbusy = (Element as Page).IsBusy; var view = View.Subviews.First(x => x != _indicator); var distance = view.Frame.Width > view.Frame.Height ? view.Frame.Width : view.Frame.Height; view.Layer.RemoveAllAnimations(); if (isbusy) { _indicator.Alpha = 0; _indicator.StartAnimating(); UIView.Animate(0.3, 1, UIViewAnimationOptions.CurveEaseIn , () => { view.Frame = new CGRect(View.Frame.X + distance, View.Frame.Y, view.Frame.Width, view.Frame.Height); view.Alpha = 0; _indicator.Alpha = 1; } , null); } else { UIView.Animate(0.3, 0, UIViewAnimationOptions.CurveEaseOut , () => { view.Frame = new CGRect(View.Frame.X, View.Frame.Y, view.Frame.Width, view.Frame.Height); view.Alpha = 1; _indicator.Alpha = 0; } , () => { _indicator.StopAnimating(); }); } }
また今回はUIViewControllerのViewからインジケータもPageのViewもRemoveSubViewせずに実装しているのでWillAnimateRotationのoverrideにてIsBusyプロパティに応じてPageのViewの調整を行っています。
このような感じでContentPageのRendererを拡張し様々な事を行うことができます。
ここからかなり詰めていけば、独自のNavigationやUXを実現することも可能です?
ではでは( `ー´)ノ
Xamarin.Forms AndroidでNavigationPageのアニメーションを変更してみる
はじめに
全国の
Xamarin Formsで何でiOSとAndroidでNavigationPageのアニメーション違うんだ。
何でAndroidはスライドインしてくれないんだ。
とお嘆きの皆様、Xamarin Formsをお楽しみでしょうか?
今回はそんなAndroidのNavigationPageのアニメーションを変更してみようかと思います。
完成系
解説
まず今回の主なコードです。
NavigationPageに表示するページはFramgmentContainerクラスというFragmentを継承したクラスにて管理されています。
一つのFragmentContainerクラスに1つのPageクラスが乗っていて、ページのアニメーションをFragmentTransactionにより行なっているイメージです。
SetupPageTransitionメソッドはNavigationPageでPushしたりPopする前に呼ばれます。
ここでSetCustomAnimationsを行うことでナビゲーションアニメーションを変更することが可能になります。
FragmentTransaction | Android Developers
アニメーションに関してはリソース登録を行なって置きます。
今回は4種類のアニメーション設定を用意しました。
中身はこんな感じです。
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" android:shareInterpolator="false"> <translate android:interpolator="@android:anim/accelerate_decelerate_interpolator" android:fromXDelta="100%" android:toXDelta="0%" android:fromYDelta="0%" android:toYDelta="0%" android:duration="@android:integer/config_mediumAnimTime" /> </set>
Translateという物を使ってアニメーションを定義しています。
これらをpush / pop のためにSetCustomAnimationsで設定したあげています。
どうでしょうか?
まあ、Back時の動きおかしくない?って話もあるのですが今回は乗り越えることができませんでした。
もう少しFragmentのアニメーションに関して学習して乗り越えたいと思います。
最悪PRカモですが・・・
ではでは(〃^∇^)o
Xamarin.Forms IsBusyプロパティの活用
はじめに
Xamarin.Formsをお楽しみの皆様、いかがお過ごしでしょうか。
Pageクラスに存在しているのに割と使われる事が少ないプロパティ「IsBusy」
このプロパティがTrueの時に
iOSではなんか画面の上の方で微妙にクルクル回って
Androidでは微妙な大きさのIndicatorが回ったり回らなかったり
まあ多分あんまり利用している人多くないんじゃないかなぁとか勝手に思っております。
でもせっかくプロパティがあるんだから活かしたい。という事で
今回はAndroidのNavigationPageをカスタマイズしてちょっとかっこいい気がするIsBusy表現を作ってみたいと思います。
完成形
コード
解説
コードを見るとわかる通り、実はそんなに大したことはしていないです。
AndroidのNavigationPageRendererを継承したMyNavigationPageRendererを作成しています。
ポイントとしては3点。
- OnElementChangedをoverrideしてAndroidのProgressBarというViewを作成しAddViewでNavagationPageの子要素に追加する。
AndroidのNavigationPageはToolbarとPageContainerという二つのViewで構成されているのですが、そこにProgressBarを追加する感じです。
if (_progress == null) { _progress = new AProgressBar(Context, null, Android.Resource.Attribute.ProgressBarStyleHorizontal) { Indeterminate = true }; AddView(_progress); _progress.Visibility = ViewStates.Invisible; }
- OnLayoutをoverrideして表示位置と大きさを決める。
OnLayoutのタイミングでProgressBarのレイアウトを設定します。 またこのタイミングで重なり順も指定してしまいます。 今回はToolBarのすぐしたくらいに配置しました。
for (var i = 0; i < ChildCount; ++i) { var view = GetChildAt(i); if (view is Toolbar) { _progress?.SetZ(view.GetZ() + 1); _progress?.Layout(l, view.Bottom - 10, r, view.Bottom + 10); } }
- OnElementPropertyChangedをoverrideしてIsBusy変更時にProgressBarのVisibilityを変更して表示非表示を切り替える。
if (e.PropertyName == nameof(Element.IsBusy)) { var page = (NavigationPage) sender; _progress.Visibility = page.IsBusy ? ViewStates.Visible : ViewStates.Invisible; }
あとは子ページのIsBusyに同期させたりしていますがしなくても良いかと思います。
どうでしょう?
たったこれだけで、なんかちょっとかっこいい気がしないでもないLoadingが作れちゃいます。
IsBusyのカスタム動作をCustomRendererで実装する利点としては必要なくなったらExportRenderer外しちゃえばよくてXamlとかViewModelに影響を与えずに切り替えられるっていう点が良いですね。
今回はこれまでです。
ではでは(〃^∇^)o
WeakなReactivePropertyを作ってみた
本題
ReactivePropertyを使う際にViewModelでModel層からToReactivePropertyなどを使う場合にモデル層にSubjectすることで強参照な結びつきが生まれてViewModelがいつまでも破棄されなくなってリークしてしまいます。
その解決アプローチとしてWeakReferenceを利用できないか考えてみました。
今回のリポジトリです
WeakReactiveProperty
とりあえずWeakなReactivePropertyを作成。
まずはWeakSubscriber。
要するにWeakに購読します、みたいな感じです。
これを利用してWeakReactivePropertyを作成。
基本的に素のReactivePropertyのObservableにSubScribeしている部分をWeakSubscribeに置き換える感じです。
詳細までは確認してません。
確認
まずは効果を検証するためにみんな大好きコンソールアプリでチェック
結論としてはWeakReactivePropertyをViewModel側で使うとダメな感じ。
まだVMのインスタンスが参照されているのにGC.Collect()で回収されてしまって動かなくなってしまいます。
良いパターンとしてはModel(コードで言うとWeakServiceクラス)のパターン。
モデル層でWeakReactivePropertyを使うとうまくいっています。
これならうまくいきそうなので実際にXamarin.Formsで検証してみました。
こんな感じのVMを作ります。
App.csはこんな感じ。
NavagationPageに沢山スタックさせてからバックボタンで最初のページまで戻ってカウントアップボタンを押します。
Weakでない普通のサービスクラスを使用するとデバッグウィンドウに作ったページ分のデバッグ出力が出てしまいます。
所謂リークしている状態。
次にServiceをWeakServiceに書き換えて同じ動作を行います。
いい感じに見える(; ・`д・´)
ただNavigationスタック上にはPageは一つしかないはずなのにデバッグメッセージは2行必ずでてきちゃう。
おそらくどこかでXFが参照を持っていると思われるのだけれども深追いはしないです。
最後に
一応こんな感じでWeakReferenceを使用したReactivePropertyでModel層を作る事でリークを解決できるかもしれないです。
ReactivePropertyの有識者の方、Xamarin.Formsチョットデキル以上な方で、この記事を読んでくれた人がいましたら
問題点とかおかしい点とか指摘してもらえると助かります。
よろしくお願いします。
ではでは(‘ω’)ノ
まどすた #2で登壇してきました
ごあいさつ
まだまだ部屋に未開封の段ボールがあります(´・ω・`)
冷蔵庫と電子レンジがありません(´・ω・`)
まだリビングテーブルも無いのでご飯食べるのがちょっと大変です(´・ω・`)
通勤電車はまだまだ慣れません(´・ω・`)
登壇してきました
どこで?
VS2017記念でC#ユーザ会との合同勉強会というイベントにて登壇させていただきました。
登壇まで
人前で話すなんて大学の卒論の発表ぶりくらいでパワポ資料つくって話すなんてことも仕事でも全然経験なくて本当に初登壇って感じでした。
引っ越しなどでゴタゴタしてパワポ資料ができたりしたのが直前になってしまったのですが岩永さんに資料の添削などを手伝っていただいて何とか登壇することができました。
ありがとうございました。
内容
タイトルと中身に少し違いがあるのですが要約すると
- 現在の.NET Frameworkでの開発環境はマルチプラットフォームを通り越してマルチタイプ開発になりつつある。
マルチタイプ
そんな環境の中でコード共有化率の向上・移植性の向上を目指すにはモデル層の設計が重要
モデル層はUI要素を考えないアプリケーションの核である
というような内容です。
以前からモヤモヤと頭の片隅で考えていた事で
今回登壇をすることによってそれを言語化できたことが凄く自分の為にもよかったです。
当日
緊張でガチガチになってしまう事はなかったんですが
話している最中に自分で何をしゃべっているのかよくわからなくなってしまっていました。
あと時間とかも考える余裕が全くなかったです。
今後
今後も話したい内容があって話す機会があれば登壇していければと思っています。
それではまた( `ー´)ノ
Xamarin.FormsでUnity.Configurationしてみた。
はじめに
引越しつらい現実逃避で記事書きました/(^o^)\
Unity
ゲーム作る方のじゃなくてDIとかする方のUnityのお話です。
現在、開発が止まって放置されているらしくて今後メンテの期待薄なので今更Unityのネタを書こうか迷ったんですがとりあえず書いてみることにしました。
UnityContainerの拡張メソッドにLoadConfigurationってあるのご存知ですか?
app.Configとかweb.configにContainerへのレジストを記述してLoadConfigurationすると、その設定どおりにContainerに登録を行ってくれます。
これ.NET45なプロジェクトだと使えるんだけどXamarinとかPCLでは使えないんです。
どうしてかというとSystem.Cofigurationに依存している拡張なのでSystem.Configurationが基本的に使えない環境を外してるんですね。
それを今回はPCLプロジェクトでもある程度使ってみるサンプルを作りました。
Xamarin.Formsへの対応
Windows8.1とかは対象外です。今回はAndroidで動作確認しています。たぶんiOSでも動くはずです。Macを既に梱包しちゃったので試せません。
UWPは頑張れば対応できそうなんですが今回は見送りました。
解説
プロジェクトの作成
まずはプロジェクトの作成を行います。
Xamarin.FormsプロジェクトをPCLにて作成してください。
PCLを.NET Standardに対応
こちらの記事を参考にPCLプロジェクトを.NET Standardに対応させてください。
.NET Standardのバージョンは1.5としてください。
Unity.Configurationの追加
UnityのConfiguration機能をPCLで使用するためにUnity,Configurationというパッケージを使用します。
ソリューションのnugetマネージャーを開いてUnity.Configurationというパッケージを追加します。
Pre版ですので「プレリリースを含める」にチェックを入れて検索してください。
テスト動作用のインターフェースと実装クラスを追加します。
めんどくさいのでこんな感じで。
namespace App4 { public interface IHogeHoge { string GetHoge(); string GetHogeHoge(); } public class HogeHogeA : IHogeHoge { public string GetHoge() => "HogeA"; public string GetHogeHoge()=> "HogeHogeA"; } }
PCLにconfigファイルを追加します。
unity.configという名前でconfigファイルを追加します。
ビルドアクションは埋込リソースにしてください。
ファイルの中はこんな感じで。
<unity xmlns="http://schemas.microsoft.com/practices/2010/unity"> <!-- Define Assemblies--> <assembly name="App4" /> <!-- End Assemblies--> <!-- Define Namespaces--> <namespace name="App4" /> <!-- End Namespaces--> <container> <register type="App4.IHogeHoge, App4" mapTo="App4.HogeHogeA, App4" /> </container> </unity>
他にも色々とできますがUnity.Configurationの使い方は別途ぐぐってください。
ConfigurationSectionReaderクラスを追加する
c# - How to Read a Configuration Section from XML in a Database? - Stack Overflow
こちらを参考にしました(コピペ)
こんな感じのクラスを追加しておきます。
public class ConfigurationSectionReader<T> where T : ConfigurationSection, new() { public T GetSection(string sectionXml) { T section = new T(); using (StringReader stringReader = new StringReader(sectionXml)) using (XmlReader reader = XmlReader.Create(stringReader, new XmlReaderSettings() { CloseInput = true })) { reader.Read(); section.GetType().GetMethod("DeserializeElement", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(section, new object[] { reader, true }); } return section; } }
LoadConfigurationする
appクラスにてcontainerを作ってLoadConfigurationします。
public App() { InitializeComponent(); IUnityContainer container = null; var assembly = typeof(App).GetTypeInfo().Assembly; using (var stream = assembly.GetManifestResourceStream("App4.unity.config")) { using (TextReader reader = new StreamReader(stream)) { var configtext = reader.ReadToEnd(); var configreader = new ConfigurationSectionReader<UnityConfigurationSection>(); var sect = configreader.GetSection(configtext); container = new UnityContainer(); container.LoadConfiguration(sect); } } MainPage = new App4.MainPage(container.Resolve<IHogeHoge>().GetHogeHoge()); }
結果
おわりに
Unity.Configurationライブラリに関しては余裕ができたらもう少し真面目に取り組んでみようかと思います。
ではでは( `ー´)ノ