.NET平臺下的Xamarin開發 - Android

       對Android的應用開發,若是熟悉Java,那麼Android studio或Eclipse將是不錯的選擇。而對熟悉.net平臺開發人員,在強大的Visual Studio幫助下,開發Android應用再也不是難題。本文基於Visual Studio 2017及以上的版本討論,若是低於2017的版本,由於xamarin並未集成,須要單獨安裝,因此在搭建開發環境上會有些麻煩。android

      本文假設你有必定的開發經驗,對Android的有基礎的瞭解。假如你還不熟悉,建議先從MSDN上的Hello, Android開始,將是不錯的入門。web

1. 開發環境搭建windows

 在Windows 10,僅須要作下面兩個就足夠了:設計模式

    a. 在Visual studio上開發,須要Mobile development with .NET組件,詳細的過程可參考Installing Xamarin in Visual Studio。也能夠經過Visual studio installer,修改已有的安裝。在最小安裝的狀況下,對components的選擇,須要注意開發不一樣Android版本的應用,其API Level也不同,Android API levels詳見MSDN。若是須要原生支持,那麼NDK也須要一併安裝。api

  

    b. Android Emulator:在模擬器的選擇上,這裏推薦Genymotion,對我的是免費的,資源佔用下,啓動迅速,對調試、可操做性都很是便利。雖然在visual studio的Mobile development with .NET默認安裝狀況下,會有一個hardware accelerated emulator,但,這裏很是不推薦。MSDN上Android Emulator Setup這篇文章提到的模擬器,在硬件不是特別強大的狀況下,都不建議去嘗試。app

       Notes:若是硬件不夠強大,vs自帶的hardware accelerated emulator啓動會很是慢,每次編譯調試會很費時。在T480筆記本上(i5-7300U+16G+SSD),默認的模擬器j僅成功了幾回,後來修改了程序,旋轉了一次模擬器,再啓動就卡在應用加載上,模擬器沒法響應或者沒法加載應用。由於這個,曾一度懷疑是否是程序那裏修改錯了或者開發環境哪裏少了步驟而沒有搭建完成,折騰了近一下午的時間。次日,安裝了Genymotion模擬器,一切都清爽了ide

  Gemymotion模擬器的安裝步驟:佈局

  • 從官網下載後(對首次下載,建議選擇帶有Virtualbox的版本),註冊帳號。由於在安裝完成,啓動該軟件,仍然須要登陸帳號,才能夠建立模擬器。在安裝完成後,能夠看到:

  • 啓動Genymotion, 建立模擬器。以下圖所示,可根據須要建立不一樣Android版本的模擬器:
      

      上面兩步完成後,開發環境就搭建成功了。ui

      啓動新建的Genymotion虛擬設備,打開Android project後,在visual studio的調試設備列表中,默認就是該模擬器,不然將是hardware accelerated emulator。this

  

2. 應用程序

   這裏會有些不一樣於MSDN上的Hello, Android,稍微有些複雜,將從Activity,View(axml),Intent相關點介紹。

   2.1 程序開發 - 應用程序結構及代碼結構:

  

  •  Logon activity & logon view:登陸相關,應用程序啓動後,此爲主activity啓動 一個main activity。其對應的view放在axml文件中
  •  Main activity & view:登陸後的相關操做,此處呈現簡單的click計數器,並提供導航到history activity和返回logon的操做。其對應的view放在axml文件中
  •  History list activity:此activity繼承自Built-in Control ListView, 不單首創建xml結構的view

   初步介紹程序結構後,接下來從建立該程序開始:

   A. 在visual studio中,新建一個Xamarin project

  

 

    B. 在接下來的嚮導中,選擇空白模板。對最小Android版本,其字面直譯,表示該應用程序運行所需的最低版本。根據開發環境搭建步驟a中所選擇安裝的API Level不一樣,該列表呈現的可選版本也不一樣。

  

   C. 完成後,可見到程序默認結構。在Resource/Layout目錄下,Activity_main.axml爲默認的啓動的activity設圖。這裏,將其做爲main activity的視圖(非程序啓動後的第一個頁面)。爲了保持一致,可將其重命名爲其它試圖的activity。爲了簡化,這裏不作更名。

默認的佈局結構爲RelativeLayout, 這裏將其修改成LinearLayout,並設置屬性android:orientation="vertical"縱向線性佈局結構。本axml使用嵌套LinearLayout佈局,

     

完整的代碼以下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:layout_margin="5dip">
    <TextView
        android:id="@+id/form_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/logon_title_tip" />
    <LinearLayout
        android:id="@+id/layout_login_name"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_margin="5.0dip"
        android:layout_marginTop="10.0dip"
        android:orientation="horizontal">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/logon_usr" />
        <EditText
            android:id="@+id/txt_login_name"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:textSize="15.0sp" />
    </LinearLayout>
    <LinearLayout
        android:id="@+id/login_pwd_layout"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/layout_login_name"
        android:layout_centerHorizontal="true"
        android:layout_margin="5.0dip"
        android:orientation="horizontal">
        <TextView
            android:id="@+id/login_pass_edit"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/logon_pwd"
            android:textSize="15.0sp" />
        <EditText
            android:id="@+id/txt_login_pwd"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:password="true"
            android:textSize="15.0sp" />
    </LinearLayout>
    <Button
        android:id="@+id/btn_login"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:text="@string/logon_logonBtnText" />
</LinearLayout>
View Code

   D. Logon對應的Activity, 其默認繼承自AppCompatActivity,且其被ActivityAttribute修飾爲MainLauncher = true。在Android程序中,並無主程序的入口點,理論上,任何一個activity均可以被做爲主入口。在xaml開發中,作了更爲易於理解的標註( AppCompatActivity, MainLauncher = true)。完整的代碼:

   [Activity(Label = "LogonActivity", MainLauncher = true)]
    public class LogonActivity : AppCompatActivity
    {
        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);

            // Create your application here
            SetContentView(Resource.Layout.activity_logon);

            EditText usr = FindViewById<EditText>(Resource.Id.txt_login_name);
            usr.KeyPress += Usr_KeyPress;

            var logonBtn = FindViewById<Button>(Resource.Id.btn_login);
            logonBtn.Click += LogonBtn_Click;

            CreateNotificationChannel();
        }

        private void Usr_KeyPress(object sender, View.KeyEventArgs e)
        {
            e.Handled = false;
            if (e.Event.Action == KeyEventActions.Down && e.KeyCode == Keycode.Enter)
            {
                var msg = FindViewById<EditText>(Resource.Id.txt_login_name).Text;
                Toast.MakeText(this, msg, ToastLength.Short).Show();

                EditText pwdTxt = FindViewById<EditText>(Resource.Id.txt_login_pwd);
                pwdTxt.Text = msg;
                e.Handled = true;

                #region notification

                var builder = new NotificationCompat.Builder(this, "location_notification")
                  .SetAutoCancel(true) // Dismiss the notification from the notification area when the user clicks on it
                  //.SetContentIntent(resultPendingIntent) // Start up this activity when the user clicks the intent.
                  .SetContentTitle("Button Clicked") // Set the title
                  //.SetNumber(count) // Display the count in the Content Info
                  .SetSmallIcon(Resource.Drawable.abc_tab_indicator_mtrl_alpha) // This is the icon to display
                  .SetContentText("只有圖標、標題、內容:" + FindViewById<EditText>(Resource.Id.txt_login_name).Text); // the message to display.

                // Finally, publish the notification:
                var notificationManager = NotificationManagerCompat.From(this);
                notificationManager.Notify(1000, builder.Build());
                #endregion
            }

        }

        private void LogonBtn_Click(object sender, EventArgs e)
        {
            var intent = new Intent(this, typeof(MainActivity));
            intent.PutExtra("username", FindViewById<EditText>(Resource.Id.txt_login_name).Text);
            StartActivity(intent);
        }

        void CreateNotificationChannel()
        {
            //in case API 26 or above
            if (Build.VERSION.SdkInt < BuildVersionCodes.O) return;
      
            var channel = new NotificationChannel("location_notification", "Noti_name", NotificationImportance.Default)
            {
                Description = "Hello description"
            };

            var notificationManager = (NotificationManager)GetSystemService(NotificationService);
            notificationManager.CreateNotificationChannel(channel);
        }
    }
LogonActivity

    這裏,User name的輸入框中,增長了按鍵press down事件,用回車鍵按下後,觸發Toast及通知欄展現(此處僅爲演示用)。對通知欄,在API 26之後,須要首先註冊Channel。

var channel = new NotificationChannel("location_notification", "Noti_name", NotificationImportance.Default)
            {
                Description = "Hello description"
            };

var notificationManager =(NotificationManager)GetSystemService(NotificationService);
            notificationManager.CreateNotificationChannel(channel);
Channel Registration - API 26

   E. 輸入user name後,點擊Logon,跳轉到Main activity頁面。此頁面,Enter code默認呈現user name。在Click me按鈕點擊後,內部計數器增長,消息呈如今Enter code並記錄到Intent中。

      

    axml完整代碼:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
    <TextView
        android:text="Enter code"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:minWidth="25px"
        android:minHeight="25px"
        android:id="@+id/textView1" />
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/editText1" />
    <Button
        android:text="Click ME"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/button1" />
    <Button
        android:text="@string/callhistory"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/callhistoryBtn" 
        android:enabled="false"
    />
    <Button
        android:text="Logout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/switchBtn" />
</LinearLayout>
Main_View axml

   F. Main Activity, 視圖對應的代碼實現

   [Activity(Label = "@string/app_name", Theme = "@style/AppTheme")]
    public class MainActivity:Activity
    {
        static readonly List<string> phoneNumbers = new List<string>();
        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);
            // Set our view from the "main" layout resource
            SetContentView(Resource.Layout.activity_main);

            Button btn = FindViewById<Button>(Resource.Id.button1);
            btn.Click += Btn_Click;

            Button callhis = FindViewById<Button>(Resource.Id.callhistoryBtn);
            callhis.Click += Callhis_Click;

            FindViewById<Button>(Resource.Id.switchBtn).Click+= (obj, e)=> {
                //SetContentView(Resource.Layout.activity_logon);
                StartActivity(typeof(LogonActivity));
            };

            //set user name
            EditText usr = FindViewById<EditText>(Resource.Id.editText1);
            usr.Text = Intent.Extras?.Get("username")?.ToString();
        }

        private void Callhis_Click(object sender, System.EventArgs e)
        {
            var intent = new Intent(this, typeof(CallHistoryActivity));
            intent.PutStringArrayListExtra("phone_numbers", phoneNumbers);
            StartActivity(intent);
        }

        private int counter = 1;
        private void Btn_Click(object sender, System.EventArgs e)
        {
          var cl =  FindViewById<EditText>(Resource.Id.editText1);
          cl.Text = $"your counter is {counter++}";

           phoneNumbers.Add(cl.Text);
           FindViewById<Button>(Resource.Id.callhistoryBtn).Enabled = true;
            
        }
    }
Main Actitity

 對該頁面,當點擊"Click ME"按鈕後,計數器自增,Call History的按鈕可用。當點擊Call History,頁面跳轉到view list頁面,呈現計數器Counter的變化歷史。

    

   G. 在Call History,該activity繼承自ListView,數據源爲計數器Counter的變化歷史記錄。詳細的代碼爲:

   [Activity(Label = "@string/callhistory")]
    public class CallHistoryActivity : ListActivity
    {
        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);

            // Create your application here
            var phoneNumbers = Intent.Extras.GetStringArrayList("phone_numbers") ?? new string[0];
            this.ListAdapter = new ArrayAdapter<string>(this, Android.Resource.Layout.SimpleListItem1, phoneNumbers);
        }
    }
