Xamarin Android 打造屬於本身的博客園APP(3)

 

打造通用下拉刷新上拉加載更多組件

android開發中最經常使用的就是列表組件,如ListView,recycleView,用到它們感受就會涉及到數據更新,分頁加載。java

最開始的時候,刷新組件我是在技術羣裏頭找了一個被人綁定好的庫,是綁定的github上一個星星不少的java原生組件。可是demo很簡單,對於當時小白的我懵逼了,不曉得咋個用,並且一直以爲banding的庫總感受有問題,就想着直接找一個java的庫翻譯成C#版本的。功夫不負苦心人,在csdn上找到了一篇 http://blog.csdn.net/zhongkejingwang/article/details/38868463android

寫的很詳細,翻譯起來也省了很多力,也很感謝原做者。git

忽然想起一句話,叫作咱們不生產代碼,只是代碼的搬運工!github

~N3DNC7EUHMBVOXE}$XIUY5

 這裏貼上我翻譯好的其中幾個很重要的組件的代碼:web

1.PullToRefreshLayoutasync

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.PullableListViewide

而後接下來就是一個自定義實現的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

相關文章
相關標籤/搜索