Xamarin Android 打造属于自己的博客园APP(3)
打造通用下拉刷新上拉加载更多组件
android开发中最常用的就是列表组件,如ListView,recycleView,用到它们感觉就会涉及到数据更新,分页加载。
最开始的时候,刷新组件我是在技术群里头找了一个被人绑定好的库,是绑定的github上一个星星很多的java原生组件。但是demo很简单,对于当时小白的我懵逼了,不晓得咋个用,而且一直觉得banding的库总感觉有问题,就想着直接找一个java的库翻译成C#版本的。功夫不负苦心人,在csdn上找到了一篇 http://blog.csdn.net/zhongkejingwang/article/details/38868463
写的很详细,翻译起来也省了不少力,也很感谢原作者。
突然想起一句话,叫做我们不生产代码,只是代码的搬运工!
这里贴上我翻译好的其中几个很重要的组件的代码:
1.PullToRefreshLayout
using System; using System.Threading.Tasks; using Android.Content; using Android.OS; using Android.Util; using Android.Views; using Android.Views.Animations; using Android.Widget; using CNBlog.Droid.Utils; namespace CNBlog.Droid.PullableView { public class PullToRefreshLayout : RelativeLayout { // 初始状态 private const int initStatus = 0; // 释放刷新 private const int releaseToRefresh = 1; // 正在刷新 private const int refreshing = 2; // 释放加载 private const int releaseToLoad = 3; // 正在加载 private const int loading = 4; // 操作完毕 private const int complete = 5; // 当前状态 private int currentStatus = 0; // 刷新回调接口 private OnRefreshListener mListener; // 刷新成功 private const int succeed = 0; // 刷新失败 private const int failed = 1; // 按下Y坐标,上一个事件点Y坐标 private float downY, lastY; // 下拉的距离。注意:pullDownY和pullUpY不可能同时不为0 private float pullDownY = 0; // 上拉的距离 private float pullUpY = 0; // 释放刷新的距离 private float refreshDist = 200; // 释放加载的距离 private float loadmoreDist = 200; private UIScheduling uScheduling; // 回滚速度 private float moveSpeed = 8; // 第一次执行布局 private bool isLayout = false; // 在刷新过程中滑动操作 private bool isTouch = false; // 手指滑动距离与下拉头的滑动距离比,中间会随正切函数变化 private float radio = 2; // 下拉箭头的转180°动画 private RotateAnimation rotateAnimation; // 均匀旋转动画 private RotateAnimation refreshingAnimation; // 下拉头 private View refreshView; // 下拉的箭头 public View pullView; // 正在刷新的图标 private View refreshingView; // 刷新结果图标 private View refreshStateImageView; // 刷新结果:成功或失败 private TextView refreshStateTextView; // 上拉头 private View loadmoreView; // 上拉的箭头 public View pullUpView; // 正在加载的图标 private View loadingView; // 加载结果图标 private View loadStateImageView; // 加载结果:成功或失败 private TextView loadStateTextView; //请求加载错误View private View errorView; // 实现了Pullable接口的View private View pullableView; // 过滤多点触碰 private int mEvents; // 这两个变量用来控制pull的方向,如果不加控制,当情况满足可上拉又可下拉时没法下拉 private bool canPullDown = true; private bool canPullUp = true; private Context mContext; private Handler updateUIHandler; public void setOnRefreshListener(OnRefreshListener listener) { mListener = listener; } public PullToRefreshLayout(Context context) : base(context) { initView(context); } public PullToRefreshLayout(Context context, IAttributeSet attrs) : base(context, attrs) { initView(context); } public PullToRefreshLayout(Context context, IAttributeSet attrs, int defStyle) : base(context, attrs, defStyle) { initView(context); } private void initView(Context context) { mContext = context; updateUIHandler = new Handler((Message msg) => { // 回弹速度随下拉距离moveDeltaY增大而增大 moveSpeed = (float)(8 + 5 * Math.Tan(Math.PI / 2 / MeasuredHeight * (pullDownY + Math.Abs(pullUpY)))); if (!isTouch) { // 正在刷新,且没有往上推的话则悬停,显示"正在刷新..." if (currentStatus == refreshing && pullDownY <= refreshDist) { pullDownY = refreshDist; uScheduling.Cancel(); } else if (currentStatus == loading && -pullUpY <= loadmoreDist) { pullUpY = -loadmoreDist; uScheduling.Cancel(); } } if (pullDownY > 0) pullDownY -= moveSpeed; else if (pullUpY < 0) pullUpY += moveSpeed; if (pullDownY < 0) { // 已完成回弹 pullDownY = 0; pullView.ClearAnimation(); // 隐藏下拉头时有可能还在刷新,只有当前状态不是正在刷新时才改变状态 if (currentStatus != refreshing && currentStatus != loading) changeStatus(initStatus); uScheduling.Cancel(); RequestLayout(); } if (pullUpY > 0) { // 已完成回弹 pullUpY = 0; pullUpView.ClearAnimation(); // 隐藏上拉头时有可能还在刷新,只有当前状态不是正在刷新时才改变状态 if (currentStatus != refreshing && currentStatus != loading) changeStatus(initStatus); uScheduling.Cancel(); RequestLayout(); } // 刷新布局,会自动调用onLayout RequestLayout(); // 没有拖拉或者回弹完成 if (pullDownY + Math.Abs(pullUpY) == 0) { uScheduling.Cancel(); } }); uScheduling = new UIScheduling(updateUIHandler); rotateAnimation = (RotateAnimation)AnimationUtils.LoadAnimation( context, Resource.Animator.reverse_anim); refreshingAnimation = (RotateAnimation)AnimationUtils.LoadAnimation( context, Resource.Animator.rotating); // 添加匀速转动动画 LinearInterpolator lir = new LinearInterpolator(); rotateAnimation.Interpolator = lir; refreshingAnimation.Interpolator = lir; } private void initView() { // 初始化下拉布局 pullView = refreshView.FindViewById<View>(Resource.Id.pull_icon); refreshStateTextView = refreshView.FindViewById<TextView>(Resource.Id.state_tv); refreshingView = refreshView.FindViewById<View>(Resource.Id.refreshing_icon); refreshStateImageView = refreshView.FindViewById<View>(Resource.Id.state_iv); // 初始化上拉布局 pullUpView = loadmoreView.FindViewById<View>(Resource.Id.pullup_icon); loadStateTextView = loadmoreView.FindViewById<TextView>(Resource.Id.loadstate_tv); loadingView = loadmoreView.FindViewById<View>(Resource.Id.loading_icon); loadStateImageView = loadmoreView.FindViewById<View>(Resource.Id.loadstate_iv); } /// <summary> /// 完成刷新操作,显示刷新结果。注意:刷新完成后一定要调用这个方法 /// </summary> /// <param name="refreshResult">succeed代表成功, failed代表失败</param> public void refreshFinish(int refreshResult) { refreshingView.ClearAnimation(); refreshingView.Visibility = ViewStates.Gone; switch (refreshResult) { case 0: // 刷新成功 refreshStateImageView.Visibility = ViewStates.Visible; refreshStateTextView.Text = "刷新成功"; refreshStateImageView.SetBackgroundResource(Resource.Mipmap.refresh_succeed); break; case 1: default: // 刷新失败 refreshStateImageView.Visibility = ViewStates.Visible; refreshStateTextView.Text = "刷新失败"; refreshStateImageView.SetBackgroundResource(Resource.Mipmap.refresh_failed); break; } if (pullDownY > 0) { // 刷新结果停留1秒 new Handler((Message msg) => { changeStatus(complete); hide(); }).SendEmptyMessageDelayed(0, 1000); } else { changeStatus(complete); hide(); } } /// <summary> /// 加载完毕,显示加载结果。注意:刷新完成后一定要调用这个方法 /// </summary> /// <param name="refreshResult">succeed代表成功, failed代表失败</param> public void loadmoreFinish(int refreshResult) { loadingView.ClearAnimation(); loadingView.Visibility = ViewStates.Gone; switch (refreshResult) { case 0: // 加载成功 loadStateImageView.Visibility = ViewStates.Visible; loadStateTextView.Text = "加载成功"; loadStateImageView.SetBackgroundResource(Resource.Mipmap.load_succeed); break; case 1: default: // 加载失败 loadStateImageView.Visibility = ViewStates.Visible; loadStateTextView.Text = "加载失败"; loadStateImageView.SetBackgroundResource(Resource.Mipmap.load_failed); pullableView.Visibility = ViewStates.Gone; break; } if (pullUpY < 0) { // 刷新结果停留1秒 new Handler((Message msg) => { changeStatus(complete); hide(); }).SendEmptyMessageDelayed(0, 1000); } else { changeStatus(complete); hide(); } } /// <summary> /// 改变界面布局状态 /// </summary> /// <param name="status"></param> private void changeStatus(int status) { currentStatus = status; Log.Debug("status:", status.ToString()); switch (currentStatus) { case 0: // 下拉布局初始状态 refreshStateImageView.Visibility = ViewStates.Gone; refreshStateTextView.Text = Context.GetString(Resource.String.pull_to_refresh); pullView.ClearAnimation(); pullView.Visibility = ViewStates.Visible; // 上拉布局初始状态 loadStateImageView.Visibility = ViewStates.Gone; loadStateTextView.Text = Context.GetString(Resource.String.pullup_to_load); pullUpView.ClearAnimation(); pullUpView.Visibility = ViewStates.Visible; break; case 1: // 释放刷新状态 refreshStateTextView.Text = Context.GetString(Resource.String.release_to_refresh); pullView.StartAnimation(rotateAnimation); break; case 2: // 正在刷新状态 pullView.ClearAnimation(); refreshingView.Visibility = ViewStates.Visible; pullView.Visibility = ViewStates.Invisible; refreshingView.StartAnimation(refreshingAnimation); refreshStateTextView.Text = Context.GetString(Resource.String.refreshing); break; case 3: // 释放加载状态 loadStateTextView.Text = Context.GetString(Resource.String.release_to_load); pullUpView.StartAnimation(rotateAnimation); break; case 4: // 正在加载状态 pullUpView.ClearAnimation(); loadingView.Visibility = ViewStates.Visible; pullUpView.Visibility = ViewStates.Invisible; loadingView.StartAnimation(refreshingAnimation); loadStateTextView.Text = Context.GetString(Resource.String.loading); break; case 5: // 刷新或加载完毕,啥都不做 break; } } /// <summary> /// 不限制上拉或下拉 /// </summary> private void releasePull() { canPullDown = true; canPullUp = true; } /// <summary> /// 由父控件决定是否分发事件,防止事件冲突 /// </summary> /// <param name="e"></param> /// <returns></returns> public override bool DispatchTouchEvent(MotionEvent e) { switch (e.ActionMasked) { case MotionEventActions.Down: downY = e.GetY(); lastY = downY; uScheduling.Cancel(); mEvents = 0; releasePull(); break; case MotionEventActions.PointerDown: case MotionEventActions.PointerUp: // 过滤多点触碰 mEvents = -1; break; case MotionEventActions.Move: if (mEvents == 0) { if (pullDownY > 0 || (((Pullable)pullableView).canPullDown() && canPullDown && currentStatus != loading)) { // 可以下拉,正在加载时不能下拉 // 对实际滑动距离做缩小,造成用力拉的感觉 pullDownY = pullDownY + (e.GetY() - lastY) / radio; if (pullDownY < 0) { pullDownY = 0; canPullDown = false; canPullUp = true; } if (pullDownY > this.MeasuredHeight) pullDownY = this.MeasuredHeight; if (currentStatus == refreshing) { // 正在刷新的时候触摸移动 isTouch = true; } } else if (pullUpY < 0 || (((Pullable)pullableView).canPullUp() && canPullUp && currentStatus != refreshing)) { // 可以上拉,正在刷新时不能上拉 pullUpY = pullUpY + (e.GetY() - lastY) / radio; if (pullUpY > 0) { pullUpY = 0; canPullDown = true; canPullUp = false; } if (pullUpY < -this.MeasuredHeight) pullUpY = -this.MeasuredHeight; if (currentStatus == loading) { // 正在加载的时候触摸移动 isTouch = true; } } else releasePull(); } else mEvents = 0; lastY = e.GetY(); // 根据下拉距离改变比例 radio = (float)(2 + 2 * Math.Tan(Math.PI / 2 / this.MeasuredHeight * (pullDownY + Math.Abs(pullUpY)))); if (pullDownY > 0 || pullUpY < 0) RequestLayout(); if (pullDownY > 0) { if (pullDownY <= refreshDist && (currentStatus == releaseToRefresh || currentStatus == complete)) { // 如果下拉距离没达到刷新的距离且当前状态是释放刷新,改变状态为下拉刷新 changeStatus(initStatus); } if (pullDownY >= refreshDist && currentStatus == initStatus) { // 如果下拉距离达到刷新的距离且当前状态是初始状态刷新,改变状态为释放刷新 changeStatus(releaseToRefresh); } } else if (pullUpY < 0) { // 下面是判断上拉加载的,同上,注意pullUpY是负值 if (-pullUpY <= loadmoreDist && (currentStatus == releaseToLoad || currentStatus == complete) && mListener.CanLoadMore() ) { changeStatus(initStatus); } // 上拉操作 if (-pullUpY >= loadmoreDist && currentStatus == initStatus && mListener.CanLoadMore() ) { changeStatus(releaseToLoad); } } // 因为刷新和加载操作不能同时进行,所以pullDownY和pullUpY不会同时不为0,因此这里用(pullDownY + // Math.Abs(pullUpY))就可以不对当前状态作区分了 if ((pullDownY + Math.Abs(pullUpY)) > 8) { // 防止下拉过程中误触发长按事件和点击事件 e.Action = MotionEventActions.Cancel; } break; case MotionEventActions.Up: if (pullDownY > refreshDist || -pullUpY > loadmoreDist) // 正在刷新时往下拉(正在加载时往上拉),释放后下拉头(上拉头)不隐藏 { isTouch = false; } if (currentStatus == releaseToRefresh) { changeStatus(refreshing); // 刷新操作 if (mListener != null) mListener.onRefresh(this); } else if (currentStatus == releaseToLoad) { changeStatus(loading); // 加载操作 if (mListener != null) mListener.onLoadMore(this); } hide(); break; default: break; } base.DispatchTouchEvent(e); return true; } protected override void OnLayout(bool changed, int l, int t, int r, int b) { if (!isLayout) { // 这里是第一次进来的时候做一些初始化 refreshView = GetChildAt(0); pullableView = GetChildAt(1); loadmoreView = GetChildAt(2); isLayout = true; initView(); refreshDist = ((ViewGroup)refreshView).GetChildAt(0) .MeasuredHeight; loadmoreDist = ((ViewGroup)loadmoreView).GetChildAt(0) .MeasuredHeight; } // 改变子控件的布局,这里直接用(pullDownY + pullUpY)作为偏移量,这样就可以不对当前状态作区分 refreshView.Layout(0, (int)(pullDownY + pullUpY) - refreshView.MeasuredHeight, refreshView.MeasuredWidth, (int)(pullDownY + pullUpY)); pullableView.Layout(0, (int)(pullDownY + pullUpY), pullableView.MeasuredWidth, (int)(pullDownY + pullUpY) + pullableView.MeasuredHeight); loadmoreView.Layout(0, (int)(pullDownY + pullUpY) + pullableView.MeasuredHeight, loadmoreView.MeasuredWidth, (int)(pullDownY + pullUpY) + pullableView.MeasuredHeight + loadmoreView.MeasuredHeight); } /// <summary> /// 隐藏刷新UI界面 /// </summary> private void hide() { uScheduling.Schedule(5); } public async Task AutoRefresh() { while (pullDownY < 4 / 3 * refreshDist) { pullDownY += moveSpeed; if (pullDownY > refreshDist) changeStatus(releaseToRefresh); RequestLayout(); await Task.Delay(20); } changeStatus(refreshing); if(mListener!=null) mListener.onRefresh(this); hide(); } } }
2.PullableListView
然后接下来就是一个自定义实现的ListView
using System; using Android.Content; using Android.Util; using Android.Views; using Android.Widget; namespace CNBlog.Droid.PullableView { public class PullableListView :ListView,Pullable { private View errorLayout; public PullableListView(Context context) :base(context) { } public PullableListView(Context context, IAttributeSet attrs) :base(context,attrs) { } public PullableListView(Context context, IAttributeSet attrs, int defStyle) : base(context, attrs,defStyle) { } public bool canPullDown() { if (Count == 0) { // 没有item的时候也可以下拉刷新 return true; } else if (FirstVisiblePosition == 0 && GetChildAt(0).Top >= 0) { // 滑到ListView的顶部了 return true; } else return false; } public bool canPullUp() { if (Count == 0) { // 没有item的时候也可以上拉加载 return true; } else if (LastVisiblePosition == (Count - 1)) { // 滑到底部了 if (GetChildAt(LastVisiblePosition - FirstVisiblePosition) != null && GetChildAt( LastVisiblePosition - FirstVisiblePosition).Bottom <= MeasuredHeight) return true; } return false; } public void SetErrorLayout() { if (errorLayout == null) errorLayout = LayoutInflater.From(Context).Inflate(Resource.Layout.error_page, null); RemoveHeaderView(errorLayout); AddHeaderView(errorLayout,null,false); SetHeaderDividersEnabled(false); } } }
3.Pullable
namespace CNBlog.Droid.PullableView { /// <summary> /// 如需扩展其它View,实现该接口即可 /// </summary> public interface Pullable { /// <summary> /// 判断是否可以下拉,如果不需要下拉功能可以直接return false /// </summary> /// <returns></returns> bool canPullDown(); /// <summary> /// 判断是否可以上拉,如果不需要上拉功能可以直接return false /// </summary> /// <returns></returns> bool canPullUp(); } }
4.OnRefreshListener
using System.Threading.Tasks; namespace CNBlog.Droid.PullableView { public interface OnRefreshListener { /// <summary> /// 刷新操作 /// </summary> /// <param name="pullToRefreshLayout"></param> Task onRefresh(PullToRefreshLayout pullToRefreshLayout); /// <summary> /// 加载更多 /// </summary> /// <param name="pullToRefreshLayout"></param> Task onLoadMore(PullToRefreshLayout pullToRefreshLayout); /// <summary> /// 是否可以加载更多 /// </summary> /// <returns><c>true</c>, if load more was caned, <c>false</c> otherwise.</returns> bool CanLoadMore(); } }
代码搬运工,注释都是一模一样的
还是大概讲解下,第 一个PullToRefreshLayout刷新组件,关键代码在于重写的 DispatchTouchEvent 函数。
dispatchTouchEvent是用来处理触摸事件分发,关于事件分发机制是咋样的,这个就复杂了,毕竟我也是菜逼,估计也讲不明白。
这儿的左右大概就是监听手指触摸事件,通过监听手指上拉以及下拉作相应的操作。
第二个PullableListView 继承自ListView以及实现了接口Pullable,Pullable里的canPullDown(),canPullUp() 用于DispatchTouchEvent 触发时判断当前View是否可以上拉加载更多,以及下拉加载更多。
而自定义的ListView通过当前Item所在位置判断是否可以是否可以上拉加载更多,以及下拉加载更多。
而OnRefreshListener定义了刷新操作以及加载更多 两个接口。
顺便再贴上一起实现的WebView
using System; using Android.Content; using Android.Util; using Android.Webkit; namespace CNBlog.Droid.PullableView { public class PullableWebView :WebView,Pullable { public PullableWebView(Context context) :base(context) { } public PullableWebView(Context context, IAttributeSet attrs) :base(context,attrs) { } public PullableWebView(Context context, IAttributeSet attrs, int defStyle) :base(context,attrs,defStyle) { } public bool canPullDown() { if (ScrollY == 0) return true; else return false; } public bool canPullUp() { //if (ScrollY >=ContentHeight * Scale // - MeasuredHeight) // return true; //else return false; } } }
这个就很简单了,因为webview我不需要上拉加载更多,所以只需要判断当前滚动条在什么位置,如果ScrollY=0即可下拉刷新。
这里上传一个目前已打包好的APK文件,让各位看官老爷体验下,如果觉得丑陋,渣的话,请各位轻点儿喷,毕竟我还是个孩子啊。
有什么好的建议的话,欢迎大家提出。
网盘地址:http://pan.baidu.com/s/1sl0Eu1f
下章贴出源码哈,待我稍稍整理下,我想这也是各位看官最关心的事儿吧。