Call History Activity

 除了這裏演示的ListView, 還有LinearLayoutRelativeLayout , TableLayout , RecyclerViewGridViewGridLayoutTabbed Layouts多種佈局構建頁面。

   2.2 程序部署

   和傳統的windows程序有些不太同樣(DEBUG/RELEASE模式下,直接編譯後獲得的爲dll而非.apk文件),在程序須要發佈的時候,在project右鍵或者Tools -> Arhive Manager,能夠看到已經建立的Archive或者新的Archive。

   NOTE:右鍵菜單中的Deploy按鈕,對沒有多少經驗的開發者有些不太友好,在模擬器環境中,一般會報不支持CPU型號的錯誤。這是因爲deploy會依賴Simulation列表設備的選擇。若是是Gemymotion模擬器,基本會失敗。若是鏈接的硬件(usb調試模式下的硬件),會直接部署到對應的設備上

 

 在Archive Manger中,選擇相應的Archive,將其分發到本地或者應用市場:

       

對Ad Hoc選項,能夠建立/選擇已有的簽名,對所要發佈的程序進行簽名。

3. 所涉及的要點

    3.1 Activity & axml

    這裏更多的是從設計的角度考慮,Activity和axml以一對一的形式構建。單從程序實現角度,一個activity可以使用多個axml文件以構建不一樣業務場景的試圖(同一個時刻,content view只會有一個),這種狀況下多個axml的事件或業務,將只能在對應的那個Activity中實現(調用SetContentView的地方)。在設計上,這種很難理解維護,即便以partial這種投機的方式達到可維護性,對OO的設計模式也是一種破壞(或美其名曰反設計模式)。

    3.2 Activity lifecycle

    在Android應用程序中(不像傳統的桌面/web程序,有指定的程序入口點Main),任何activity均可以成爲入口點。在vs中,Xamarin.Android很好的照顧了剛入門的開發人員,將activity及對應的axml文件直接以main關鍵字命名。借用MSDN上的這幅圖,形象生動說明整個actity的生命週期。

      

    對各個關鍵點,提供了相應的重寫方法。如默認的OnCreate, 執行activity啓動以初始化。須要注意,該方法是在OnStart以後執行。

    3.3 Activity之間的數據傳遞

  對於不一樣Activity之間的數據傳遞,Intent類提供了多種方式。對簡單數據類型,調用內置的PutExtra不會有任何問題。對實例對象或複雜對象,須要將其序列化,在取的時候,反序列化便可。

     而對於同一個Activity不一樣的活動期間,則無需這麼複雜,經過Bundle便可。如OnCreate, OnPause等可重寫的方法,經過參數Bundle便可完成生命週期內的數據傳遞。在實際應用中,OnSaveInstanceState在activity被銷燬時保存相應數據或試圖狀態,在恢復的時候,OnRestoreInstanceState是一種選擇,但更多的時候, 經過OnCreate已經足夠。

protected override void OnSaveInstanceState (Bundle outState)
{
  outState.PutString("UsrCfg", MyStringData);
  base.OnSaveInstanceState (outState);
}
OnSaveInstanceState

    3.4 Localization

     如演示程序所示,若是應用須要多語言支持,對本地化策略:

android:text="@string/callhistory"

    以@string或者相似值,將以字面直譯的方式處理,涉及的resource在Resources/Values/xx.axml文件中。好比上述代碼所演示的,具體的resource資源在Resources/Values/string.axml中。

 

對熟悉.NET平臺開發,又想開發Android應用的朋友,但願這篇文章對你有所幫助。

另外,在寫這篇文章2天前,我也沒有相關的Android開發經驗。由於基於項目要求,須要在PDA設備開發相應的程序,因而便有了此文。對於想要了解更詳細的知識點,可詳見Application Fundamentals

相關文章
相關標籤/搜索