.NET平台下的Xamarin開發 - Android


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

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

1. 開發環境搭建

 在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也需要一並安裝。

  

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

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

  Gemymotion模擬器的安裝步驟:

  • 從官網下載后(對首次下載,建議選擇帶有Virtualbox的版本),注冊賬號。因為在安裝完成,啟動該軟件,仍然需要登錄賬號,才可以創建模擬器。在安裝完成后,可以看到:

  • 啟動Genymotion, 創建模擬器。如下圖所示,可根據需要創建不同Android版本的模擬器:
      

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

      啟動新建的Genymotion虛擬設備,打開Android project后,在visual studio的調試設備列表中,默認就是該模擬器,否則將是hardware accelerated emulator。

  

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM