読者です 読者をやめる 読者になる 読者になる

かずきのBlog@hatena

XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

Xamarin AndroidでRelativeLayoutを見てみよう

Xamarin Android

過去記事

RelativeLayout

他のコントロールや親のパネルから見て相対的にどういう位置に表示するかといった指定方法で並べるレイアウトになります。親からの相対位置の指定方法は以下のものがあります。

  • layout_alignParentLeft:親の左側
  • layout_alignParentRight:親の右側
  • layout_alignParentTop:親の上側
  • layout_alignParentBottom:親の下側
  • layout_centerInParent:親の中央

以下のようなaxmlを記述すると

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/MyButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Center" />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button1"
        android:text="Top|Left"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true" />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button2"
        android:text="Bottom|Right"
        android:layout_alignParentBottom="true"
        android:layout_alignParentRight="true" />
</RelativeLayout>

以下のように表示されます。

f:id:okazuki:20160821193737p:plain

他の要素から相対的に位置を設定するには以下の属性を指定します。属性の値は相対的に配置したい元になるコントロールのID(@id/XXXX)になります。

  • layout_toLeftOf:左に配置する
  • layout_toRightOf:右に配置する
  • layout_above:上に配置する
  • layout_below:下に配置する
  • layout_alignLeft:左端に位置を合わせる
  • layout_alignRight:右端に位置を合わせる
  • layout_alignTop:上端に位置を合わせる
  • layout_alignBottom:下端に位置を合わせる

全組み合わせは試しませんが、以下のようなaxmlで

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/MyButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Center" />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button1"
        android:text="Right|above"
        android:layout_toRightOf="@id/MyButton"
        android:layout_above="@id/MyButton" />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button2"
        android:text="Left|below"
        android:layout_toLeftOf="@id/MyButton"
        android:layout_below="@id/MyButton" />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button3"
        android:text="alignLeft|below"
        android:layout_alignLeft="@id/MyButton"
        android:layout_below="@id/MyButton" />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button4"
        android:text="alignRight|above"
        android:layout_alignRight="@id/MyButton"
        android:layout_above="@id/MyButton" />
</RelativeLayout>

以下のように表示されます。

f:id:okazuki:20160821193844p:plain

Xamarin AndroidでLinearLayoutを見てみよう

Xamarin Android

過去記事

LinearLayout

要素を縦と横に並べることができるレイアウトです。android:layout_width、android:layout_height、andorid:layout_gravity、android:layout_weightを使ってレイアウト内のコントロールの表示を制御できます。 android:layout_widthとandroid:layout_heightは10dpのように数値で指定することもできますし、wrap_contentかmatch_parentと指定することが出来ます。wrap_contentでは、表示サイズは、コントロール内のコンテンツの大きさによって決まります。match_parentは、親要素いっぱいにひろがります。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/MyButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/Hello" />
</LinearLayout>

上記のようなaxmlではボタンの横幅が親要素いっぱいに表示され、縦幅がコンテンツの内容の高さになります。LinearLayoutのandroid:orientationでvertical、horizontalで縦並びか横並びを指定できます。つまり、以下のようにもう1つボタンを置いたとすると縦に2個並びます。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
  <Button
      android:id="@+id/MyButton"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="@string/Hello" />
  <Button
      android:id="@+id/MyButton2"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="@string/Hello" />
</LinearLayout>

以下のように表示されます。

f:id:okazuki:20160821154833p:plain

android:layout_gravity属性を指定すると、上下左右どちらに寄せるのか中央寄せにするのかなどが指定できます。例えば、以下のようにボタンの幅をwrap_contentにしてandroid:layout_gravityをleftとrightに指定すると左寄せ(デフォルト)と右寄せになります。axmlを以下に示します。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/MyButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/Hello"
        android:gravity="left" />
    <Button
        android:id="@+id/MyButton2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/Hello"
        android:layout_gravity="right" />
</LinearLayout>

表示は以下のようになります。

f:id:okazuki:20160821154909p:plain

android:layout_weightを使うと、レイアウトで余った余白部分を、どのような比率で分け合うかということが指定できます。例えば、ボタンが3つあって2つ目のボタンを画面の余白いっぱいに表示したい場合は以下のようなaxmlになります。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
  <Button
      android:id="@+id/MyButton"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="@string/Hello"/>
  <Button
      android:id="@+id/MyButton2"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="@string/Hello"
      android:layout_weight="1"/>
  <Button
      android:id="@+id/MyButton3"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="@string/Hello" />
</LinearLayout>

表示は以下のようになります。

f:id:okazuki:20160821154945p:plain

例えば、2つ目と3つ目を、残りの余白を均等に分け合いたいといった場合には、android:layout_heightを0dpに指定して、android:layout_weightに同じ値を指定することで実現できます。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/MyButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/Hello" />
    <Button
        android:id="@+id/MyButton2"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:text="@string/Hello"
        android:layout_weight="1" />
    <Button
        android:id="@+id/MyButton3"
        android:layout_width="match_parent"
        android:layout_height="0od"
        android:text="@string/Hello"
        android:layout_weight="1"/>
</LinearLayout>

表示は以下のようになります。

f:id:okazuki:20160821155033p:plain

Xamarin AndroidでActivityにライフサイクルを確認してみた

Xamarin Android

過去記事

Activityのライフサイクル

Activityのライフサイクルについて説明します。Activityのライフサイクルで呼び出されるメソッドは以下の7つがあります。があります。

  • OnCreateメソッド: Activityが作成されたときに呼び出される。
  • OnStartメソッド: アクティビティがユーザーに表示される直前に呼び出される。
  • OnResumeメソッド: アクティビティがユーザーとの操作が開始される直前に呼び出される。
  • OnPauseメソッド: 別のアクティビティを表示する直前に呼び出される
  • OnStopメソッド: アクティビティがユーザーから見えなくなると表示される
  • OnDestroyメソッド: アクティビティが破棄される前に呼び出される
  • OnRestartメソッド: アクティビティが停止したあと再開する直前に呼び出される

通常は、OnCreateメソッドで初期化処理を行い、OnPauseメソッドで未保存の永続化データを保存すると良いでしょう。別のActivityが表示されると、もともと表示されていたActivityは、通常OnPause→OnStopが呼び出されて次の表示が来るまで待ちますが、メモリが圧迫されたりするとActivityが破棄されたりします。そうなると、次に戻ってきたときはOnCreateからやり直しになります。その時、EditTextなどの入力途中のデータなどは、IDが振られていると、自動的に復元されます。その他のユーザーがActivityのフィールドなどに保持していたデータは何もしないとクリアされてしまいます。これに対応するためには、OnSaveInstanceStateメソッドをオーバーライドして、Bundleにデータを保存します。OnCreateの引数で渡されるBundleがnullじゃないときは、保存されたデータがあるということなので、データの復元をBundleから行います。 この動作を確認するためのプログラムを以下に示します。MainActivityとNextActivityを持っただけのシンプルなプロジェクトで、以下のようなコードを記述します。

using Android.App;
using Android.Content;
using Android.OS;
using Android.Util;
using Android.Views;
using Java.Interop;
using System;

namespace ActivityLifecycle
{
    [Activity(Label = "ActivityLifecycle", MainLauncher = true, Icon = "@drawable/icon")]
    public class MainActivity : Activity
    {
        private string Id { get; set; }

        protected override void OnSaveInstanceState(Bundle outState)
        {
            base.OnSaveInstanceState(outState);
            Log.Debug(nameof(MainActivity), $"{nameof(OnSaveInstanceState)}: {this.Id}");
            outState.PutString(nameof(Id), this.Id);
        }

        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);

            // Set our view from the "main" layout resource
            this.SetContentView(Resource.Layout.Main);

            if (bundle == null)
            {
                this.Id = Guid.NewGuid().ToString();
            }
            else
            {
                this.Id = bundle.GetString(nameof(Id));
            }
            Log.Debug(nameof(MainActivity), $"{nameof(OnCreate)}: {this.Id}");
        }

        protected override void OnStart()
        {
            base.OnStart();
            Log.Debug(nameof(MainActivity), $"{nameof(OnStart)}: {this.Id}");
        }

        protected override void OnResume()
        {
            base.OnResume();
            Log.Debug(nameof(MainActivity), $"{nameof(OnResume)}: {this.Id}");
        }

        protected override void OnRestart()
        {
            base.OnRestart();
            Log.Debug(nameof(MainActivity), $"{nameof(OnRestart)}: {this.Id}");
        }

        protected override void OnPause()
        {
            base.OnPause();
            Log.Debug(nameof(MainActivity), $"{nameof(OnPause)}: {this.Id}");
        }

        protected override void OnStop()
        {
            base.OnStop();
            Log.Debug(nameof(MainActivity), $"{nameof(OnStop)}: {this.Id}");
        }

        protected override void OnDestroy()
        {
            base.OnDestroy();
            Log.Debug(nameof(MainActivity), $"{nameof(OnDestroy)}: {this.Id}");
        }

        [Export(nameof(MyButtonClick))]
        public void MyButtonClick(View v)
        {
            this.StartActivity(new Intent(this, typeof(NextActivity)));
        }
    }
}

ポイントは、OnSaveInstanceStateメソッドとOnCreateメソッドになります。それ以外はボタンが押されたときの画面遷移の処理と、各ライフサイクルメソッドが呼び出されたかログを出力するためのものになります。アプリケーションを実行してMainActivityを表示するとログは以下のようなものが表示されます。

08-20 15:36:59.111 D/MainActivity( 2702): OnCreate: 6986edf6-5759-46a9-98be-4f9185ad23de
08-20 15:36:59.116 D/MainActivity( 2702): OnStart: 6986edf6-5759-46a9-98be-4f9185ad23de
08-20 15:36:59.117 D/MainActivity( 2702): OnResume: 6986edf6-5759-46a9-98be-4f9185ad23de

そして、NextActivityへ遷移すると以下のように表示されます。

08-20 15:37:51.231 D/MainActivity( 2702): OnPause: 6986edf6-5759-46a9-98be-4f9185ad23de
08-20 15:37:52.000 D/MainActivity( 2702): OnSaveInstanceState: 6986edf6-5759-46a9-98be-4f9185ad23de
08-20 15:37:52.001 D/MainActivity( 2702): OnStop: 6986edf6-5759-46a9-98be-4f9185ad23de

そして、戻るボタンでMainActivityに戻ると以下のように表示されます。

08-20 15:39:41.493 D/MainActivity( 2702): OnRestart: 6986edf6-5759-46a9-98be-4f9185ad23de
08-20 15:39:41.493 D/MainActivity( 2702): OnStart: 6986edf6-5759-46a9-98be-4f9185ad23de
08-20 15:39:41.493 D/MainActivity( 2702): OnResume: 6986edf6-5759-46a9-98be-4f9185ad23de

OnDestroyメソッドが呼び出されていないので、Idは保持されたままなのは当然ですよね。次に、実機の開発者オプションで「アクティビティを保持しない」を選択して、疑似的にメモリ不足などでActivityが破棄された時の動作をエミュレートしてみたいと思います。実行してMainActivityが表示されると以下のようなログが出ます。

08-20 15:42:38.221 D/MainActivity(31579): OnCreate: dbc9cedb-a67a-4e61-b6b1-911925fbd001
08-20 15:42:38.223 D/MainActivity(31579): OnStart: dbc9cedb-a67a-4e61-b6b1-911925fbd001
08-20 15:42:38.225 D/MainActivity(31579): OnResume: dbc9cedb-a67a-4e61-b6b1-911925fbd001

そして、NextActivityへ遷移を行います。

08-20 15:43:10.217 D/MainActivity(31579): OnPause: dbc9cedb-a67a-4e61-b6b1-911925fbd001
08-20 15:43:10.667 D/MainActivity(31579): OnSaveInstanceState: dbc9cedb-a67a-4e61-b6b1-911925fbd001
08-20 15:43:10.673 D/MainActivity(31579): OnStop: dbc9cedb-a67a-4e61-b6b1-911925fbd001
08-20 15:43:10.689 D/MainActivity(31579): OnDestroy: dbc9cedb-a67a-4e61-b6b1-911925fbd001

OnDestroyメソッドが呼び出されてMainActivityが破棄されたことが確認できます。この状態で戻るボタンを押してMainActivityに戻ると以下のように表示されます。

08-20 15:44:12.607 D/MainActivity(31579): OnCreate: dbc9cedb-a67a-4e61-b6b1-911925fbd001
08-20 15:44:12.609 D/MainActivity(31579): OnStart: dbc9cedb-a67a-4e61-b6b1-911925fbd001
08-20 15:44:12.624 D/MainActivity(31579): OnResume: dbc9cedb-a67a-4e61-b6b1-911925fbd001

一度MainActivityが破棄されたにも関わらず、Idの値が保持されていることが確認できます。繰り返しますが、画面に置かれた部品でIDが振られたものは自動的に値が保持されるため気にする必要はありませんが、今回のように自分でActivityに一時的に保持しているものは自前で保存と復元を行う必要があります。

Xamarin.Androidで画面遷移してみよう

Xamarin Android

過去記事

画面遷移してみよう

