打造通用下拉刷新上拉加载更多组件

android开发中最常用的就是列表组件,如ListView,recycleView,用到它们感觉就会涉及到数据更新,分页加载。

最开始的时候,刷新组件我是在技术群里头找了一个被人绑定好的库,是绑定的github上一个星星很多的java原生组件。但是demo很简单,对于当时小白的我懵逼了,不晓得咋个用,而且一直觉得banding的库总感觉有问题,就想着直接找一个java的库翻译成C#版本的。功夫不负苦心人,在csdn上找到了一篇 http://blog.csdn.net/zhongkejingwang/article/details/38868463

写的很详细,翻译起来也省了不少力,也很感谢原作者。

突然想起一句话,叫做我们不生产代码,只是代码的搬运工!

~N3DNC7EUHMBVOXE}$XIUY5

 这里贴上我翻译好的其中几个很重要的组件的代码:

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

 

下章贴出源码哈,待我稍稍整理下,我想这也是各位看官最关心的事儿吧。

6W~5A7[Y}WVQ1AJBO(BJIEI

转载于:https://www.cnblogs.com/CallMeUncle/p/6139532.html

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