上次使用Xamarin.Android實現了一個比較常用的功能PullToRefresh,詳情見:Xamarin. Android實現下拉刷新功能
這次將實現另外一個手機App中比較常用的功能:側滑菜單。通過搜索,發現有很多側滑菜單,有仿手機QQ的側滑菜單,有折疊的側滑菜單,有SlidingMenu等,不過我還是比較喜歡 ResideMenu實現的效果,所以想通過Xamarin.Android的綁定實現該效果。這次實現該菜單遇到的問題比較多,花的時間也較多,花了三四個晚上才解決所有的問題。下面是詳細的實現步驟:
一、生成ResideMenu.dll
- 從網上下載ResideMenu的源代碼,我是下載的master分支的代碼,如果有需要可以下載其他分支的代碼。
- 導入到MyEclispe中,編譯一下(默認情況導入后會自動編譯)。
- 打開ResideMenu所在的目錄,將res目錄和生成的bin目錄里的內容打包成residemenu.zip。
- 在Visual Studio中新建一個Android Binding 項目,命名為ResideMenuLib。
- 在ResideMenuLib項目的Jars目錄里添加residemenu.zip和nineoldandroids-library-2.4.0.jar(在ResideMenu項目的libs目錄里),將residemenu.zip的生成操作設置為LibraryProjectZip,nineoldandroids-library-2.4.0.jar的生成操作設置為ReferenceJar,注意是ReferenceJar而不是EmbeddedReferenceJar。
- 編譯ResideMenuLib項目。
二、使用ResideMenu
普通方式使用就不貼代碼了,簡單描述一下使用步驟,詳細的代碼請看Mvvmcross中使用ResideMenu
- 在Visual Studio中新建ResideMenuDemo項目。
- 分別添加對ResideMenuLib和NineOldAndroids的引用,NineOldAndroids直接引用Nuget里面的就ok,否則需要重新綁定NineOldAndroids,然后添加引用。
- 將Java的ResideMenuDemo(與ResideMenu在同一目錄)轉換為C#的即可。
- 編譯C#版的ResideMenuDemo,然后運行。
三、MvvmCross中使用ResideMenu
其實在MvvmCross中使用ResideMenu和普通方式使用差不多,只是MvvmCross中需要設置對應的ViewModel。需要注意的是,使用低版本SDK時需要引用Xamarin.Android.Support.v4.dll,下面是具體的步驟:
- 新建一個可以移植的類庫項目MvxResideMenu.Core,通過Nuget添加對MvvmCross的引用
- 添加ViewModel的代碼
- 新建Android項目MvxResideMenu.Droid,刪除自動生成的MainActivity,通過Nuget添加對MvvmCross和NineOldAndroids的引用
- 編寫對應的View和相關布局代碼
- 編譯並運行
下面是代碼:
ViewModel的代碼:

public class BaseViewModel : MvxViewModel { private string _hello = "Hello MvvmCross BaseViewModel"; public string Hello { get { return _hello; } set { _hello = value; RaisePropertyChanged(() => Hello); } } private string _title; public string Title { get { return _title; } set { _title = value; RaisePropertyChanged(() => Title); } } } public class MainViewModel : BaseViewModel { public MainViewModel() { Hello = "Hello MvvmCross MainViewModel"; Title = "MainViewModel"; } } public class FirstViewModel : BaseViewModel { public FirstViewModel() { Hello = "Hello MvvmCross FirstViewModel"; Title = "FirstViewModel"; } } public class SecondViewModel : BaseViewModel { public SecondViewModel() { Hello = "Hello MvvmCross SecondViewModel"; Title = "SecondViewModel"; } } public class ThirdViewModel : BaseViewModel { public ThirdViewModel() { Hello = "Hello MvvmCross ThirdViewModel"; Title = "ThirdViewModel"; } } public class FourthViewModel : BaseViewModel { public FourthViewModel() { Hello = "Hello MvvmCross FourthViewModel"; Title = "FourthViewModel"; } }
View的代碼:

[Activity(Label = "View for MainViewModel")] public class MainView : MvxActivity<MainViewModel>, View.IOnClickListener { private ResideMenu _resideMenu; private ResideMenuItem _firstMenuItem; private ResideMenuItem _secondMenuItem; private ResideMenuItem _thirdMenuItem; private ResideMenuItem _fourthMenuItem; protected override void OnCreate(Bundle bundle) { base.OnCreate(bundle); SetContentView(Resource.Layout.Main); InitMenus(); ChangeFragment(new FirstView() { ViewModel = new FirstViewModel() }); } #region Overrides of Activity public override bool DispatchTouchEvent(MotionEvent ev) { return _resideMenu.DispatchTouchEvent(ev); } #endregion private void InitMenus() { _resideMenu = new ResideMenu(this); _resideMenu.SetBackground(Resource.Drawable.background2); _resideMenu.AttachToActivity(this); _resideMenu.SetScaleValue(0.6f); _firstMenuItem=new ResideMenuItem(this,Resource.Drawable.mail,"First View"); _secondMenuItem=new ResideMenuItem(this,Resource.Drawable.home,"Second View"); _thirdMenuItem=new ResideMenuItem(this,Resource.Drawable.download,"Third View"); _fourthMenuItem=new ResideMenuItem(this,Resource.Drawable.weather,"Fourth View"); _firstMenuItem.SetOnClickListener(this); _secondMenuItem.SetOnClickListener(this); _thirdMenuItem.SetOnClickListener(this); _fourthMenuItem.SetOnClickListener(this); _resideMenu.AddMenuItem(_firstMenuItem, ResideMenu.DirectionLeft); _resideMenu.AddMenuItem(_secondMenuItem, ResideMenu.DirectionLeft); _resideMenu.AddMenuItem(_thirdMenuItem, ResideMenu.DirectionLeft); _resideMenu.AddMenuItem(_fourthMenuItem, ResideMenu.DirectionRight); } private void ChangeFragment(MvxFragment fragment) { _resideMenu.ClearIgnoredViewList(); FragmentManager .BeginTransaction() .Replace(Resource.Id.main_fragment, fragment, "fragment") .SetTransition(FragmentTransit.FragmentFade) .Commit(); ViewModel.Title = (fragment.ViewModel as BaseViewModel).Title; } #region Implementation of IOnClickListener public void OnClick(View v) { if (v == _firstMenuItem) { ChangeFragment(new FirstView(){ViewModel = new FirstViewModel()}); } else if (v == _secondMenuItem) { ChangeFragment(new SecondView() { ViewModel = new SecondViewModel() }); } else if (v == _thirdMenuItem) { ChangeFragment(new ThirdView() { ViewModel = new ThirdViewModel() }); } else if (v == _fourthMenuItem) { ChangeFragment(new FourthView() { ViewModel = new FourthViewModel() }); } _resideMenu.CloseMenu(); } #endregion } public class FirstView : MvxFragment<FirstViewModel> { public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { base.OnCreateView(inflater, container, savedInstanceState); return this.BindingInflate(Resource.Layout.FirstView, null); } } public class SecondView : MvxFragment<SecondViewModel> { public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { base.OnCreateView(inflater, container, savedInstanceState); return this.BindingInflate(Resource.Layout.SecondView, null); } } public class ThirdView : MvxFragment<ThirdViewModel> { public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { base.OnCreateView(inflater, container, savedInstanceState); return this.BindingInflate(Resource.Layout.ThirdView, null); } } public class FourthView : MvxFragment<FourthViewModel> { public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { base.OnCreateView(inflater, container, savedInstanceState); return this.BindingInflate(Resource.Layout.FourthView, null); } } public class MenuOnClickListener : Java.Lang.Object, View.IOnClickListener { public ResideMenu Menu { get; set; } public bool IsLeft { get; set; } public MenuOnClickListener(ResideMenu menu, bool isLeft) { Menu = menu; IsLeft = isLeft; } #region Implementation of IOnClickListener public void OnClick(View v) { Menu.OpenMenu(IsLeft ? 0 : 1); } #endregion }
布局文件的代碼:

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:local="http://schemas.android.com/apk/res-auto" android:orientation="vertical" android:background="@android:color/white" android:layout_width="fill_parent" android:layout_height="fill_parent"> <LinearLayout android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/layout_top"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="7dp" android:text="ReSideMenu MvvmCross DEMO" android:textSize="24sp" android:textColor="#999999" local:MvxBind="Text Title" android:layout_gravity="center"/> <ImageView android:layout_width="match_parent" android:layout_height="3dp" android:background="#FF21A549"/> </LinearLayout> <FrameLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:id="@+id/main_fragment"> </FrameLayout> </LinearLayout>
幾個Fragment對應View的布局代碼都是一樣的,這里就只給出一個Fragment的代碼

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:local="http://schemas.android.com/apk/res-auto" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <EditText android:layout_width="fill_parent" android:layout_height="wrap_content" android:textSize="40dp" local:MvxBind="Text Hello" /> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:textSize="40dp" local:MvxBind="Text Hello" /> </LinearLayout>
運行效果如下:
四、遇到的問題以及總結
1.現象:綁定的ResideMenu對象的MenuListener屬性只有get方法,沒有set方法,不能設置值。
原因:不太清楚,知道的朋友可以說一下。我的理解是set方法引用了R.java里的內容,而R.java生成的時間晚於綁定代碼的生成,所以導致了找不到引用的問題。
解決方法:在Metadata.xml文件里增加下面的代碼,手動增加一個方法。
<add-node path="/api/package[@name='com.special.ResideMenu']/class[@name='ResideMenu']"> <method name="setMenuListener" return="void" abstract="false" native="false" synchronized="false" static="false" final="false" deprecated="not deprecated" visibility="public" > <parameter name="listener" type="com.special.ResideMenu.ResideMenu.OnMenuListener"/> </method> </add-node>
2.現象:編譯能夠通過,運行時報Java.Lang.NullPointerException異常
at com.special.ResideMenu.ResideMenu.setBackground(ResideMenu.java:143),通過跟蹤發現要設置背景的對象為空,所以導致了空引用異常。
原因:residemenu.jar文件內包含了R.java的代碼,最開始我是手動導出的residemenu.jar,將R.java的代碼一起導出了。這樣會導致ResideMenu類里的所有findViewById方法返回null,解決這個問題花的時間最長,差不多過了兩天才發現。
解決方法:residemenu.jar文件里不要包含R.java的代碼。
3.現象:編譯能夠通過,運行時報Java.Lang.NoClassDefFoundError: com.special.ResideMenu.ResideMenu$2異常
原因:查看Visual Studio的Output窗口,可以發現如下信息:
Failed resolving Lcom/special/ResideMenu/ResideMenu$2; interface 264 'Lcom/nineoldandroids/animation/Animator$AnimatorListener;',通過提示,我們發現錯誤原因是不能解析nineoldandroids.jar里的Animator.AnimatorListener接口
解決方法:在Nuget里添加NineOldAndroids的引用。這里有一點還沒弄明白,ResideMenuLib項目已經包含了引用的NineOldAndroids.jar,正常情況下應該不需要再次添加引用了。
4.現象:運行時滑動界面無法顯示側滑菜單
原因:未重寫DispatchTouchEvent方法
解決方法:添加如下代碼即可
public override bool DispatchTouchEvent(MotionEvent ev) { return _resideMenu.DispatchTouchEvent(ev); }
5.MvvmCross中使用ResideMenu稍微有一點問題,每次切換Fragment時需要手動指定Fragment的ViewModel。如果需要實現ViewModel的單例,還需要額外處理,並且ViewModel的構造函數帶有注入參數時,處理起來更麻煩。
6.網上也有ResideMenu的綁定,見https://github.com/nishanil/XResideMenu ,本來我是想直接用這個綁定好的ResideMenu的,但是我用最新的java版residemenu生成的代碼替換此綁定里ResideMenu.aar對應的文件后,重新生成后的dll還是有問題,所以就重新綁定了一個。網上這個庫也說了綁定的時候有點問題,他給出了兩種解決方案:
1)將java庫的package從大寫修改為小寫,並將AndroidManifest.xml文件里的名稱也修改為小寫,然后重新編譯
2)手動修改VS生成的R.java文件里的package名稱,然后重新運行就可以了,修改之后不能重新生成和清理解決方案
上面說的問題只存在於monodroid-4.18以前的版本,4.18之后已修復了大小寫問題的BUG
7.最近綁定了一些java的庫,有時間我整理一下發出來。