ここでは、Andoridの画面遷移について説明します。Andoridでは、画面遷移にIntentというものを使います。このIntentは、とても汎用的なメッセージング機構でサービスとよばれるバックグラウンドで実行される処理の起動や、ここで説明する別画面(Activity)の起動などができます。さらには、別アプリのActivityやサービスなども起動することもできます。Intentは、簡単に言うと宛先とデータを持った入れ物です。それを投げつけると、それに応答するように設定されたものが応答します。 では、画面遷移するアプリケーションを作ってIntentの簡単な使い方を見てみたいと思います。NavigationAppという名前でAndroidのBlank Appを作成します。そして、新規作成から、ActivityをNextActivityという名前で作成します。NextActivityに対応する見た目を定義するaxmlをResources/layoutフォルダにNext.axmlという名前で作成します。 Main.axmlにEditTextを追加します。idは@+id/GreetMessageにしました。axmlを以下に示します。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:minWidth="25px"
    android:minHeight="25px">
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/GreetMessage" />
</LinearLayout>

メニューの定義

ここで、画面遷移のほかに新しいことをやってみたいと思います。メニューをアクションバーに追加してみたいと思います。メニューは、ActivityのOnCreateOptionsMenuメソッドをオーバーライドしてMenuInflaterのInflateメソッドを使うことで作成します。メニューは、Resources/menuフォルダにXMLで定義します。ここでは、MainMenu.xmlという名前で以下のようなXMLを定義しました。

<?xml version="1.0" encoding="utf-8" ?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:id="@+id/NavigateMenu"
        android:title="@string/NavigateMenuText" 
        android:onClick="NavigateMenuClick"
        android:showAsAction="always"
        android:icon="@drawable/Icon"/>
</menu>

android:idに識別用のIDを定義し、android:titleにメニューを長押ししたときに表示されるテキストを指定し、android:onClickに選択時に実行されるメソッドを指定し、android:showAsActionにアクションバーへの表示方法を指定し、android:iconにアイコンを指定します。android:showAsActionには、バーには表示しないnever、余裕があればバーに表示するifRoom、常に表示するalways、android:titleのテキストを表示するwithTextなどがあります。その他の完全な属性の定義については、以下のページを参照してください。

itemは複数定義することが出来ます。また、menu/menu/itemのようにメニューにメニューを入れ子にすることもできます。 上記メニューを画面に表示します。MainActivityのOnCreateOptionsMenuでMenuInflaterのInflateメソッドでメニューのリソースと引数で渡されたIMenuを結びつけます。そして、NavigateMenuClickメソッドを定義して選択時のアクションを定義します。NavigateMenuClickは、Java側にメソッドを教えるためにMono.Android.dllアセンブリ(追加で参照が必要)に定義されているJava.Interop.Export属性で名前を指定します。コードは以下のようになります。

using System;
using Android.App;
using Android.Content;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Android.OS;
using Android.Util;
using Java.Interop;

namespace NavigationApp
{
    [Activity(Label = "NavigationApp", MainLauncher = true, Icon = "@drawable/icon")]
    public class MainActivity : Activity
    {
        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);

            // Set our view from the "main" layout resource
            this.SetContentView(Resource.Layout.Main);
        }

        public override bool OnCreateOptionsMenu(IMenu menu)
        {
            this.MenuInflater.Inflate(Resource.Menu.MainMenu, menu);
            return true;
        }

        [Export(nameof(NavigateMenuClick))]
        public void NavigateMenuClick(IMenuItem menuItem)
        {
            Log.Debug("MainActivity", $"Clicked: {menuItem.TitleFormatted}");
        }
    }
}

これで、実行すると以下のような画面が表示され、アクションバーのメニューを選択することで出力ウィンドウにメッセージが表示されます。

f:id:okazuki:20160819220438p:plain

画面遷移

メニューを選択したときに画面遷移を行うようにします。以下のようにNextActivityを対象としたIntentを作成して、PutExtraメソッドでIntentにデータを設定してStartActivityメソッドで次の画面を起動します。

[Export(nameof(NavigateMenuClick))]
public void NavigateMenuClick(IMenuItem menuItem)
{
    // NextActivityへのIntentを作成して
    var intent = new Intent(this, typeof(NextActivity));
    // データを詰めて
    intent.PutExtra("Message", this.FindViewById<EditText>(Resource.Id.GreetMessage).Text);

    this.StartActivity(intent);
}

NextActivity用の画面であるNext.axmlではTextViewを1つおいて受け取ったメッセージを表示したいと思います。axmlの内容を以下に示します。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:minWidth="25px"
    android:minHeight="25px">
    <TextView
        android:text="Text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/TextViewGreetMessage" />
</LinearLayout>

画面遷移先のNextActivityでは、Intentプロパティを通じて渡されたIntentが参照できます。ここからGetStringExtraメソッドで渡された値を参照してTextViewに設定しています。

using Android.App;
using Android.OS;
using Android.Widget;

