PS:发现文章被其他网站或者博客抓取后发表为原创了,给图片加了个水印
下拉刷新功能在安卓和iOS中非常常见,一般实现这样的功能都是直接使用第三方的库,网上能找到很多这样的开源库。然而在Xamarin. Android中要实现一个好用的下拉刷新功能却不是很容易,在网上找了几个Xamarin.Android的下拉刷新控件,都不是很满意,所以想重新绑定一个java写的下拉刷新控件。在网上找了几个这样的开源库,通过对比发现android-pull-to-refresh实现的功能比较多,实现的效果也比较满意。
Android-Pull-To-Refresh项目地址:https://github.com/naver/android-pull-to-refresh
该库包含如下功能点:
- 支持顶部下拉刷新和顶部上拉刷新(可以同时启用这两个功能)
- Android2.3以上设备支持滚动
- 支持以下控件
- ListView
- ExpandableListView
- GridView
- WebView
- ScrollView
- HorizontalScrollView
- ViewPager
- 支持检测列表是否滚动到最末尾
- 支持ListFragment
- 支持很多自定义选项(1.自定义正在加载界面,可以修改图标和文字 2.支持多个提示图标,平滑滚动时间间隔设置等)
其他详细说明请到该项目的网站查看。
本文主要包含以下五个部分
下面开始进行绑定操作,要能够进行绑定,首先需要将java项目编译为jar文件,我是通过fat-jar插件生成pulltorefresh.jar文件的,通过其他方式生成也是可以的。
只选中项目的output即可
接下来创建Android Binding项目,将生成的jar文件添加到项目中,生成类型选择embeddedjar,生成的版本选择2.3,由于该项目没有引用其他项目,所以不需要进行其他设置,项目的结构图如下:
接下来编译项目,编译时VS给出了如下的错误提示:
error CS0060: Inconsistent accessibility: base class 'Com.Handmark.Pulltorefresh.Library.PullToRefreshListView.InternalListView' is less accessible than class 'Com.Handmark.Pulltorefresh.Library.PullToRefreshListView.InternalListViewSDK9'
error CS0102: The type 'Com.Handmark.Pulltorefresh.Library.PullToRefreshBase' already contains a definition for 'Mode'
error CS0102: The type 'Com.Handmark.Pulltorefresh.Library.PullToRefreshBase' already contains a definition for 'State'
根据错误提示可以看出:
第一个错误是由于子类方法的可访问性比父类的高,双击错误提示可以看到InternalListView类是protected修饰的,而子类InternalListViewSDK9是public修饰,在c#是不允许的,那么我们可以通过metadata.xml配置类型修饰符,使他们的修饰符统一,我这里将InternalListView类的修饰符修改public,代码如下:
<attr path="/api/package[@name='com.handmark.pulltorefresh.library']/class[@name='PullToRefreshListView.InternalListView']" name="visibility">public</attr>
第二个和第三个错误的提示为已经包含了Mode和State的定义,通过查看java的源代码发现是由以下原因引起的:
PullToRefreshBase类里面定义了两个枚举Mode和State,并且定义了getMode、getState、setMode、setState方法,java里的get和set方法会被转换为c#里的属性,生成的代码如下:
public class PullToRefreshBase{
public enum Mode{}
public enum State{}
public Mode Mode{get;set;}
public State State{get;set;}
}
在C#中,这样的代码是无法通过编译的,因为属性的名称和类型的名称一样,所以必须进行修改才行,我们可以将枚举的名称分别修改为PullToRefreshMode和PullToRefreshState.我们在metadata.xml里添加如下代码:
<attr path="/api/package[@name='com.handmark.pulltorefresh.library']/class[@name='PullToRefreshBase.Mode']"
name="managedName">
PullToRefreshMode
</attr>
<attr path="/api/package[@name='com.handmark.pulltorefresh.library']/class[@name='PullToRefreshBase.State']"
name="managedName">
PullToRefreshState
</attr>
再次编译,发现还是报错,一共报了10个错误:
第一个错误是由于访问修饰符不统一造成的,通过添加如下代码可以解决:
<attr path="/api/package[@name='com.handmark.pulltorefresh.library.internal']/class[@name='RotateLoadingLayout']/method[@name='onLoadingDrawableSet' and count(parameter)=1 and parameter[1][@type='android.graphics.drawable.Drawable']]"
name="visibility">protected</attr>
剩下的9个问题都是类似的,未实现某个接口或者抽象类的某个方法,双击其中的错误提示,发现有6个方法都是实现了,但编译的时候还是提示未实现方法。通过查看java源代码与生成的c#代码,找到了原因,java源代码里面有一个泛型类PullToRefreshBase<V entends View>,该类里面有一个抽象的泛型方法protected abstract T createRefreshableView(Context context, AttributeSet attrs);,转换为c#代码后泛型抽象方法的返回值变为了Java.Lang.Object,而实际上应该是生成一个泛型类,然后生成一个泛型抽象方法,可见转换程序还不是太完善。此处的修改方式为:将子类的对应方法的返回值修改为Java.Lang.Object,代码如下:
<attr
path="/api/package[@name='com.handmark.pulltorefresh.library']/class[@name='PullToRefreshScrollView']/method[@name='createRefreshableView']"
name="managedReturn"
>Java.Lang.Object</attr>
<attr
path="/api/package[@name='com.handmark.pulltorefresh.library']/class[@name='PullToRefreshExpandableListView']/method[@name='createRefreshableView']"
name="managedReturn"
>Java.Lang.Object</attr>
<attr
path="/api/package[@name='com.handmark.pulltorefresh.library']/class[@name='PullToRefreshGridView']/method[@name='createRefreshableView']"
name="managedReturn"
>Java.Lang.Object</attr>
<attr
path="/api/package[@name='com.handmark.pulltorefresh.library']/class[@name='PullToRefreshHorizontalScrollView']/method[@name='createRefreshableView']"
name="managedReturn"
>Java.Lang.Object</attr>
<attr
path="/api/package[@name='com.handmark.pulltorefresh.library']/class[@name='PullToRefreshListView']/method[@name='createRefreshableView']"
name="managedReturn"
>Java.Lang.Object</attr>
<attr
path="/api/package[@name='com.handmark.pulltorefresh.library']/class[@name='PullToRefreshWebView']/method[@name='createRefreshableView']"
name="managedReturn"
>Java.Lang.Object</attr>
剩下3个方法,在生成的C#代码里确实没找到,那就是没进行转换或者转换时报错了,此时我们需要查看对应的java源代码,分别找到报错的方法InternalListView.setEmptyView
InternalExpandableListView.setEmptyView
InternalGridView.setEmptyView
这3个方法里的代码都很简单,都只有一句代码
PullToRefreshListView.this.setEmptyView(emptyView);
PullToRefreshExpandableListView.this.setEmptyView(emptyView);
PullToRefreshGridView.this.setEmptyView(emptyView);
这3句代码都非常类似,都是”ClassName.this.MethodName”,在java里,只有内部类里可以这样写,该代码的作用是访问外部类的实例方法。 由于c#里面没有” ClassName.this.MethodName”的写法,所以猜想可能是这个原因导致了转换失败,那么我们就只有修改java源代码进行测试了。去掉this,换成同等效果的写法,修改方法如下,3个类的修改方法类似,这里就只写一个:
1. 在InternalListView类增加一个PullToRefreshListView类型的字段 _view
2. 在InternalListView类的构造函数添加一个PullToRefreshListView类型的参数view,并在构造函数内部给新增的字段赋值,使用view的值
3. 修改setEmptyView方法的代码,改为_view. setEmptyView(emptyView)
4. 增加一个get方法,返回刚才新增的_view字段
5. 修改引用的代码,传入对应的参数
修改完成后重新导出jar文件,替换为vs项目中的对应jar文件,然后重新编译,编译之后还是报同样的错误,这样就证明我们的猜想不正确,那么到底是什么原因导致转换失败内?我开始试了一些其他方法,都没有成功编译,最后发现InternalListView以及InternalListViewSDK9都是内部类(嵌套到PullToRefreshListView里面的,另外两个两个类也是内部类),就是这个内部类导致了错误,我们把对应的6个内部类(每个类里面2个内部类,一个InternalXXX一个InternalXXXSDK9)改为普通类,再次导出jar,再次编译,编译通过了,我们现在来测试一下绑定的库能否正常工作。
新建一个测试项目PullToRefresh.Sample,添加相关资源及引用,并将原项目的java代码翻译为c#代码,翻译的时候有以下两个地方需要注意:
1. 实现java接口的类要继承自Java.Lang.Object,否则需要自己实现IJavaObject,而自己实现的IJavaObject很有可能无法正常工作,具体信息可以参考:http://developer.xamarin.com/guides/android/advanced_topics/java_integration_overview/working_with_jni/#Implementing_Interfaces
2. C#的内部类是无法直接访问外部类的实例成员的,所以需要对其中的内部类做调整
3. 向集合里添加数据使用mAdapter.Insert方法,直接向List集合添加界面不会显示数据
翻译后的C#代码如下:
[Activity(Label = "PullToRefresh.Sample", MainLauncher = true, Icon = "@drawable/icon")]
public class MainActivity : Activity, PullToRefreshBase.IOnRefreshListener, PullToRefreshBase.IOnLastItemVisibleListener
{
private List<string> mListItems;
private PullToRefreshListView mPullRefreshListView;
private ArrayAdapter<string> mAdapter;
private string[] mStrings = { "Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi",
"Acorn", "Adelost", "Affidelice au Chablis", "Afuega'l Pitu", "Airag", "Airedale", "Aisy Cendre",
"Allgauer Emmentaler", "Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi",
"Acorn", "Adelost", "Affidelice au Chablis", "Afuega'l Pitu", "Airag", "Airedale", "Aisy Cendre",
"Allgauer Emmentaler" };
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
// Set our view from the "main" layout resource
SetContentView(Resource.Layout.Main);
mPullRefreshListView = FindViewById<PullToRefreshListView>(Resource.Id.pull_refresh_list);
var actualListView = (ListView)mPullRefreshListView.RefreshableView;
mListItems=new List<string>(mStrings);
mAdapter=new ArrayAdapter<string>(this,Android.Resource.Layout.SimpleListItem1,mListItems);
var soundListener = new SoundPullEventListener (this);
soundListener.AddSoundEvent(PullToRefreshBase.PullToRefreshState.PullToRefresh, Resource.Raw.pull_event);
soundListener.AddSoundEvent(PullToRefreshBase.PullToRefreshState.Reset, Resource.Raw.reset_sound);
soundListener.AddSoundEvent(PullToRefreshBase.PullToRefreshState.Refreshing, Resource.Raw.refreshing_sound);
mPullRefreshListView.SetOnPullEventListener(soundListener);
mPullRefreshListView.SetOnRefreshListener(this);
mPullRefreshListView.SetOnLastItemVisibleListener(this);
actualListView.Adapter = mAdapter;
}
private class GetDataTask : AsyncTask<Java.Lang.Void, Java.Lang.Void, string[]>
{
private readonly MainActivity _mainActivity;
public GetDataTask(MainActivity mainActivity)
{
_mainActivity = mainActivity;
}
protected override string[] RunInBackground(params Java.Lang.Void[] @params)
{
try
{
Thread.Sleep(4000);
}
catch (InterruptedException)
{
}
return _mainActivity.mStrings;
}
protected override void OnPostExecute(Object result)
{
_mainActivity. mAdapter.Insert("added after refresh:" + DateTime.Now.ToString("t"),0);
_mainActivity.mAdapter.NotifyDataSetChanged();
_mainActivity.mPullRefreshListView.OnRefreshComplete();
base.OnPostExecute(result);
}
}
public void OnRefresh(PullToRefreshBase p0)
{
p0.GetLoadingLayoutProxy(true,true).SetLastUpdatedLabel(string.Format("上次更新:{0:t}",DateTime.Now));
new GetDataTask(this).Execute();
}
public void OnLastItemVisible()
{
Toast.MakeText(this,"End of List", ToastLength.Short).Show();
}
}
代码调整完成后进行编译,发现编译的时候又报错了,报错信息里很多乱码,不过可以看到几个关键字“OnSmoothScrollFinishedListener”,所以猜想可能是“OnSmoothScrollFinishedListener”这个接口可能有问题,我们返回到java源代码查看。
在java源代码里的“PullToRefreshBase”类里搜索“OnSmoothScrollFinishedListener”,
找到接口的定义:
static interface OnSmoothScrollFinishedListener {
void onSmoothScrollFinished();
}
我们发现接口是static且是默认的修饰符,我们试一试将修饰符改为public,重新导出jar,再次编译,发现能够通过了,现在我们来看看功能是否正常
运行的时候报错了,提示找不到资源,原来我们绑定java库时忘记打包资源了,我们将资源文件一起打包,然后重新编译.打包资源文件时需要注意以下问题:
- jar文件和资源文件都打包到一个zip文件中,zip压缩包的目录结构如下:
再次编译并运行,终于正常运行了。
由于最近在使用Mvvmcross,所以也写一个Mvvmcross的例子,下面以ListView为例,实现一个MvxPullToRefreshListView。代码如下:
public class MvxPullToRefreshListView:PullToRefreshListView,PullToRefreshBase.IOnRefreshListener
{
public MvxPullToRefreshListView(Context context, IAttributeSet attrs)
: this(context, attrs, new MvxAdapter(context))
{
}
public MvxPullToRefreshListView(Context context, IAttributeSet attrs, IMvxAdapter adapter)
: base(context, attrs)
{
Mode = PullToRefreshMode.Both;
if (adapter == null)
return;
var itemTemplateId = MvxAttributeHelpers.ReadListItemTemplateId(context, attrs);
adapter.ItemTemplateId = itemTemplateId;
var lv = (ListView) RefreshableView;
lv.Adapter = adapter;
base.SetOnRefreshListener(this);
}
public IMvxAdapter Adapter
{
get
{
var v = ((ListView)RefreshableView);
var adapter=((HeaderViewListAdapter) v.Adapter).WrappedAdapter as IMvxAdapter;
return adapter;
}
set
{
var existing = Adapter;
if (existing == value)
return;
if (value != null && existing != null)
{
value.ItemsSource = existing.ItemsSource;
value.ItemTemplateId = existing.ItemTemplateId;
}
var v = ((ListView)base.RefreshableView);
v.Adapter = value;
}
}
[MvxSetToNullAfterBinding]
public IEnumerable ItemsSource
{
get { return Adapter.ItemsSource; }
set { Adapter.ItemsSource = value; }
}
public bool IsLoading
{
get { return Refreshing; }
set
{
if (!value)
{
OnRefreshComplete();
}
}
}
public int ItemTemplateId
{
get { return Adapter.ItemTemplateId; }
set { Adapter.ItemTemplateId = value; }
}
private ICommand _itemClick;
public new ICommand ItemClick
{
get { return _itemClick; }
set { _itemClick = value; if (_itemClick != null) EnsureItemClickOverloaded(); }
}
private bool _itemClickOverloaded = false;
private void EnsureItemClickOverloaded()
{
if (_itemClickOverloaded)
return;
_itemClickOverloaded = true;
var v = ((ListView)base.RefreshableView);
v.ItemClick += (sender, args) => ExecuteCommandOnItem(this.ItemClick, args.Position);
}
private ICommand _itemLongClick;
public new ICommand ItemLongClick
{
get { return _itemLongClick; }
set { _itemLongClick = value; if (_itemLongClick != null) EnsureItemLongClickOverloaded(); }
}
private bool _itemLongClickOverloaded = false;
private void EnsureItemLongClickOverloaded()
{
if (_itemLongClickOverloaded)
return;
_itemLongClickOverloaded = true;
var v = ((ListView)base.RefreshableView);
v.ItemLongClick += (sender, args) => ExecuteCommandOnItem(this.ItemLongClick, args.Position);
}
protected virtual void ExecuteCommandOnItem(ICommand command, int position)
{
if (command == null)
return;
var item = Adapter.GetRawItem(position);
if (item == null)
return;
if (!command.CanExecute(item))
return;
command.Execute(item);
}
public ICommand RefreshCommand { get; set; }
#region IOnRefreshListener Members
public void OnRefresh(PullToRefreshBase p0)
{
var lastUpdatedLabel = string.Format("上次更新:{0:T}", DateTime.Now);
p0.LoadingLayoutProxy.SetLastUpdatedLabel(lastUpdatedLabel);
if (RefreshCommand != null)
{
if (RefreshCommand.CanExecute(null))
{
RefreshCommand.Execute(null);
}
}
}
#endregion
}
使用MvxPullToRefreshListView的关键代码如下:
FirstViewModel.cs
/// <summary>
/// The _refresh command
/// </summary>
private MvxCommand _refreshCommand;
/// <summary>
/// Gets the refresh command.
/// </summary>
/// <value>The refresh command.</value>
public System.Windows.Input.ICommand RefreshCommand
{
get
{
_refreshCommand = _refreshCommand ?? new MvxCommand(async()=>await DoRefreshCommand());
return _refreshCommand;
}
}
/// <summary>
/// Does the refresh command.
/// </summary>
/// <returns>Task.</returns>
private async Task DoRefreshCommand()
{
Users = await LoadDataAsync();
}
/// <summary>
/// load data as an asynchronous operation.
/// </summary>
/// <returns>Task{List{UserInfo}}.</returns>
private async Task<List<UserInfo>> LoadDataAsync()
{
IsLoading = true;
await Task.Delay(SleepMilliSeconds);
var r = new Random();
var count = r.Next(1, 6);
return await Task.Run(() =>
{
var list = new List<UserInfo>();
for (int i = 0; i < count; i++)
{
list.Insert(0, new UserInfo
{
FirstName = "FirstName" + DateTime.Now.ToString("HH:mm:ss fff") + "__" + i,
LastName = "LastName" + DateTime.Now.ToString("T") + "__" + i + r.Next(),
});
}
IsLoading = false;
return list;
});
}
FirstView.axml代码如下:
<?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">
<pulltorefreshsample.droid.views.MvxPullToRefreshListView
android:id="@+id/pull_refresh_list"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:cacheColorHint="#00000000"
android:divider="#FF0C79E9"
android:dividerHeight="1dp"
android:fadingEdge="none"
android:fastScrollEnabled="false"
android:footerDividersEnabled="false"
android:headerDividersEnabled="false"
android:smoothScrollbar="true"
local:MvxItemTemplate="@layout/user_item"
local:MvxBind="ItemsSource Users;RefreshCommand RefreshCommand;IsLoading IsLoading"
/>
</LinearLayout>
user_item.xaml的代码如下:
<?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">
<TextView
local:MvxBind="Text FirstName"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/textView1" />
<TextView
local:MvxBind="Text LastName"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/textView2" />
</LinearLayout>
运行的效果和上面一样,就不上图了。
C#要使用java的jar不容易啊,会出现各种各样的问题,感觉比直接在java里使用麻烦很多很多,需要耐心的解决这些问题,并且可能需要修改java代码,如果没有源代码,可能会比较麻烦。不过使用Xamarin的好处是,逻辑代码可以完全重用,并且编写代码的效率比直接用java要高。