namespace NavigationApp
{
    [Activity(Label = "NextActivity")]
    public class NextActivity : Activity
    {
        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);
            this.SetContentView(Resource.Layout.Next);

            var textView = this.FindViewById<TextView>(Resource.Id.TextViewGreetMessage);
            textView.Text = this.Intent.GetStringExtra("Message");
        }
    }
}

実行すると、以下のようになります。

f:id:okazuki:20160819220537p:plain

f:id:okazuki:20160819220547p:plain

画面遷移が行われて、値が渡されていることが確認できます。

Xamarin.AndroidでHello world

Xamarin Android

ハローワールドを通じて、簡単なアプリケーションの開発の流れを見てみようと思います。Visual Studioのプロジェクトの新規作成から「Android」→「Blank App(Android)」を選択します。

f:id:okazuki:20160817211930p:plain

「HelloWorld」とプロジェクト名をつけてプロジェクトを作成します。そうすると、以下のような構造をもったプロジェクトが作成されます。

f:id:okazuki:20160817211948p:plain

MainActivity.csがメインの画面を表すクラスになります。Main.axmlが、MainActivityで使用されている画面のレイアウトを定義したファイルになります。Strings.xmlが、アプリ内で使用する文字列を定義したファイルになります。Resource.Designer.csファイルはMain.axmlやStrings.xmlなどから自動生成される、プログラム内から各種リソースにアクセスするためのIDが定義されたクラスになります。MainActivityクラスは以下のようになっています。

using System;
using Android.App;
using Android.Content;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Android.OS;

namespace HelloWorld
{
    [Activity(Label = "HelloWorld", MainLauncher = true, Icon = "@drawable/icon")]
    public class MainActivity : Activity
    {
        int count = 1;

        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);

            // Set our view from the "main" layout resource
            SetContentView(Resource.Layout.Main);

            // Get our button from the layout resource,
            // and attach an event to it
            Button button = FindViewById<Button>(Resource.Id.MyButton);

            button.Click += delegate { button.Text = string.Format("{0} clicks!", count++); };
        }
    }
}

まず、ポイントとなるのがActivityクラスを継承している点です。このクラスを継承したものがAndroidでは画面として使えます。次に、このクラスが初期起動時に呼び出されるActivityであることを定義するために属性が定義されています。 [Activity(Label = "HelloWorld", MainLauncher = true, Icon = "@drawable/icon")] MainLauncher = trueの記述がそれになります。LabelはラベルでIconはアイコンです。Activityが生成されたときにフレームワークから呼び出されるOnCreateメソッドで初期化処理が行われています。SetContentViewメソッドが画面定義ファイルとActivityの紐づけを行うメソッドになります。Resourceクラスが、先ほど説明したResource.Designer.csファイルで自動生成されたファイルでMain.axmlを指し示すためのIDにResource.Layout.Mainでアクセス出来るようになっています。これでMain.axmlで定義された見た目が使用されるようになります。Main.axmlは、以下のように定義されています。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/MyButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/Hello" />
</LinearLayout>

xmlns:android=”http://schemas.android.com/apk/res/android”名前空間が定義されたXMLファイルになります。LinearLayoutという要素を縦や横に並べるレイアウトの中に、Buttonが置かれているというくらいの理解で今は大丈夫(というかこれを書いてる時点で、その程度の理解です)だと思います。ポイントとして、押さえておきたいのが、Buttonに対して定義されているandroid:id=”@+id/MyButton”という属性です。これはMyButtonという名前のIDをボタンに割り当ててることになります。 ではMainActivityに戻ります。MainActivityでは、FindViewByIdメソッドを使って画面内のコントロールにアクセス出来ます。ここでは、MyButtonというIDのコントロール(先ほど確認したMain.axml内で定義されてるやつですね)を取得してイベントを登録しています。処理自体はcount変数をカウントアップしているだけのシンプルなものですね。では、実行してみましょう。

f:id:okazuki:20160817212051p:plain

ボタンを押すとカウントアップされた値が表示されます。

f:id:okazuki:20160817212112p:plain

BMIを計算してみよう

これを少しいじってBMIを計算するアプリを作ってみようと思います。身長と体重を入力するボックスを作ります。Main.axmlを以下のように変更します。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/MyButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/Hello" />
    <EditText
        android:id="@+id/EditTextHeight"
        android:hint="@string/Height"
        android:inputType="numberDecimal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <EditText
        android:id="@+id/EditTextWeight"
        android:hint="@string/Weight"
        android:inputType="numberDecimal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

EditTextがAndroidのテキスト入力用コントロールになります。android:hint属性は、ウォーターマークをだすための属性になります。@string/Heightや@string/Weightは、strings.xmlで以下のように定義している文字列を参照する書き方です。BmiMessageは、プログラム内から使用する予定の文字列になります。

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="Hello">Hello World, Click Me!</string>
  <string name="ApplicationName">HelloWorld</string>
  <string name="Weight">体重</string>
  <string name="Height">身長</string>
  <string name="BmiMessage">BMIは%fです</string>
</resources>

android:inputType=”numberDecimal”は、数値を入力するための設定になります。android:layout_widthとandorid:layout_heightは、幅と高さをどうするかを指定します。今回は横幅は親に合わせて、縦幅はコンテンツに合わせるように指定しています。(つまり横長) 実行すると、以下のように表示されます。

f:id:okazuki:20160817212205p:plain

身長、体重のウォーターマークが出ている点と、入力欄にフォーカスを合わせたときのソフトウェアキーボードが数字になっているので、先ほどの設定が効いていることがわかります。 最後に、処理を書いていきます。BMIの計算式は「体重(kg) ÷ {身長(m) X 身長(m)}」なので、ボタンが押されたときに、そう表示されるようにしています。アプリの仕様的にわかりにくいですが、今回は体重はkgで、身長はmで入力されてるものとして処理しています。

using Android.App;
using Android.OS;
using Android.Widget;

namespace HelloWorld
{
    [Activity(Label = "HelloWorld", MainLauncher = true, Icon = "@drawable/icon")]
    public class MainActivity : Activity
    {
        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);

            this.SetContentView(Resource.Layout.Main);

            var button = this.FindViewById<Button>(Resource.Id.MyButton);
            var height = this.FindViewById<EditText>(Resource.Id.EditTextHeight);
            var weight = this.FindViewById<EditText>(Resource.Id.EditTextWeight);

            button.Click += (_, __) =>
            {
                double h;
                if (!double.TryParse(height.Text, out h))
                {
                    return;
                }

                double w;
                if (!double.TryParse(weight.Text, out w))
                {
                    return;
                }

                // 体重(kg) ÷ {身長(m) X 身長(m)}
                var bmi = w / (h * h);
                button.Text = string.Format(this.Resources.GetString(Resource.String.BmiMessage ,bmi));
            };
        }
    }
}

ActivityにResourcesというプロパティがあり、これのGetStringメソッドを使うことでStrings.xmlに定義した文字列をプログラム中から使うことが出来ます。今回はBMIの計算結果を表示する文字列として使っています。 実行して入力してボタンを押すと以下のようになります。

f:id:okazuki:20160817212237p:plain

Androidで定期的に処理を実行したい

Android

そんなときはAlarmManagerを使います。 使い方はPendingIntentを作ってAlarmManagerを取得してsetRepeatingする感じ。 一回こっきりでいい場合は、setメソッドでもいいみたいですね。

詳細はAlarmManagerのドキュメントを見よう。

AlarmManager | Android Developers

ということで、こんなコードを書いてやれば現在時間から1分間隔でIntentが投げられるようになります。今回の場合はサービスをキックしています。

Intent intent = new Intent(this, LogService.class);
intent.setAction("LogServiceAction");
PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

AlarmManager am = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE);
am.setRepeating(AlarmManager.RTC_WAKEUP, Calendar.getInstance().getTimeInMillis(), 60 * 1000, pendingIntent);

Activityでやれば、アプリを起動したあとから有効になります。端末再起動時や、アプリアップデート時にリセットされるらしいので、それがいやならしかるべきIntentを受け取るReceiverを作ってそこで上記処理を書けばいいっぽいです。

ふむふむ。

Android 6.0でGeofencingをしてみた

Android

Android 6.0とかパーミッションまわりがめんどくさいんですね…。 ということで自分用メモです。

まず、permissionを設定します。AndoridManifest.xmlに以下の定義を追加。

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

追加するのはmanifestタグの直下ですね。

gms使うっぽいので、build.gradle(Module: app)に依存関係を追加します。

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:24.1.1'
    compile 'com.google.android.gms:play-services:9.4.0'
}

一番最後の依存関係ですね。

今回はサービスで色々ごにょごにょやりたかったので、GeofenceTransitionsIntentServiceという名前でサービスを1つ作りました。こんな感じで。

package com.example.kazuki.myapplication;

import android.*;
import android.app.IntentService;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Location;
import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;

import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.location.Geofence;
import com.google.android.gms.location.GeofencingEvent;
import com.google.android.gms.location.GeofencingRequest;
import com.google.android.gms.location.LocationServices;

import java.util.LinkedList;
import java.util.List;

public class GeofenceTransitionsIntentService extends Service implements GoogleApiClient.ConnectionCallbacks{
    private List<Geofence> geofences = new LinkedList<>();
    private GoogleApiClient client;

    @Override
    public void onCreate() {
        super.onCreate();
        client = new GoogleApiClient.Builder(this)
                .addApi(LocationServices.API)
                .addConnectionCallbacks(this)
                .build();
        client.connect();
        Log.d("Sample", "onCreate");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        client.disconnect();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            return super.onStartCommand(intent, flags, startId);
        }
        Location lastLocation = LocationServices.FusedLocationApi.getLastLocation(this.client);
        if (lastLocation == null) {
            Log.d("Sample", "lastLocation is null");
        } else {
            float[] distances = new float[3];
            Location.distanceBetween(34.38511, 132.4539817, lastLocation.getLatitude(), lastLocation.getLongitude(), distances);
            Log.d("Sample", "lastLocation: " + lastLocation.getLatitude() + ", " + lastLocation.getLongitude() + ", " + distances[0] + "m");
        }
        GeofencingEvent e = GeofencingEvent.fromIntent(intent);
        if (e == null || e.getTriggeringLocation() == null) {
            Log.d("Sample", "GeofencingEvent#getTriggeringLocation is null");
            return super.onStartCommand(intent, flags, startId);
        }

        String reason = "";
        if (e.getGeofenceTransition() == Geofence.GEOFENCE_TRANSITION_ENTER) {
            reason = "ENTER";
        } else if (e.getGeofenceTransition() == Geofence.GEOFENCE_TRANSITION_EXIT) {
            reason = "EXIT";
        } else if (e.getGeofenceTransition() == Geofence.GEOFENCE_TRANSITION_DWELL) {
            reason = "DWELL";
        }
        Log.d("Sample", reason + ": " + e.getTriggeringLocation().getLatitude() + ", " + e.getTriggeringLocation().getLongitude());
        return super.onStartCommand(intent, flags, startId);
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onConnected(@Nullable Bundle bundle) {
        this.setupGeofence();
    }

    private void setupGeofence() {
        if (!this.geofences.isEmpty()) {
            return;
        }
        if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            return;
        }
        Log.d("Sample", "setupGeofence");
        this.geofences.add(
                new Geofence.Builder()
                        .setRequestId("home")
                        .setCircularRegion(
                                34.38511,
                                132.4539817,
                                1000
                        )
                        .setExpirationDuration(Geofence.NEVER_EXPIRE)
                        .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT)
                        .build()
        );

        GeofencingRequest req = new GeofencingRequest.Builder()
                .setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER | GeofencingRequest.INITIAL_TRIGGER_EXIT | GeofencingRequest.INITIAL_TRIGGER_DWELL)
                .addGeofences(this.geofences)
                .build();

        Intent intent = new Intent(this, GeofenceTransitionsIntentService.class);
        PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        LocationServices.GeofencingApi.addGeofences(
                this.client,
                req,
                pendingIntent
        );

    }

    @Override
    public void onConnectionSuspended(int i) {

    }
}

色々ごにょごにょやってますが、パーミッションのチェックがOKならgeofenceを追加しています。座標は広島市役所に半径1㎞(1000m)のジオフェンスを作ってます。GeofenceingRequestとPendingIntentを作ってaddGeofencesをしています。

最後にMainActivityです。 ここでパーミッションOKしてもらったら、先ほど作ったサービスを起動しています。

package com.example.kazuki.myapplication;

import android.Manifest;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.location.Geofence;
import com.google.android.gms.location.GeofencingRequest;
import com.google.android.gms.location.LocationServices;

import java.util.LinkedList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    public void onStart() {
        super.onStart();
        if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.ACCESS_FINE_LOCATION}, 0);
            return;
        }
        this.startService(new Intent(this, GeofenceTransitionsIntentService.class));
    }

    public void onClick(View v) {
        this.startService(new Intent(this, GeofenceTransitionsIntentService.class));
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == 0) {
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                this.startService(new Intent(this, GeofenceTransitionsIntentService.class));
            }
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

あとは、Fake GPSアプリとか入れて開発者オプションから疑似ロケーションの設定をして広島市役所付近でいじってやればLogにENTERとかEXITとか出るはずです。

ソースコードはGitHubに上げておきました。

github.com

.NET Standard 1.1のプロジェクトをPCL(.NET Standard 1.2相当)から参照すると警告が出てDLLが生成されない?

.NET

先ほどReactiveProperty v3.0.0をリリースしたのですが、PCLから参照できないので困ってます。(Xamarinは.NET Standard化することでお茶を濁してる)

現象としては.NET Standard 1.1のクラスライブラリを作る。 PCL(.net451とwin81をターゲット)から.NET Standard 1.1のライブラリを参照する。

この時点で警告が出ます(警告なのでスルーしても最悪問題ないよね)

そして、それらのライブラリを参照する.NET 4.6.1あたりのコンソールアプリを作って実行しようとするとクラスライブラリ側のDLLが生成されてないっぽいエラーになる。

再現プロジェクトは以下のリポジトリです。

github.com

.NET Standardとかちゃんと勉強してないので、多分私のチョンボな気がするのですが何だろう…。

ReactiveProperty v3.0.0をリリースしました

ReactiveProperty

Reactive Extensions 3.0.0に対応したReactiveProperty v3.0.0をリリースしました。 新機能はありません!

インストールはNuGetからお願いします。

www.nuget.org

Windows 8.1とWindows Phone 8.1をサポート対象から外しました。(Windows 8.1では動くんだけどね)Windows 8.1とWindows Phone 8.1で使いたい人はReactiveProperty v2.x系を使用してください。

あと.NET Standard 1.1で動くようにしました。

めんどくさいことが…

Windows 8.1とWindows Phone 8.1のサポートを外したことで、Xamarin.Formsでデフォルトでプロジェクトを作った状態ではインストールできなくなりました。(Xamarin.FormsのPCLプロジェクトがWindows 8.1とWindows Phone 8.1も対象にしているため) そのため、.NET Standard 1.2にアップグレードしたうえで使っていただく必要があります。

手順は以下のページにまとめてあるので参照してください。

github.com

ただ、Windows Phone 8.1だけDataAnnotationsを使えなかったのでXamarin.Formsでは、これまでDataAnnotationsをサポートしてなかったのですが、めでたくサポートから外したのでDataAnnotationsが使えるようになりました。やったね!

まとめ

ということで、良いリアクティブプログラミングを!

Xamarin.AndroidからJavaのネイティブライブラリ(jarね)を使おう

Xamarin

お題の通りやってみようと思います。

今回挑戦したのは癖のなさそうな、Apache CommonsのLangです。ここからjarをダウンロードしました。

Lang – Download Apache Commons Lang

バージョンは現時点で最新の3.4にしました。

前提知識としてBindings Libraryというものがあるということだけ知ってたので、そのプロジェクトを作ります。

f:id:okazuki:20160809215019p:plain

プロジェクトを新規作成するとJarsという名前の、いかにも怪しそうなフォルダができるのでそこを見てみます。そうすると、AboudJars.txtというテキストがあって、こういうことが書いてあります。

This directory is for Android .jars.

There are 2 types of jars that are supported:

== Input Jar ==

This is the jar that bindings should be generated for.

For example, if you were binding the Google Maps library, this would
be Google's "maps.jar".

Set the build action for these jars in the properties page to "InputJar".


== Reference Jars ==

These are jars that are referenced by the input jar.  C# bindings will
not be created for these jars.  These jars will be used to resolve
types used by the input jar.

NOTE: Do not add "android.jar" as a reference jar.  It will be added automatically
based on the Target Framework selected.

Set the build action for these jars in the properties page to "ReferenceJar".

つまりJarsフォルダあたりにjarファイルつっこんでプロパティウィンドウでInputJarやReferenceJarを設定すればよろしいということっぽいですね。ということで、ダウンロードしたcommons-lang3-3.4.jarをJarsフォルダにコピーして、ビルドアクションをInputJarにします。(というかソリューションエクスプローラーにぽとっと落としたらそうなってました)

ビルドすると、めでたしめでたし…なのかと思ったらコンパイルエラーだらけになりました。

f:id:okazuki:20160809215602p:plain

今日は自動生成はあきらめて、AndroidのプロジェクトにJarを追加して、ビルドアクションをAndroidJavaLibraryに変更して、以下のコードでisEmptyを呼び出せることだけ確認しました。

IntPtr classHandle = IntPtr.Zero;
JNIEnv.FindClass("org/apache/commons/lang3/StringUtils", ref classHandle);
var method = JNIEnv.GetStaticMethodID(classHandle, "isEmpty", "(Ljava/lang/CharSequence;)Z");
string emptyString = "";
var handle = CharSequence.ToLocalJniHandle(emptyString);
try
{
    var result = JNIEnv.CallStaticBooleanMethod(classHandle, method, new JValue(handle));
    button.Text = result.ToString();
}
finally
{
    JNIEnv.DeleteLocalRef(handle);
}

俺たちの戦いは、これからだ!!!