下拉刷新_下拉刷新控件 - CSDN
下拉刷新 订阅
下拉刷新,中文词语,用于智能手机、ipad等终端。即向下拉重新加载、刷新。 展开全文
下拉刷新,中文词语,用于智能手机、ipad等终端。即向下拉重新加载、刷新。
信息
外文名
Pull down to refresh
汉语拼音读音
xialashuaxin
中文名
下拉刷新
词    义
动词
下拉刷新原理
首先看看下拉列表的组成部分。如右图,下拉列表就是UITableView了,在UITableView中添加一个子View,用来显示下拉刷新的状态,我把它叫做HeaderView,HeaderView初始化frame的位置是{0,-60,0,60},所以正常情况下我们看不到HeaderView,当用户下拉列表时HeaderView就会显示出来。第二张图的数值表示的是初始化的时候,不是下拉时的数值,为了方便看到HeaderView,便于理解,所以把列表下拉后标注数值。UITableView在顶部时UIScrollView的contentoffset.y=0,在用户下拉滑动列表时,contentoffset.y为负数增大,当contentoffset.y <= -65时表示HeaderView已经完全显示出来了,此时HeaderView便显示“松开刷新”并把下拉的箭头图标向上。 此时若用户松开手,停止下拉,HeaderView的状态就改为等待数据的状态,如下图所示(下图的contentoffset.y=0标错了,是=-60)以上就是顶部下拉刷新数据的原理了,根据这个原理也就不难写出底部上拉刷新数据的实现了。
收起全文
精华内容
参与话题
  • 下拉刷新超简单

    千次阅读 2016-06-15 10:37:13
    最近项目中需要用到ListView下拉刷新的功能,一开始想图省事,在网上直接找一个现成的,可是尝试了网上多个版本的下拉刷新之后发现效果都不怎么理想。有些是因为功能不完整或有Bug,有些是因为使用起来太复杂,...

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/9255575

    最近项目中需要用到ListView下拉刷新的功能,一开始想图省事,在网上直接找一个现成的,可是尝试了网上多个版本的下拉刷新之后发现效果都不怎么理想。有些是因为功能不完整或有Bug,有些是因为使用起来太复杂,十全十美的还真没找到。因此我也是放弃了在网上找现成代码的想法,自己花功夫编写了一种非常简单的下拉刷新实现方案,现在拿出来和大家分享一下。相信在阅读完本篇文章之后,大家都可以在自己的项目中一分钟引入下拉刷新功能。

    首先讲一下实现原理。这里我们将采取的方案是使用组合View的方式,先自定义一个布局继承自LinearLayout,然后在这个布局中加入下拉头和ListView这两个子元素,并让这两个子元素纵向排列。初始化的时候,让下拉头向上偏移出屏幕,这样我们看到的就只有ListView了。然后对ListView的touch事件进行监听,如果当前ListView已经滚动到顶部并且手指还在向下拉的话,那就将下拉头显示出来,松手后进行刷新操作,并将下拉头隐藏。原理示意图如下:

                                

    那我们现在就来动手实现一下,新建一个项目起名叫PullToRefreshTest,先在项目中定义一个下拉头的布局文件pull_to_refresh.xml,代码如下所示:

    [html] view plaincopy
    1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    2.     xmlns:tools="http://schemas.android.com/tools"  
    3.     android:id="@+id/pull_to_refresh_head"  
    4.     android:layout_width="fill_parent"  
    5.     android:layout_height="60dip" >  
    6.   
    7.     <LinearLayout  
    8.         android:layout_width="200dip"  
    9.         android:layout_height="60dip"  
    10.         android:layout_centerInParent="true"  
    11.         android:orientation="horizontal" >  
    12.   
    13.         <RelativeLayout  
    14.             android:layout_width="0dip"  
    15.             android:layout_height="60dip"  
    16.             android:layout_weight="3"  
    17.             >  
    18.             <ImageView   
    19.                 android:id="@+id/arrow"  
    20.                 android:layout_width="wrap_content"  
    21.                 android:layout_height="wrap_content"  
    22.                 android:layout_centerInParent="true"  
    23.                 android:src="@drawable/arrow"  
    24.                 />  
    25.             <ProgressBar   
    26.                 android:id="@+id/progress_bar"  
    27.                 android:layout_width="30dip"  
    28.                 android:layout_height="30dip"  
    29.                 android:layout_centerInParent="true"  
    30.                 android:visibility="gone"  
    31.                 />  
    32.         </RelativeLayout>  
    33.   
    34.         <LinearLayout  
    35.             android:layout_width="0dip"  
    36.             android:layout_height="60dip"  
    37.             android:layout_weight="12"  
    38.             android:orientation="vertical" >  
    39.   
    40.             <TextView  
    41.                 android:id="@+id/description"  
    42.                 android:layout_width="fill_parent"  
    43.                 android:layout_height="0dip"  
    44.                 android:layout_weight="1"  
    45.                 android:gravity="center_horizontal|bottom"  
    46.                 android:text="@string/pull_to_refresh" />  
    47.   
    48.             <TextView  
    49.                 android:id="@+id/updated_at"  
    50.                 android:layout_width="fill_parent"  
    51.                 android:layout_height="0dip"  
    52.                 android:layout_weight="1"  
    53.                 android:gravity="center_horizontal|top"  
    54.                 android:text="@string/updated_at" />  
    55.         </LinearLayout>  
    56.     </LinearLayout>  
    57.   
    58. </RelativeLayout>  
    在这个布局中,我们包含了一个下拉指示箭头,一个下拉状态文字提示,和一个上次更新的时间。当然,还有一个隐藏的旋转进度条,只有正在刷新的时候我们才会将它显示出来。
    布局中所有引用的字符串我们都放在strings.xml中,如下所示:
    [html] view plaincopy
    1. <?xml version="1.0" encoding="utf-8"?>  
    2. <resources>  
    3.   
    4.     <string name="app_name">PullToRefreshTest</string>  
    5.     <string name="pull_to_refresh">下拉可以刷新</string>  
    6.     <string name="release_to_refresh">释放立即刷新</string>  
    7.     <string name="refreshing">正在刷新…</string>  
    8.     <string name="not_updated_yet">暂未更新过</string>  
    9.     <string name="updated_at">上次更新于%1$s前</string>  
    10.     <string name="updated_just_now">刚刚更新</string>  
    11.     <string name="time_error">时间有问题</string>  
    12.       
    13. </resources>  
    然后新建一个RefreshableView继承自LinearLayout,代码如下所示:
    [java] view plaincopy
    1. public class RefreshableView extends LinearLayout implements OnTouchListener {  
    2.   
    3.     /** 
    4.      * 下拉状态 
    5.      */  
    6.     public static final int STATUS_PULL_TO_REFRESH = 0;  
    7.   
    8.     /** 
    9.      * 释放立即刷新状态 
    10.      */  
    11.     public static final int STATUS_RELEASE_TO_REFRESH = 1;  
    12.   
    13.     /** 
    14.      * 正在刷新状态 
    15.      */  
    16.     public static final int STATUS_REFRESHING = 2;  
    17.   
    18.     /** 
    19.      * 刷新完成或未刷新状态 
    20.      */  
    21.     public static final int STATUS_REFRESH_FINISHED = 3;  
    22.   
    23.     /** 
    24.      * 下拉头部回滚的速度 
    25.      */  
    26.     public static final int SCROLL_SPEED = -20;  
    27.   
    28.     /** 
    29.      * 一分钟的毫秒值,用于判断上次的更新时间 
    30.      */  
    31.     public static final long ONE_MINUTE = 60 * 1000;  
    32.   
    33.     /** 
    34.      * 一小时的毫秒值,用于判断上次的更新时间 
    35.      */  
    36.     public static final long ONE_HOUR = 60 * ONE_MINUTE;  
    37.   
    38.     /** 
    39.      * 一天的毫秒值,用于判断上次的更新时间 
    40.      */  
    41.     public static final long ONE_DAY = 24 * ONE_HOUR;  
    42.   
    43.     /** 
    44.      * 一月的毫秒值,用于判断上次的更新时间 
    45.      */  
    46.     public static final long ONE_MONTH = 30 * ONE_DAY;  
    47.   
    48.     /** 
    49.      * 一年的毫秒值,用于判断上次的更新时间 
    50.      */  
    51.     public static final long ONE_YEAR = 12 * ONE_MONTH;  
    52.   
    53.     /** 
    54.      * 上次更新时间的字符串常量,用于作为SharedPreferences的键值 
    55.      */  
    56.     private static final String UPDATED_AT = "updated_at";  
    57.   
    58.     /** 
    59.      * 下拉刷新的回调接口 
    60.      */  
    61.     private PullToRefreshListener mListener;  
    62.   
    63.     /** 
    64.      * 用于存储上次更新时间 
    65.      */  
    66.     private SharedPreferences preferences;  
    67.   
    68.     /** 
    69.      * 下拉头的View 
    70.      */  
    71.     private View header;  
    72.   
    73.     /** 
    74.      * 需要去下拉刷新的ListView 
    75.      */  
    76.     private ListView listView;  
    77.   
    78.     /** 
    79.      * 刷新时显示的进度条 
    80.      */  
    81.     private ProgressBar progressBar;  
    82.   
    83.     /** 
    84.      * 指示下拉和释放的箭头 
    85.      */  
    86.     private ImageView arrow;  
    87.   
    88.     /** 
    89.      * 指示下拉和释放的文字描述 
    90.      */  
    91.     private TextView description;  
    92.   
    93.     /** 
    94.      * 上次更新时间的文字描述 
    95.      */  
    96.     private TextView updateAt;  
    97.   
    98.     /** 
    99.      * 下拉头的布局参数 
    100.      */  
    101.     private MarginLayoutParams headerLayoutParams;  
    102.   
    103.     /** 
    104.      * 上次更新时间的毫秒值 
    105.      */  
    106.     private long lastUpdateTime;  
    107.   
    108.     /** 
    109.      * 为了防止不同界面的下拉刷新在上次更新时间上互相有冲突,使用id来做区分 
    110.      */  
    111.     private int mId = -1;  
    112.   
    113.     /** 
    114.      * 下拉头的高度 
    115.      */  
    116.     private int hideHeaderHeight;  
    117.   
    118.     /** 
    119.      * 当前处理什么状态,可选值有STATUS_PULL_TO_REFRESH, STATUS_RELEASE_TO_REFRESH, 
    120.      * STATUS_REFRESHING 和 STATUS_REFRESH_FINISHED 
    121.      */  
    122.     private int currentStatus = STATUS_REFRESH_FINISHED;;  
    123.   
    124.     /** 
    125.      * 记录上一次的状态是什么,避免进行重复操作 
    126.      */  
    127.     private int lastStatus = currentStatus;  
    128.   
    129.     /** 
    130.      * 手指按下时的屏幕纵坐标 
    131.      */  
    132.     private float yDown;  
    133.   
    134.     /** 
    135.      * 在被判定为滚动之前用户手指可以移动的最大值。 
    136.      */  
    137.     private int touchSlop;  
    138.   
    139.     /** 
    140.      * 是否已加载过一次layout,这里onLayout中的初始化只需加载一次 
    141.      */  
    142.     private boolean loadOnce;  
    143.   
    144.     /** 
    145.      * 当前是否可以下拉,只有ListView滚动到头的时候才允许下拉 
    146.      */  
    147.     private boolean ableToPull;  
    148.   
    149.     /** 
    150.      * 下拉刷新控件的构造函数,会在运行时动态添加一个下拉头的布局。 
    151.      *  
    152.      * @param context 
    153.      * @param attrs 
    154.      */  
    155.     public RefreshableView(Context context, AttributeSet attrs) {  
    156.         super(context, attrs);  
    157.         preferences = PreferenceManager.getDefaultSharedPreferences(context);  
    158.         header = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh, nulltrue);  
    159.         progressBar = (ProgressBar) header.findViewById(R.id.progress_bar);  
    160.         arrow = (ImageView) header.findViewById(R.id.arrow);  
    161.         description = (TextView) header.findViewById(R.id.description);  
    162.         updateAt = (TextView) header.findViewById(R.id.updated_at);  
    163.         touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();  
    164.         refreshUpdatedAtValue();  
    165.         setOrientation(VERTICAL);  
    166.         addView(header, 0);  
    167.     }  
    168.   
    169.     /** 
    170.      * 进行一些关键性的初始化操作,比如:将下拉头向上偏移进行隐藏,给ListView注册touch事件。 
    171.      */  
    172.     @Override  
    173.     protected void onLayout(boolean changed, int l, int t, int r, int b) {  
    174.         super.onLayout(changed, l, t, r, b);  
    175.         if (changed && !loadOnce) {  
    176.             hideHeaderHeight = -header.getHeight();  
    177.             headerLayoutParams = (MarginLayoutParams) header.getLayoutParams();  
    178.             headerLayoutParams.topMargin = hideHeaderHeight;  
    179.             listView = (ListView) getChildAt(1);  
    180.             listView.setOnTouchListener(this);  
    181.             loadOnce = true;  
    182.         }  
    183.     }  
    184.   
    185.     /** 
    186.      * 当ListView被触摸时调用,其中处理了各种下拉刷新的具体逻辑。 
    187.      */  
    188.     @Override  
    189.     public boolean onTouch(View v, MotionEvent event) {  
    190.         setIsAbleToPull(event);  
    191.         if (ableToPull) {  
    192.             switch (event.getAction()) {  
    193.             case MotionEvent.ACTION_DOWN:  
    194.                 yDown = event.getRawY();  
    195.                 break;  
    196.             case MotionEvent.ACTION_MOVE:  
    197.                 float yMove = event.getRawY();  
    198.                 int distance = (int) (yMove - yDown);  
    199.                 // 如果手指是下滑状态,并且下拉头是完全隐藏的,就屏蔽下拉事件  
    200.                 if (distance <= 0 && headerLayoutParams.topMargin <= hideHeaderHeight) {  
    201.                     return false;  
    202.                 }  
    203.                 if (distance < touchSlop) {  
    204.                     return false;  
    205.                 }  
    206.                 if (currentStatus != STATUS_REFRESHING) {  
    207.                     if (headerLayoutParams.topMargin > 0) {  
    208.                         currentStatus = STATUS_RELEASE_TO_REFRESH;  
    209.                     } else {  
    210.                         currentStatus = STATUS_PULL_TO_REFRESH;  
    211.                     }  
    212.                     // 通过偏移下拉头的topMargin值,来实现下拉效果  
    213.                     headerLayoutParams.topMargin = (distance / 2) + hideHeaderHeight;  
    214.                     header.setLayoutParams(headerLayoutParams);  
    215.                 }  
    216.                 break;  
    217.             case MotionEvent.ACTION_UP:  
    218.             default:  
    219.                 if (currentStatus == STATUS_RELEASE_TO_REFRESH) {  
    220.                     // 松手时如果是释放立即刷新状态,就去调用正在刷新的任务  
    221.                     new RefreshingTask().execute();  
    222.                 } else if (currentStatus == STATUS_PULL_TO_REFRESH) {  
    223.                     // 松手时如果是下拉状态,就去调用隐藏下拉头的任务  
    224.                     new HideHeaderTask().execute();  
    225.                 }  
    226.                 break;  
    227.             }  
    228.             // 时刻记得更新下拉头中的信息  
    229.             if (currentStatus == STATUS_PULL_TO_REFRESH  
    230.                     || currentStatus == STATUS_RELEASE_TO_REFRESH) {  
    231.                 updateHeaderView();  
    232.                 // 当前正处于下拉或释放状态,要让ListView失去焦点,否则被点击的那一项会一直处于选中状态  
    233.                 listView.setPressed(false);  
    234.                 listView.setFocusable(false);  
    235.                 listView.setFocusableInTouchMode(false);  
    236.                 lastStatus = currentStatus;  
    237.                 // 当前正处于下拉或释放状态,通过返回true屏蔽掉ListView的滚动事件  
    238.                 return true;  
    239.             }  
    240.         }  
    241.         return false;  
    242.     }  
    243.   
    244.     /** 
    245.      * 给下拉刷新控件注册一个监听器。 
    246.      *  
    247.      * @param listener 
    248.      *            监听器的实现。 
    249.      * @param id 
    250.      *            为了防止不同界面的下拉刷新在上次更新时间上互相有冲突, 请不同界面在注册下拉刷新监听器时一定要传入不同的id。 
    251.      */  
    252.     public void setOnRefreshListener(PullToRefreshListener listener, int id) {  
    253.         mListener = listener;  
    254.         mId = id;  
    255.     }  
    256.   
    257.     /** 
    258.      * 当所有的刷新逻辑完成后,记录调用一下,否则你的ListView将一直处于正在刷新状态。 
    259.      */  
    260.     public void finishRefreshing() {  
    261.         currentStatus = STATUS_REFRESH_FINISHED;  
    262.         preferences.edit().putLong(UPDATED_AT + mId, System.currentTimeMillis()).commit();  
    263.         new HideHeaderTask().execute();  
    264.     }  
    265.   
    266.     /** 
    267.      * 根据当前ListView的滚动状态来设定 {@link #ableToPull} 
    268.      * 的值,每次都需要在onTouch中第一个执行,这样可以判断出当前应该是滚动ListView,还是应该进行下拉。 
    269.      *  
    270.      * @param event 
    271.      */  
    272.     private void setIsAbleToPull(MotionEvent event) {  
    273.         View firstChild = listView.getChildAt(0);  
    274.         if (firstChild != null) {  
    275.             int firstVisiblePos = listView.getFirstVisiblePosition();  
    276.             if (firstVisiblePos == 0 && firstChild.getTop() == 0) {  
    277.                 if (!ableToPull) {  
    278.                     yDown = event.getRawY();  
    279.                 }  
    280.                 // 如果首个元素的上边缘,距离父布局值为0,就说明ListView滚动到了最顶部,此时应该允许下拉刷新  
    281.                 ableToPull = true;  
    282.             } else {  
    283.                 if (headerLayoutParams.topMargin != hideHeaderHeight) {  
    284.                     headerLayoutParams.topMargin = hideHeaderHeight;  
    285.                     header.setLayoutParams(headerLayoutParams);  
    286.                 }  
    287.                 ableToPull = false;  
    288.             }  
    289.         } else {  
    290.             // 如果ListView中没有元素,也应该允许下拉刷新  
    291.             ableToPull = true;  
    292.         }  
    293.     }  
    294.   
    295.     /** 
    296.      * 更新下拉头中的信息。 
    297.      */  
    298.     private void updateHeaderView() {  
    299.         if (lastStatus != currentStatus) {  
    300.             if (currentStatus == STATUS_PULL_TO_REFRESH) {  
    301.                 description.setText(getResources().getString(R.string.pull_to_refresh));  
    302.                 arrow.setVisibility(View.VISIBLE);  
    303.                 progressBar.setVisibility(View.GONE);  
    304.                 rotateArrow();  
    305.             } else if (currentStatus == STATUS_RELEASE_TO_REFRESH) {  
    306.                 description.setText(getResources().getString(R.string.release_to_refresh));  
    307.                 arrow.setVisibility(View.VISIBLE);  
    308.                 progressBar.setVisibility(View.GONE);  
    309.                 rotateArrow();  
    310.             } else if (currentStatus == STATUS_REFRESHING) {  
    311.                 description.setText(getResources().getString(R.string.refreshing));  
    312.                 progressBar.setVisibility(View.VISIBLE);  
    313.                 arrow.clearAnimation();  
    314.                 arrow.setVisibility(View.GONE);  
    315.             }  
    316.             refreshUpdatedAtValue();  
    317.         }  
    318.     }  
    319.   
    320.     /** 
    321.      * 根据当前的状态来旋转箭头。 
    322.      */  
    323.     private void rotateArrow() {  
    324.         float pivotX = arrow.getWidth() / 2f;  
    325.         float pivotY = arrow.getHeight() / 2f;  
    326.         float fromDegrees = 0f;  
    327.         float toDegrees = 0f;  
    328.         if (currentStatus == STATUS_PULL_TO_REFRESH) {  
    329.             fromDegrees = 180f;  
    330.             toDegrees = 360f;  
    331.         } else if (currentStatus == STATUS_RELEASE_TO_REFRESH) {  
    332.             fromDegrees = 0f;  
    333.             toDegrees = 180f;  
    334.         }  
    335.         RotateAnimation animation = new RotateAnimation(fromDegrees, toDegrees, pivotX, pivotY);  
    336.         animation.setDuration(100);  
    337.         animation.setFillAfter(true);  
    338.         arrow.startAnimation(animation);  
    339.     }  
    340.   
    341.     /** 
    342.      * 刷新下拉头中上次更新时间的文字描述。 
    343.      */  
    344.     private void refreshUpdatedAtValue() {  
    345.         lastUpdateTime = preferences.getLong(UPDATED_AT + mId, -1);  
    346.         long currentTime = System.currentTimeMillis();  
    347.         long timePassed = currentTime - lastUpdateTime;  
    348.         long timeIntoFormat;  
    349.         String updateAtValue;  
    350.         if (lastUpdateTime == -1) {  
    351.             updateAtValue = getResources().getString(R.string.not_updated_yet);  
    352.         } else if (timePassed < 0) {  
    353.             updateAtValue = getResources().getString(R.string.time_error);  
    354.         } else if (timePassed < ONE_MINUTE) {  
    355.             updateAtValue = getResources().getString(R.string.updated_just_now);  
    356.         } else if (timePassed < ONE_HOUR) {  
    357.             timeIntoFormat = timePassed / ONE_MINUTE;  
    358.             String value = timeIntoFormat + "分钟";  
    359.             updateAtValue = String.format(getResources().getString(R.string.updated_at), value);  
    360.         } else if (timePassed < ONE_DAY) {  
    361.             timeIntoFormat = timePassed / ONE_HOUR;  
    362.             String value = timeIntoFormat + "小时";  
    363.             updateAtValue = String.format(getResources().getString(R.string.updated_at), value);  
    364.         } else if (timePassed < ONE_MONTH) {  
    365.             timeIntoFormat = timePassed / ONE_DAY;  
    366.             String value = timeIntoFormat + "天";  
    367.             updateAtValue = String.format(getResources().getString(R.string.updated_at), value);  
    368.         } else if (timePassed < ONE_YEAR) {  
    369.             timeIntoFormat = timePassed / ONE_MONTH;  
    370.             String value = timeIntoFormat + "个月";  
    371.             updateAtValue = String.format(getResources().getString(R.string.updated_at), value);  
    372.         } else {  
    373.             timeIntoFormat = timePassed / ONE_YEAR;  
    374.             String value = timeIntoFormat + "年";  
    375.             updateAtValue = String.format(getResources().getString(R.string.updated_at), value);  
    376.         }  
    377.         updateAt.setText(updateAtValue);  
    378.     }  
    379.   
    380.     /** 
    381.      * 正在刷新的任务,在此任务中会去回调注册进来的下拉刷新监听器。 
    382.      *  
    383.      * @author guolin 
    384.      */  
    385.     class RefreshingTask extends AsyncTask<Void, Integer, Void> {  
    386.   
    387.         @Override  
    388.         protected Void doInBackground(Void... params) {  
    389.             int topMargin = headerLayoutParams.topMargin;  
    390.             while (true) {  
    391.                 topMargin = topMargin + SCROLL_SPEED;  
    392.                 if (topMargin <= 0) {  
    393.                     topMargin = 0;  
    394.                     break;  
    395.                 }  
    396.                 publishProgress(topMargin);  
    397.                 sleep(10);  
    398.             }  
    399.             currentStatus = STATUS_REFRESHING;  
    400.             publishProgress(0);  
    401.             if (mListener != null) {  
    402.                 mListener.onRefresh();  
    403.             }  
    404.             return null;  
    405.         }  
    406.   
    407.         @Override  
    408.         protected void onProgressUpdate(Integer... topMargin) {  
    409.             updateHeaderView();  
    410.             headerLayoutParams.topMargin = topMargin[0];  
    411.             header.setLayoutParams(headerLayoutParams);  
    412.         }  
    413.   
    414.     }  
    415.   
    416.     /** 
    417.      * 隐藏下拉头的任务,当未进行下拉刷新或下拉刷新完成后,此任务将会使下拉头重新隐藏。 
    418.      *  
    419.      * @author guolin 
    420.      */  
    421.     class HideHeaderTask extends AsyncTask<Void, Integer, Integer> {  
    422.   
    423.         @Override  
    424.         protected Integer doInBackground(Void... params) {  
    425.             int topMargin = headerLayoutParams.topMargin;  
    426.             while (true) {  
    427.                 topMargin = topMargin + SCROLL_SPEED;  
    428.                 if (topMargin <= hideHeaderHeight) {  
    429.                     topMargin = hideHeaderHeight;  
    430.                     break;  
    431.                 }  
    432.                 publishProgress(topMargin);  
    433.                 sleep(10);  
    434.             }  
    435.             return topMargin;  
    436.         }  
    437.   
    438.         @Override  
    439.         protected void onProgressUpdate(Integer... topMargin) {  
    440.             headerLayoutParams.topMargin = topMargin[0];  
    441.             header.setLayoutParams(headerLayoutParams);  
    442.         }  
    443.   
    444.         @Override  
    445.         protected void onPostExecute(Integer topMargin) {  
    446.             headerLayoutParams.topMargin = topMargin;  
    447.             header.setLayoutParams(headerLayoutParams);  
    448.             currentStatus = STATUS_REFRESH_FINISHED;  
    449.         }  
    450.     }  
    451.   
    452.     /** 
    453.      * 使当前线程睡眠指定的毫秒数。 
    454.      *  
    455.      * @param time 
    456.      *            指定当前线程睡眠多久,以毫秒为单位 
    457.      */  
    458.     private void sleep(int time) {  
    459.         try {  
    460.             Thread.sleep(time);  
    461.         } catch (InterruptedException e) {  
    462.             e.printStackTrace();  
    463.         }  
    464.     }  
    465.   
    466.     /** 
    467.      * 下拉刷新的监听器,使用下拉刷新的地方应该注册此监听器来获取刷新回调。 
    468.      *  
    469.      * @author guolin 
    470.      */  
    471.     public interface PullToRefreshListener {  
    472.   
    473.         /** 
    474.          * 刷新时会去回调此方法,在方法内编写具体的刷新逻辑。注意此方法是在子线程中调用的, 你可以不必另开线程来进行耗时操作。 
    475.          */  
    476.         void onRefresh();  
    477.   
    478.     }  
    479.   
    480. }  

    这个类是整个下拉刷新功能中最重要的一个类,注释已经写得比较详细了,我再简单解释一下。首先在RefreshableView的构造函数中动态添加了刚刚定义的pull_to_refresh这个布局作为下拉头,然后在onLayout方法中将下拉头向上偏移出了屏幕,再给ListView注册了touch事件。之后每当手指在ListView上滑动时,onTouch方法就会执行。在onTouch方法中的第一行就调用了setIsAbleToPull方法来判断ListView是否滚动到了最顶部,只有滚动到了最顶部才会执行后面的代码,否则就视为正常的ListView滚动,不做任何处理。当ListView滚动到了最顶部时,如果手指还在向下拖动,就会改变下拉头的偏移值,让下拉头显示出来,下拉的距离设定为手指移动距离的1/2,这样才会有拉力的感觉。如果下拉的距离足够大,在松手的时候就会执行刷新操作,如果距离不够大,就仅仅重新隐藏下拉头。

    具体的刷新操作会在RefreshingTask中进行,其中在doInBackground方法中回调了PullToRefreshListener接口的onRefresh方法,这也是大家在使用RefreshableView时必须要去实现的一个接口,因为具体刷新的逻辑就应该写在onRefresh方法中,后面会演示使用的方法。

    另外每次在下拉的时候都还会调用updateHeaderView方法来改变下拉头中的数据,比如箭头方向的旋转,下拉文字描述的改变等。更加深入的理解请大家仔细去阅读RefreshableView中的代码。
    现在我们已经把下拉刷新的所有功能都完成了,接下来就要看一看如何在项目中引入下拉刷新了。打开或新建activity_main.xml作为程序主界面的布局,加入如下代码:
    [html] view plaincopy
    1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    2.     xmlns:tools="http://schemas.android.com/tools"  
    3.     android:layout_width="match_parent"  
    4.     android:layout_height="match_parent"  
    5.     tools:context=".MainActivity" >  
    6.   
    7.     <com.example.pulltorefreshtest.RefreshableView  
    8.         android:id="@+id/refreshable_view"  
    9.         android:layout_width="fill_parent"  
    10.         android:layout_height="fill_parent" >  
    11.   
    12.         <ListView  
    13.             android:id="@+id/list_view"  
    14.             android:layout_width="fill_parent"  
    15.             android:layout_height="fill_parent" >  
    16.         </ListView>  
    17.     </com.example.pulltorefreshtest.RefreshableView>  
    18.   
    19. </RelativeLayout>  
    可以看到,我们在自定义的RefreshableView中加入了一个ListView,这就意味着给这个ListView加入了下拉刷新的功能,就是这么简单!
    然后我们再来看一下程序的主Activity,打开或新建MainActivity,加入如下代码:
    [java] view plaincopy
    1. public class MainActivity extends Activity {  
    2.   
    3.     RefreshableView refreshableView;  
    4.     ListView listView;  
    5.     ArrayAdapter<String> adapter;  
    6.     String[] items = { "A""B""C""D""E""F""G""H""I""J""K""L" };  
    7.   
    8.     @Override  
    9.     protected void onCreate(Bundle savedInstanceState) {  
    10.         super.onCreate(savedInstanceState);  
    11.         requestWindowFeature(Window.FEATURE_NO_TITLE);  
    12.         setContentView(R.layout.activity_main);  
    13.         refreshableView = (RefreshableView) findViewById(R.id.refreshable_view);  
    14.         listView = (ListView) findViewById(R.id.list_view);  
    15.         adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, items);  
    16.         listView.setAdapter(adapter);  
    17.         refreshableView.setOnRefreshListener(new PullToRefreshListener() {  
    18.             @Override  
    19.             public void onRefresh() {  
    20.                 try {  
    21.                     Thread.sleep(3000);  
    22.                 } catch (InterruptedException e) {  
    23.                     e.printStackTrace();  
    24.                 }  
    25.                 refreshableView.finishRefreshing();  
    26.             }  
    27.         }, 0);  
    28.     }  
    29.   
    30. }  

    可以看到,我们通过调用RefreshableView的setOnRefreshListener方法注册了一个监听器,当ListView正在刷新时就会回调监听器的onRefresh方法,刷新的具体逻辑就在这里处理。而且这个方法已经自动开启了线程,可以直接在onRefresh方法中进行耗时操作,比如向服务器请求最新数据等,在这里我就简单让线程睡眠3秒钟。另外在onRefresh方法的最后,一定要调用RefreshableView中的finishRefreshing方法,这个方法是用来通知RefreshableView刷新结束了,不然我们的ListView将一直处于正在刷新的状态。
    不知道大家有没有注意到,setOnRefreshListener这个方法其实是有两个参数的,我们刚刚也是传入了一个不起眼的0。那这第二个参数是用来做什么的呢?由于RefreshableView比较智能,它会自动帮我们记录上次刷新完成的时间,然后下拉的时候会在下拉头中显示距上次刷新已过了多久。这是一个非常好用的功能,让我们不用再自己手动去记录和计算时间了,但是却存在一个问题。如果当前我们的项目中有三个地方都使用到了下拉刷新的功能,现在在一处进行了刷新,其它两处的时间也都会跟着改变!因为刷新完成的时间是记录在配置文件中的,由于在一处刷新更改了配置文件,导致在其它两处读取到的配置文件时间已经是更改过的了。那解决方案是什么?就是每个用到下拉刷新的地方,给setOnRefreshListener方法的第二个参数中传入不同的id就行了。这样各处的上次刷新完成时间都是单独记录的,相互之间就不会再有影响。
    好了,全部的代码都在这里了,让我们来运行一下,看看效果吧。

                                                     
    效果看起来还是非常不错的。我们最后再来总结一下,在项目中引入ListView下拉刷新功能只需三步:

    1. 在Activity的布局文件中加入自定义的RefreshableView,并让ListView包含在其中。

    2. 在Activity中调用RefreshableView的setOnRefreshListener方法注册回调接口。

    3. 在onRefresh方法的最后,记得调用RefreshableView的finishRefreshing方法,通知刷新结束。

    从此以后,在项目的任何地方,一分钟引入下拉刷新功能妥妥的。

    展开全文
  • 下拉刷新功能实现

    2015-12-31 13:36:27
    总的来说下拉刷新的功能实现较为简单,主要内容是对屏幕的一个滑动监听,并提供一个接口在适当的位置进行更新服务器数据。 在本项目中使用给ListView添加ListViewHeader的方式,添加一个View,并在开始时进行隐藏,在...

    总的来说下拉刷新的功能实现较为简单,主要内容是对屏幕的一个滑动监听,并提供一个接口在适当的位置进行更新服务器数据。

    在本项目中使用给ListView添加ListViewHeader的方式,添加一个View,并在开始时进行隐藏,在检测到向下滑动时,拉出View控件,并更新数据。

    效果图;


    下拉刷新XML布局文件:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="5dp"
        android:orientation="horizontal" >
        
        <FrameLayout 
            
              android:layout_width="wrap_content"
       		  android:layout_height="wrap_content"
       		  android:layout_weight="1" 
            >
        
        <ImageView 
            android:id="@+id/iv_reflash_arr"
            android:layout_width="wrap_content"
       		android:layout_height="wrap_content"
       		android:src="@drawable/common_listview_headview_red_arrow"  		
       		android:layout_gravity="center"  
       		
       		android:layout_marginTop="7dp"		
            />
    
        <ProgressBar
            android:id="@+id/pb_reflash_ProgressBar"
            style="?android:attr/progressBarStyleLarge"
            android:layout_width="58dp"
            android:layout_height="58dp"
          	android:visibility="invisible"
          	android:indeterminateDrawable="@drawable/custom_progress"
            android:layout_gravity="center" />    
       </FrameLayout>
        
        
        
        <LinearLayout         
        	android:layout_width="0dp"
       		android:layout_height="wrap_content" 
       		android:layout_weight="8"
       		android:orientation="vertical"       
            >
            <TextView 
                android:id="@+id/tv_reflash_content"
                android:layout_width="wrap_content"
       			android:layout_height="wrap_content" 
       			android:text="下拉刷新" 
       			android:textColor="@android:color/holo_red_light"
       			android:layout_gravity="center"
       			android:textSize="22sp"
                />
             
            <TextView 
                android:id="@+id/tv_reflash_date"
                android:layout_width="wrap_content"
       			android:layout_height="wrap_content" 
       			android:text="2015-12-30 13:40:25"  
       			android:layout_gravity="center"
       			android:layout_marginTop="7dp"
       			android:textColor="@android:color/darker_gray"
       			android:textSize="16sp"   			
                />   
                    
        </LinearLayout>
        
    
    </LinearLayout>
    


    项目里介绍了一个非常好的结构方式,即自定义一个ListView:RefreshListView,并在内部进行相应配置。

    package com.example.zhihuibj.view;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    import com.example.zhihuibj.R;
    
    import android.content.Context;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    import android.view.View;
    import android.view.animation.Animation;
    import android.view.animation.RotateAnimation;
    import android.widget.ImageView;
    import android.widget.ListView;
    import android.widget.ProgressBar;
    import android.widget.TextView;
    
    public class RefreshListView extends ListView {
    
    	
    	private static final int STATE_PULL_REFRESH=0;//下拉刷新
    	private static final int STATE_RELEASH_REFRESH=1;//松开刷新
    	private static final int STATE_REFRESHING=2;//正在刷新
    	
    	private int mCurrentState=STATE_PULL_REFRESH;
    	private View mHeaderView;
    	private int mHeaderViewHeight;
    	private int startY=-1;
    	private TextView reflash_content;
    	private TextView  reflash_data;
    	private ImageView reflash_arr;
    	private ProgressBar reflash_ProgressBar;
    	private RotateAnimation Animation_up;
    	private RotateAnimation Animation_down;
    
    	public RefreshListView(Context context, AttributeSet attrs, int defStyle) {
    		super(context, attrs, defStyle);
    		InitHeaderView();
    	}	
    	
    	public RefreshListView(Context context, AttributeSet attrs) {
    		super(context, attrs);
    		InitHeaderView();
    	}	
    	
    	public RefreshListView(Context context) {
    		super(context);
    		InitHeaderView();
    	}	
    	
    	/*
    	 * 初始化头布局
    	 */
    	private void InitHeaderView() {
    		//实例化刷新列表
    		mHeaderView = View.inflate(getContext(), R.layout.reflash, null);
    		//添加一个头布局
    		this.addHeaderView(mHeaderView);
    		//默认隐藏下拉列表
    		mHeaderView.measure(0, 0);
    		mHeaderViewHeight = mHeaderView.getMeasuredHeight();
    		mHeaderView.setPadding(0, -mHeaderViewHeight, 0, 0);
    		
    		
    		 reflash_content =(TextView) mHeaderView.findViewById(R.id.tv_reflash_content);
    		 reflash_data= (TextView)mHeaderView.findViewById(R.id.tv_reflash_date);
    		 reflash_arr=(ImageView)mHeaderView.findViewById(R.id.iv_reflash_arr);
    		 reflash_ProgressBar =(ProgressBar)mHeaderView.findViewById(R.id.pb_reflash_ProgressBar);
    		 
    		 InitAnimation();
    		 reflash_data.setText(GetCurrentTime());
    		
    	}
    	
    	@Override
    	public boolean onTouchEvent(MotionEvent ev) {
    		
    		
    		//System.out.println("触摸屏幕!!!");
    		switch (ev.getAction()) {
    		/*
    		 * 模拟器无法检测ACTION_DOWN事件
    		 */
    		case MotionEvent.ACTION_DOWN:			
    			 startY =(int) ev.getRawY();
    			// System.out.println("ACTION_DOWN : "+startY);
    			break;
    		/*
    		 * 	测量手指移动距离,并下移下拉列表
    		 */
    		case MotionEvent.ACTION_MOVE:
    			
    			
    			if(mCurrentState==STATE_REFRESHING)
    				break;
    			
    			while(startY==-1)
    			{
    				startY =(int) ev.getRawY();//确保startY有效				
    			}			
    			int endY= (int)ev.getRawY(); //得到重点坐标			
    			int dy = endY-startY;//移动偏移量
    			if(dy>0&&getFirstVisiblePosition()==0)//只有下拉并且是第一个item,才允许下拉			
    			{
    				int padding=dy-mHeaderViewHeight;
    				mHeaderView.setPadding(0, padding, 0, 0);
    				
    				if(padding>0&&mCurrentState!=STATE_RELEASH_REFRESH){//padding大于0,改为松开刷新
    					mCurrentState=STATE_RELEASH_REFRESH;
    					RefreshData(mCurrentState);
    				}
    				else if(padding<0&&mCurrentState!=STATE_PULL_REFRESH){//padding<0,改为下拉刷新
    					mCurrentState=STATE_PULL_REFRESH;
    					RefreshData(mCurrentState);
    				}
    				return true;
    			}		
    			break;				
    			
    		case MotionEvent.ACTION_UP:				
    			startY=-1;
    			
    			if(mCurrentState==STATE_RELEASH_REFRESH)//松开刷新状态时,手指离开屏幕,进入正在刷新状态
    			{
    				mCurrentState=STATE_REFRESHING;
    				RefreshData(mCurrentState);
    				mHeaderView.setPadding(0, 0, 0, 0);
    			}
    			else if(mCurrentState==STATE_PULL_REFRESH)//下拉刷新时,松开手指,恢复原状态
    			{
    				mHeaderView.setPadding(0, -mHeaderViewHeight, 0, 0);
    			}	
    			break;
    		default:
    			break;
    		}		
    		
    		return super.onTouchEvent(ev);
    	}
    
    	private void RefreshData(int state) {
    		switch (state) {
    		case STATE_PULL_REFRESH:
    			reflash_content.setText("下拉刷新");
    			reflash_ProgressBar.setVisibility(View.INVISIBLE);
    			reflash_arr.setVisibility(View.VISIBLE);
    			reflash_arr.startAnimation(Animation_down);
    			break;
    		case STATE_RELEASH_REFRESH:
    			reflash_content.setText("松开刷新");
    			reflash_arr.clearAnimation();//切换下一个动画时,需要先清除动画
    			reflash_arr.startAnimation(Animation_up);
    			reflash_ProgressBar.setVisibility(View.INVISIBLE);
    			reflash_arr.setVisibility(View.VISIBLE);
    		
    			break;
    		case STATE_REFRESHING:
    			reflash_content.setText("正在刷新...");			
    			reflash_ProgressBar.setVisibility(View.VISIBLE);
    			reflash_arr.clearAnimation();//隐藏箭头图标时,需要先清除动画
    			reflash_arr.setVisibility(View.INVISIBLE);	
    			
    			if(mListener!=null)
    			mListener.onRefresh();
    			
    			break;
    
    		default:
    			break;
    		}
    		
    	}
    	
    	
    	
    	public void setOnRefreshListener(OnRefreshListener listener)
    	{
    		mListener=listener;
    	}
    	public interface OnRefreshListener{
    		public void onRefresh();
    	}
    	OnRefreshListener mListener;
    	/*
    	 * 收起下拉刷新控件
    	 */
    	public void onRefreshComplete(boolean success){
    		mCurrentState=STATE_PULL_REFRESH;
    		reflash_content.setText("下拉刷新");
    		reflash_ProgressBar.setVisibility(View.INVISIBLE);
    		reflash_arr.setVisibility(View.VISIBLE);
    		mHeaderView.setPadding(0, -mHeaderViewHeight, 0, 0);
    		if(success)
    		reflash_data.setText(GetCurrentTime());
    		
    	}
    	
    	/*
    	 * 获取当前时间
    	 */
    	public String GetCurrentTime()
    	{
    		SimpleDateFormat Format  = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    		return Format.format(new Date());
    	}
    	
    	/*
    	 * 箭头动画
    	 */
    	public void InitAnimation(){
    		Animation_up=new RotateAnimation(0, -180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
    		Animation_up.setDuration(100);
    		Animation_up.setFillAfter(true);
    		
    		Animation_down=new RotateAnimation(-180, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
    		Animation_down.setDuration(100);
    		Animation_down.setFillAfter(true);
    	}
    
    	
    
    
    	
    
    }
    

    ontouchEvent :实现了对屏幕滑动的监听

    View.measure:实现了对控件大小的测量

    View.padding:实现了对下拉刷新View的位置控制

    需要注意到的是;

    	public void setOnRefreshListener(OnRefreshListener listener)
    	{
    		mListener=listener;
    	}
    	public interface OnRefreshListener{
    		public void onRefresh();
    	}
    	OnRefreshListener mListener;


    这里作为一个接口向外部提供了一个下拉刷新的函数,方便进行操作。在外部进行如下调用即可。lv_tablist,是在另一个文件中通过FindViewById获得到的listView
    //设置下拉刷新监听
    		lv_tablist.setOnRefreshListener(new OnRefreshListener() {
    			
    			@Override
    			public void onRefresh() {
    				TabDetailPagerGetDataFromService();
    			}
    		});

    经过上面即实现了下拉刷新的功能。


    展开全文
  • 上拉刷新和下拉刷新的实现

    千次阅读 2017-11-10 18:22:31
    用recyclerview加上SwipeRefreshLayout再加上recyclerview的自定义footer实现了下拉与上拉刷新,并给出网格布局处理方法,然后对刷新做了相应优化。

    先来两张效果图

             

    关于下拉刷新,Google提供了一个布局SwipRefreshLayout,它里面可以包涵一个滑动控件,然后你可以设置它的刷新事件就OK了,非常简单用。但是上拉刷新就有点麻烦了。网上很多方法都是给recyclerview添加footer的方法,我也采用这种方法实现了一个。其实也就是recyclerview的item多布局。一般布局和footer布局。,在上滑到最后一个item的时候,就把加一个footer类型的item,这里,然后很明显这里也就行滑动监听,就监听它上滑动到最后一个item。下面先。下面直接给代码

    。导入依赖。由于wish我是用了用OKhttp就是实现的联网刷新,所以导入了OKhttp的依赖包,当然还得加联网权限。

     compile 'com.android.support:recyclerview-v7:25.3.1'
        compile 'com.squareup.okhttp:okhttp:2.4.0'
    贴个footer的布局,因为footer包含两个部分“正在加载:”,和没有“没有更多了,”。用用的时候根据情况选择隐藏、显示或者gone
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical" android:layout_width="match_parent"
        android:layout_height="wrap_content">
    
    
        <LinearLayout
            android:id="@+id/line1"
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:gravity="center">
    
            <ProgressBar
                android:layout_width="40dp"
                android:layout_height="40dp"
                />
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="50dp"
                android:gravity="center_vertical"
                android:layout_marginLeft="5dp"
                android:text="正在加载..."
                android:textSize="20dp"/>
    
        </LinearLayout>
    
        <LinearLayout
            android:id="@+id/line2"
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:gravity="center">
    
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="没有更多了!"
                android:textSize="20sp"/>
    
        </LinearLayout>
    
    </LinearLayout>

    最核心的两个类adapter和监听。
    public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{
        private ArrayList<String> list;
        private final int ITEM=0;//加载的为一般item
        private final int FOOT=1;//加载footer
    
        //上拉加载的状态
        private int loadstate=2;//默认加载完成
        public final int LOADING = 1;//正在加载
        public final int FINISH = 2;// 加载完成
        public final int END = 3;  //没有更多数据了(显示另一footer)
    
        public MyAdapter(ArrayList<String> list) {
            this.list = list;
        }
    
        public void setLoadstate(int loadstate) {//用于动态设置加载状态
            this.loadstate = loadstate;
            notifyDataSetChanged();
        }
    
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            //通过加载类型,来显示不同的view。viewType由getItemViewType确定
            if (viewType == ITEM) {
                View view = LayoutInflater.from(parent.getContext())
                        .inflate(R.layout.item, parent, false);
                return new ItemViewHodler(view);
    
            } else if (viewType == FOOT) {
                View view = LayoutInflater.from(parent.getContext())
                        .inflate(R.layout.foot, parent, false);
                return new FootViewHodler(view);
            }
            return null;
        }
    
        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            //绑定viewholder的时候根据类型来绑定
            if (holder instanceof ItemViewHodler) {
                ItemViewHodler itemhodler= (ItemViewHodler) holder;
                itemhodler.textView.setText(list.get(position));
            } else if (holder instanceof FootViewHodler) {
                FootViewHodler foothoder = (FootViewHodler) holder;
                switch (loadstate){
                    case LOADING:
                        foothoder.line1.setVisibility(View.VISIBLE);
                        foothoder.line2.setVisibility(View.GONE);
                        break;
                    case FINISH:
                        //设置成INVISIBLE而不是gone,为了给footer预留位置,美观
                        foothoder.line1.setVisibility(View.INVISIBLE);
                        foothoder.line2.setVisibility(View.GONE);
                        break;
                    case END:
                        foothoder.line1.setVisibility(View.GONE);
                        foothoder.line2.setVisibility(View.VISIBLE);
                        break;
                }
            }
    
        }
    
        @Override
        public int getItemViewType(int position) {
            //最后一个加载footer,
            if (position + 1 == getItemCount()) {
                return FOOT;
            } else {
                return ITEM;
            }
        }
    
        @Override
        public int getItemCount() {
            return list.size()+1;//因为要加上一个footer
        }
    
        
    
        private class ItemViewHodler extends RecyclerView.ViewHolder{
    
            TextView textView;
            public ItemViewHodler(View itemView) {
                super(itemView);
                textView= (TextView) itemView.findViewById(R.id.text);
            }
        }
    
        private class FootViewHodler extends RecyclerView.ViewHolder{
    
            LinearLayout line1;
            LinearLayout line2;
            public FootViewHodler(View itemView) {
                super(itemView);
                line1= (LinearLayout) itemView.findViewById(R.id.line1);
                line2= (LinearLayout) itemView.findViewById(R.id.line2);
            }
        }
    }
    

    public abstract class FootScrollListener extends RecyclerView.OnScrollListener {
        //通过重写RecyclerView的滑动监听来判断是否滑动到底部
        private boolean isScrollUp=false;
    
        @Override
        public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
            /*newState表示当前滑动的状态
            SCROLL_STATE_IDLE:不滑动
            SCROLL_STATE_DRAGGING;滑动(手指在屏幕上)
            SCROLL_STATE_SETTLING;滑动(手指移开屏幕)
            */
            LinearLayoutManager manager = (LinearLayoutManager) recyclerView.getLayoutManager();
            // 当不滑动时
            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                //返回最后一个完成可见视图位置
                int lastItemPosition = manager.findLastCompletelyVisibleItemPosition();
                int itemCount = manager.getItemCount();
                // 判断是否滑动到了最后一个item,并且是向上滑动
                if (lastItemPosition == (itemCount - 1) && isScrollUp) {
                    // 加载更多
                    myLoad();
                }
            }
        }
    
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            /*
            这个方法实时监测页面(不是手指)滑动距离
            dx表示横向滑动距离
            dy表示纵向
            大于0向上,小于0向下,等于0不滑动
            */
            isScrollUp= dy>=0;
    
        }
        //这里可以用接口回调,然后就不用将此类设置成抽象类,
        //设置一个回调,
        public abstract void myLoad();
    }
    

    最后主活动类
    public class MainActivity extends AppCompatActivity {
    
        private RecyclerView recyclerView;
        private SwipeRefreshLayout refreshLayout;
        private MyAdapter myAdapter;
        private ArrayList<String> list=new ArrayList<String>();
        private static boolean isloading=false;//判断是否在加载
    
        private Handler handler=new Handler(){
            @Override
            public void handleMessage(Message msg) {
                if(msg.what==1){
                    refreshLayout.setEnabled(true);
                    if (refreshLayout != null && refreshLayout.isRefreshing()) {
                                refreshLayout.setRefreshing(false);
                    }
                    myAdapter.setLoadstate(myAdapter.FINISH);
                    isloading=false;
                    smooth();
                }
            }
        };
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            initvew();
            somelisten();
        }
    
        public void initvew(){
            refreshLayout= (SwipeRefreshLayout) findViewById(R.id.refresh);
            //关于SwipeRefreshLayout的其他属性和方法,自行查阅文档
    
            recyclerView= (RecyclerView) findViewById(R.id.recycle);
            for(int i=0;i<25;i++) list.add("item");
            myAdapter=new MyAdapter(list);
    
            recyclerView.setLayoutManager(new LinearLayoutManager(this));
            //recyclerView.setLayoutManager(new GridLayoutManager(this,3));
            recyclerView.setAdapter(myAdapter);
        }
    
        public void somelisten(){
            refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
                @Override
                public void onRefresh() {
                    //避免下拉刷新与上拉刷新冲突(虽然可能性很小),进行屏蔽,保证只有一个刷新
                    if(!isloading){
                        isloading=true;
                        list.add(0,"newitem");
                        httppost();
                    }
                }
            });
    
            recyclerView.addOnScrollListener(new FootScrollListener() {
                @Override
                public void myLoad() {
                    /*
                    SwipeRefreshLayout刷新加载的时候屏蔽了下拉刷新事件,避免同时刷新多次
                    但是我们自定义的上拉刷新不能自己屏蔽,我们这里用静态变量来实现屏蔽功能
                    */
                    if(!isloading){
                        Log.e("刷新","进入");
                        myAdapter.setLoadstate(myAdapter.LOADING);
                        isloading=true;
                        refreshLayout.setEnabled(false);//上拉刷新的时候屏蔽掉下拉刷新
                        if (list.size() < 28) {
                            //网络请求
                            //list.add("lastitem");
                            httppost();
    
    
                        } else {
                            // 显示加载到底的提示
                            myAdapter.setLoadstate(myAdapter.END);
                        }
                    }
                }
            });
        }
    
        //recyclerview滑动到适当位置
        public void smooth(){
            /*getChildAt返回组中指定位置的视图,组是指屏幕显示的item组
            *getTop返回此视图相对于其父项顶部的位置
            *smoothScrollBy沿任意轴给定的像素滑动
            *关于recyclerview滑动最常用的是smoothScrollToPosition,不做讲解
            * */
            int top=recyclerView.getChildAt(0).getTop();
            Log.e("top", String.valueOf(top));
            recyclerView.smoothScrollBy(0,top);
            Log.e("count", String.valueOf(recyclerView.getChildCount()));
        }
    
        public void httppost(){
            final OkHttpClient client=new OkHttpClient();
            RequestBody formBody = new FormEncodingBuilder()
                    .add("passwd","123")
                    .add("number","123")
                    .build();
            final Request request=new Request.Builder()
                    .url("http://10.126.149.157:8080/Test/HelloServlet")
                    .post(formBody)
                    .build();
            new Thread(){
                @Override
                public void run() {
                    Response response = null;
                    try {
                        response = client.newCall(request).execute();
                        if (response.isSuccessful()) {
                            Log.e("结果", response.body().string());
                            handler.sendEmptyMessage(1);
                        }
                        else {
                            Log.e("结果", "请求出错");
                            handler.sendEmptyMessage(1);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                        Log.e("结果", "抛出异常");
                        handler.sendEmptyMessageDelayed(1,500);
                    }
                }
            }.start();
        }
    }
    

    我这里是直接拿了以前的OKhttp的一个代码段,当然服务器没开,这里就有一点需要注意的了,有时候网络请求需要很长时间才结束,在实际开发中肯定是不行,然后你可以再加一个计时器在里面,当请求时间过长时,直接停止刷新提示网络超时。


    还有一个大问题,那就是当recyclerview是表格布局的时候,加的那个footer也是表格的一格,这样显得非常难看。所以要重写Adapter里这个函数onAttachedToRecyclerView 

    根据文档解释,在recyclerview observe这个适配器的时候调用这个方法。通过这个方法判断网格布局的格数然后给footer设置占几个item。但是要注意,在你的主活动中必须将setadapter放在setlayout后面,因为这个方法里面做了网格布局的判断,设置了网格布局才有效。代码如下

     //setadapter必须放在setlayout的后面,不然不会调用到这个方法
        @Override
        public void onAttachedToRecyclerView(RecyclerView recyclerView) {
            super.onAttachedToRecyclerView(recyclerView);
            RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
            if (manager instanceof GridLayoutManager) {
                final GridLayoutManager gridManager = ((GridLayoutManager) manager);
                gridManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                    @Override
                    public int getSpanSize(int position) {
    
                        // 如果当前是footer的位置,那么该item占据2个单元格,正常情况下占据1个单元格
                        return getItemViewType(position) == FOOT ? gridManager.getSpanCount() : 1;
                    }
                });
            }
        }

    最后说几点应当注意的地方。上拉或者下拉正在刷新的时候应该不然它触发新的刷新,虽然这种情况很少,但是也要避免。网络请求的时候,时间是不可控的,有时候时间过长影响体验,所以应该在设一个计时器,刷新达到一定时间据强制关闭刷新并提醒(我的代码中没写这一段的代码)。当上拉的时候底部的footer应该要有不让它显示出来,这样有拖动效果,而这一部分区域正好用来被上拉刷新替换。然后刷新完后,footer还在,所以要上滑一段距离。最后再给一张改进过的效果图


    代码已上传:http://download.csdn.net/download/lhp15575865420/10112127

    展开全文
  • 下拉刷新实现

    2019-10-22 23:13:48
    下拉刷新在Android应用开发中是一种很常见的交互方式,在实际开发中都会引用第三方的下拉刷新库来实现,第三方库通常都经过多个应用程序集成测试,有着相对较高的稳定性和可靠性,里面的代码逻辑也相对比较庞杂,对...

    下拉刷新在Android应用开发中是一种很常见的交互方式,在实际开发中都会引用第三方的下拉刷新库来实现,第三方库通常都经过多个应用程序集成测试,有着相对较高的稳定性和可靠性,里面的代码逻辑也相对比较庞杂,对新手相对不太友好,学习起来比较费时费力,本节就通过前面学习的Android视图基本原理来实现自定义的下拉刷新库。

    补白和边距

    补白(Padding)指的是视图内部的内容与视图边界之间的距离,通常上下左右四个方向都可以指定补白宽度,补白就相当于视图内容的镶边,它们处于视图范围内。边距(Margin)指的是当前视图与其他视图之间的距离,其他的视图可以是它的父视图也可以是兄弟视图,边距的位置通常都属于视图的父视图,它主要负责将不同的视图分隔开防止它们相互叠加。开发中通常Padding和Margin都设置的是正数,假如把Padding和Margin的值设置为负数又会有什么样的效果呢,这里只测试常见的LinearLayout布局,在它们的内部添加子视图并且设置负数的Padding和Margin值。
    在这里插入图片描述
    在这里插入图片描述

    上图展示了在LinearLayout中设置了负数值的子视图展示情况,可以看到负数的Padding不仅会影响子视图内容的展示还会影响父布局的尺寸大小。我们知道onMeasure()方法负责测量当前视图的宽高值,onLayout()负责将布局中的视图设置到指定的位置,查看LinearLayout竖向布局的尺寸测量代码。

    在measureVertical()测量竖向布局高度时会首先计算内部可见的子视图高度总值,子布局的高度还要加上下补白的数值得到heightSize数值,heightSize还有与最小高度作比较,其实大部分情况都能确保最终setMeasureDimension()方法中使用的高度值就是heightSize的值。考虑前面的mPaddingTop设置成负值的情况,负值会减少heightSize最终的计算结果值也就导致LinearLayout的高度减小。接着查阅LinearLayout竖向布局方法的实现,看它如何排放内部的子视图位置。

    // LinearLayout测量布局源代码
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    	if (mOrientation == VERTICAL) {
    		measureVertical(widthMeasureSpec, heightMeasureSpec);
    	} else {
    		measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    	}
    }
    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    	// 计算子控件的总高度mTotalLength
    	// 总高度会加上自己的上下补白,mPaddingTop为负值会减小布局高度
    	mTotalLength += mPaddingTop + mPaddingBottom;
    	int heightSize = mTotalLength;
    	heightSize = Math.max(heightSize, getSuggestedMinimumHeight()); 
    	int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
    	setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
    		heightSizeAndState); // 设置LinearLayout的高度为heightSize
    }
    
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
    	if (mOrientation == VERTICAL) {
    		layoutVertical(l, t, r, b);
    	} else {
    		layoutHorizontal(l, t, r, b);
    	}
    }
    
    void layoutVertical(int left, int top, int right, int bottom) {
        final int paddingLeft = mPaddingLeft;
        int childTop = mPaddingTop; // mPaddingTop为负值也会影响子视图展示位置
        int childLeft;
    	for (View view : getChildren()) {
    		childTop += lp.topMargin; // 此处如果topMargin是负值,会影响子视图展示位置
    	   childLeft = paddingLeft + lp.leftMargin;
    		// child.layout(left, top, left + width, top + height);
    		setChildFrame(child, childLeft, childTop + getLocationOffset(child),
    					childWidth, childHeight);
    		childTop += view.getMeasureHeight();
    	}
    }
    

    在layoutVertical()方法中会根据LinearLayout的paddingLeft和子视图的LayoutParams.leftMargin计算当前子视图左边界距离,mPaddingTop和子视图的LayoutParams.topMargin计算子视图的上边界在布局中的位置。考虑前面的marginTop边距设置为负值的情况,由于负值会使得childTop值变小,也就是说子视图距离LinearLayout顶部边界变短,子视图的位置也就更加靠上。

    如果测试横向的LinearLayout会发现即使设置了负值mPaddingTop,它的高度也不会发生变化,查阅layoutHorizontal()方法会发现在测量高度的时候并不会把mPaddingTop值计算在内,自然也就不会发生最终的布局高度改变的效果。测试其他四大布局会发现有些负值mPaddingTop能够改变布局高度,有些设置负值根本不会对布局高度产生任何效果,总结来说在setMeasureDimension() 方法中设置的高度值如果计算了mPaddingTop和mPaddingBottom那么负值补白就可以改变布局高度。

    刷新视图原理

    在下拉刷新中控件的顶部会慢慢地出现下拉视图,下拉视图展示过程中就代表正在执行网络请求操作,等到网络请求成功返回下拉视图会慢慢消失,展示已经刷修改完数据的新界面。下拉视图的展示和消失都是有一个渐进的过程,不是setVisible()那种即刻消失或展示的样式,想要实现这种渐进展示和消失的动画效果就可以利用负值补白来改变下拉视图的高度值,当mPaddingTop为0的时候刷新视图正常展示;当mPaddingTop从0到负下拉视图高度变化时下拉视图组件高度逐渐变成0,也就是逐渐消失;当mPaddingTop从负下拉视图高度到0变化时下拉视图高度逐渐变大,也就是逐渐展示。

    // PullRefreshView基础实现代码
    public class PullRefreshView extends FrameLayout {
        private static final String TAG = "PullRefreshView";
    
        private View mHeaderView;
        private View mContentView;
        private static final int MAX_PULL_LENGTH = Utils.dp2px(200);
        private static final long MAX_GOBACK_DURATION = 200;
    
        private static final int REFRESH_IDLE = 0; // 静止状态
        private static final int REFRESH_PULL = 1; // 手动下拉
        private static final int REFRESH_RELEASED = 2; // 下拉松手
        private static final int REFRESH_REFRESHING = 3; // 正在刷新
        private int mState = REFRESH_IDLE;
        private int mHeaderHeight;
    	private int mTouchSlop;
    
    	public interface RefreshListener {
            void onRefresh();
        }
    
        private RefreshListener mRefreshListener;
        private void notifyRefreshStart() {
            if (mRefreshListener != null) { // 通知开始刷新操作
                mRefreshListener.onRefresh();
            }
        }
        public void notifyRefreshComplete() {
            if (isRefreshing()) { // 刷新结束,头部视图弹回到不可见
                headerGoBack();
            }
        }
    	// 暂时省略其他部分
    }
    

    现在开始自定义的下拉刷新控件的实现,让它继承自FrameLayout布局,内部包含两个主要的成员mHeaderView也就是下拉视图,mContentView也就是包含内容的视图对象,比如后面会提到的ScrollView、ListView和RecyclerView。下拉刷新过程是要消耗比较长的时间,对于不能即刻完成的动作为了避免错误访问可以使用状态机来保存它的内部状态,在某种状态下只能执行一些合法的操作避免出现错误。默认情况下的状态为空闲状态REFRESH_IDLE,当用户向下拉动内容控件时处于REFRESH_PULL下拉状态,如果头部视图完全展示出来等到用户松手此时控件内部处于REFRESH_RELEASED状态,用户松手后开始发起网络请求控件处于刷新状态REFRESH_REFRESHING,刷新完成后控件又进入了空闲状态,下拉刷新的状态迁移如下图。控件的有些操作只有在特定状态下才可以执行,比如onRefreshComplete()完成刷新操作必须要求之前状态是REFRESH_REFRESHING正在刷新,如果不是就说明内部状态有问题,需要开发者及时修改内部状态维护出现的异常情况。
    在这里插入图片描述
    如果用户下拉时头部视图完全可见再释放下拉刷新后需要触发网络请求,定义RefreshListener接口内部包含onRefresh()方法,需要监控下拉刷新事件的开发者可以注册刷新监听器。当网络请求完成后可以调用notifyRefreshComplete()方法通知下拉刷新控件收起下拉视图修改内部状态值。如果用户下拉时头部仅仅漏出一部分内容如下图,在用户释放刷新时仅仅将头部视图回弹到不可见,并不会触发网络请求操作。
    在这里插入图片描述
    在headerGoBack()方法中会使用ValueAnimator逐渐修改mHeaderView的mPaddingTop值使得下拉视图高度组件变小直到消失不见。在动画结束的时候同时把下拉刷新控件内部的状态更新成空闲状态,完成一次下拉刷新状态迁移。这里并没有提到下拉刷新视图是如何展示出来的,不同的内容控件有不同方式触发展示逻辑,后面刷新具体的内容控件时再详述下拉视图的展示动画实现。

    // HeaderView拉伸回弹实现代码
    private void setHeaderPaddingTop(int top) {
    // HeaderView会随着paddingTop变化逐渐消失或逐渐展示
    mHeaderView.setPadding(0, top, 0, 0);
    mHeaderView.requestLayout();
    }
    
    private void headerGoBack() {
        if (!isReleased() && !isRefreshing()) {
            return;
        }
        ValueAnimator valueAnimator = 
    ValueAnimator.ofInt(getHeaderPaddingTop(), -mHeaderHeight);
        valueAnimator.setDuration(MAX_GOBACK_DURATION);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int value = (int) animation.getAnimatedValue();
                setHeaderPaddingTop(value); // 不断地更改头部视图paddingTop
            }
         });
        valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mState = REFRESH_IDLE; // 头部视图完全不可见时进入REFRESH_IDEL状态
            }
        });
        valueAnimator.start();
    }
    

    ScrollView下拉刷新

    PullRefreshView接收到的触摸事件一概传递给它的内容控件来处理,不过原生的ScrollView控件内部的触摸事件处理已经固定下来,需要使用ScrollView的子类覆盖dispatchTouchEvent()来修改它默认的处理方式。为此需要在加载PullRefreshView内部的InternalScrollView控件的时候替换系统提供的原生ScrollView。

    // PullRefreshView替换内部的用户ScrollView
    private void initViews() {
        mContentView = getChildAt(0);
        FrameLayout.LayoutParams contentParams = (LayoutParams) 
    mContentView.getLayoutParams();
        contentParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
        contentParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
        removeView(mContentView);
          
    // 将原生ScrollView替换成支持下拉刷新的InternalScrollView
    if (mContentView instanceof ScrollView) { 
             mContentView = new InternalScrollView(getContext(),
     (ScrollView) mContentView);
         }
    	 mContentView.setLayoutParams(contentParams);
         addView(mContentView);
    }
    	
    // 将PullRefreshView接收到的所有触摸事件都传递给内容控件
    public boolean dispatchTouchEvent(MotionEvent event) {
        return mContentView.dispatchTouchEvent(event);
    }
    

    替换后的InternalScrollView作为PullRefreshView内部的mContentView成员对象,系统派发过来的所有MotionEvent事件都由InternalScrollView负责处理。由于原生ScrollView内部只有用户内容布局存在, InternalScrollView需要先将原生的ScrollView内部用户内容对象添加到竖向LinearLayout底部,LinearLayout的上面部分则负责展示下拉视图。在dispatchTouchEvent()方法中首先判断用户是否在做滑动操作,如果是滑动操作是否满足下拉刷新的条件,满足条件就要执行下拉刷新视图展示动画,否则需要调用super.dispatchTouchEvent()实现默认的ScrollView触摸事件处理。

    // InternalScrollView实现代码
    private class InternalScrollView extends ScrollView {
        private int mDownY;
        private int mLastY;
        private boolean mIsDragging = false; // 是否在做下拉刷新动作
    
        public InternalScrollView(Context context, ScrollView origin) {
            super(context);
            setId(origin.getId()); // 将原生ScrollView的id设置给InternalScrollView
            LinearLayout linearLayout = new LinearLayout(getContext());
            linearLayout.setOrientation(LinearLayout.VERTICAL);
            // 获取原生ScrollView中的用户内容布局
            View content = origin.getChildAt(0);
            origin.removeAllViews();
            // 竖向LinearLayout内部包含下拉视图和用户内容布局
            linearLayout.addView(mHeaderView);
            linearLayout.addView(content);
            // InternalScrollView内部的布局包含下拉刷新视图和用户内容视图
            addView(linearLayout);
        }
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            int action = event.getActionMasked();
            int y = (int) event.getRawY();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    mDownY = y;
                    break;
                case MotionEvent.ACTION_MOVE:
                    int motionY = y - mDownY; // 代表用户滑动的方向
                    int diff = y - mLastY; // 用户本次滑动与上次滑动的偏差
                    // 如果当前没有滑动操作而且用户移动距离超出最小滑动
    // 距离mTouchSlop,如果用户向下滑动且内容控件的第一
    // 条数据处在内容顶部,此时需要准备开始下拉操作;如果
    // 用户向上滑动而且头部视图部分可见,准备向上滑动头部视图
                    if (!mIsDragging && Math.abs(motionY) > mTouchSlop &&
     ((motionY > 0 && isFirstAtTop()) ||
                                      isFirstAtTop() && motionY < 0 &&
     getHeaderPaddingTop() > -mHeaderHeight)) {
                         mIsDragging = true;
                    }
    
                    if (mIsDragging) { // 用户正在做滑动操作
                        mState = REFRESH_PULL;
                        offsetHeader(diff);  // 渐进增大或减小下拉视图
                        if (getHeaderPaddingTop() <= -mHeaderHeight) {
                            // 如果用户手动将下拉视图推到了
    // 不可见位置,不再修改下拉视图的大小
                            mState = REFRESH_IDLE;
                            mIsDragging = false;
                            setHeaderPaddingTop(-mHeaderHeight);
                        }
                    }
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    mIsDragging = false;
                    if (isPulling()) { // 如果用户正在下拉过程中松手
                        mState = REFRESH_RELEASED;
    // 如果下拉视图已经全部展示出来需要
    // 先退回展示全部,再触发刷新操作
                        if (shouldRefresh()) { 
                            mState = REFRESH_REFRESHING;
                            goBackAndShowRefresh(); // 参考代码4-38
                        } else {
    // 如果下拉视图没有全部展示,只下拉了一下部分,
    // 直接退回去不触发刷新
                            headerGoBack();
                        }
                    }
                    break;
           }
           mLastY = y;
           // 如果mIsDragging为true代表用户正在做下拉刷新,
    // 否则就执行ScrollView内部滚动
           return mIsDragging || super.dispatchTouchEvent(event);
       }
       // 判定当前用户内容视图的顶部在InternalScrollView的顶部,没有内容被卷起来
    // 用户这时向下拉就是要做下拉刷新
       public boolean isFirstAtTop() {
           return mContentView.getScrollY() <= 0;
        }
    }
    

    上面的代码完整展示了InternalScrollView内部处理下拉刷新的整个过程,最开始的构造函数中先要为原始用户内容控件添加下拉刷新头部视图,最终替换成下图所示,在初始情况下HeaderView是完全不展示的,仅仅展示底部原始用户内容布局。当用户在InternalScrollView上按下,首先记录下最初的按下位置mDownY,并且由super.dispatchTouchEvent(event)处理返回true代表接受后续的触摸事件,如果用户接着移动手指就会发送ACTION_MOVE事件,判定用户正在做滑动操作,除了要求用户从ACTION_DOWN到ACTION_MOVE移动的距离超出最小滑动距离外,还要求用户向上或向下滑动时内容视图没有卷起高度,也就是mScrollY的值为0,而且此时的HeaderView需要完全不可见,此时认定用户正在做下拉刷新操作,之所以存在用户向上滑动是因为下拉过程中用户是可以向上滑动的。
    在这里插入图片描述
    确定用户在做下拉滑动操作后就需要根据用户滑动偏移不断调整HeaderView的paddingTop大小,此时就能见到HeaderView不断变大或者不断减小的效果,当然如果用户一直向上移动HeaderView的paddingTop值就可能越减越小,当paddingTop减小到HeaderView的负值高度时可以忽略用户向上移动。当用户最终释放下拉拖动时在ACTION_UP中判定HeaderView是否已经完全展示,如果是就触发刷新操作,否就直接将部分展示的HeaderView弹回不可见。为了保证用户操作的平滑性用户下拉可以把HeaderView拉到比实际高度高很多的距离,这种情况下就需要先将多拉出来的高度隐藏再开始触发刷新工作。
    在这里插入图片描述
    上图中用户下拉很长距离导致HeaderView整体的高度比原始高度高了很多,此时就需要先把HeaderView被多拉出来的高度隐藏起来,等到超长高度隐藏结束后就可以通知触发刷新操作。

    // HeaderView拉伸过长弹回到实际高度展示
    private void goBackAndShowRefresh() {
        if (!isRefreshing()) {
            return;
        }
    
    int paddingTop = getHeaderPaddingTop();
    // paddingTop为零的时候下拉视图完全展示,超出0时需要先回到0
    if (paddingTop > 0) {             
    ValueAnimator valueAnimator = ValueAnimator.ofInt(paddingTop, 0);
            valueAnimator.setDuration(MAX_GOBACK_DURATION);
            valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    int value = (int) animation.getAnimatedValue();
                    setHeaderPaddingTop(value);
                }
            });
             valueAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                   // 下拉视图paddingTop为零时触发刷新操作
                    notifyRefreshStart();
                 }
            });
            valueAnimator.start();
        } else {
            notifyRefreshStart();// 下拉视图paddingTop为零时触发刷新操作
        }
    }
    

    代码中paddingTop大于零就代表用户将HeaderView下拉的比实际高度要高出paddingTop的长度,需要先将HeaderView缩回到paddingTop为零的正常高度再触发刷新操作。到目前为止ScrollView的下拉刷新就成功触发了网络请求,等到网络请求成功后会通知刷新操作已完成并调用headGoBack()实现下拉视图渐进消失操作,ScrollView的一次下拉刷新交互就完成了。

    ListView下拉刷新

    在初始化PullRefreshView内部控件时如果子控件是ListView类型,需要将它替换成InternalListView自定义控件,InternalListView需要控件会在调用addHeaderView()方法添加HeaderView为头部视图,这样当头部视图的高度发生变化的时候ListView内部的用户内容控件也会随之变化位置。

    // InternalListView实现代码
    private class InternalListView extends ListView {
    	private int mDownY;
    	private int mLastY;
    	private boolean mIsDragging = false;
    	public InternalListView(Context context, ListView origin) {
    		super(context);
    		ListView.LayoutParams layoutParams = 
    new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 
    ViewGroup.LayoutParams.WRAP_CONTENT);
    		FrameLayout frameLayout = new FrameLayout(getContext());
    		frameLayout.addView(mHeaderView);
    		frameLayout.setLayoutParams(layoutParams);
    		addHeaderView(frameLayout); // 将下拉视图添加成ListView的头部视图
    		setId(origin.getId());
    	}
    	// 与InternalScrollView的处理基本一样
    	public void dispatchTouchEvent(MotionEvent e) { .... }
    	public boolean isFirstAtTop() { // 判定ListView头部没有内容被卷起
    		if (getChildCount() < 2) {
    			return false;
    		}
    		View view = getChildAt(1);
            // 第二个View,其实就是第一个用户内容View并且展示的是第一条用户数据
    		return view.getTop() < mTouchSlop && getFirstVisiblePosition() <= 1;
    	}
    }
    

    InternalListView和InternalScrollView在判定顶部内容没有卷起稍有不同,InternalListView要判定它内部的第二个视图处于顶部位置,用户在这种情况下向下滑动才能够被判定是在做下拉刷新操作。在InternalListView替换ListView控件时会在头部添加下拉视图,下拉视图就是它内部的第一个视图。ListView内部会使用回收复用机制防止过多创建视图对象,第二个视图并不代表它展示的是用户数据中的第一条内容,需要加上getFirstVisiblePosition() <= 1确保第二个视图展示的是用户数据列表里的第一条数据。
    在这里插入图片描述

    RecycleView下拉刷新

    RecyclerView是Android Design包中提供的用于替换ListView和GridView等动态视图的控件,通过设置不同的LayoutManager对象就可以实现展示成ListView样式还是GridView样式,这里仅仅讨论ListView样式展示的RecyclerView的下拉刷新实现。RecyclerView自带了ViewHolder机制实现,但不包含添加头部视图和底部视图的功能,想要像ListView那样通过添加头部视图来实现下拉刷新就需要先实现RecyclerView的头部和底部视图添加功能。

    //  InternalRecyclerView实现代码
    private class InternalRecyclerView extends BaseRecyclerView {
    	private boolean mIsDragging = false;
    	private View mHeaderView;
    
    	public InternalRecyclerView(Context context, RecyclerView origin) {
    		super(context);
    		setId(origin.getId());
    		setLayoutManager(new LinearLayoutManager(context));
    		mHeaderView = LayoutInflater.from(context).inflate(R.layout.layout_header, this, false);
    		addHeaderView(mHeaderView);
    	}
    	// 与InternalScrollView的处理基本一样
    	public void dispatchTouchEvent(MotionEvent e) { .... }
    	public boolean isFirstAtTop() {
    		return !canScrollVertically(-1);
    	}
    }
    

    总体上来说RecyclerView的实现和ListView基本类似,不过RecyclerView的下拉刷新判定还是有点特殊的,RecyclerView可以使用 !canScrollVertically(-1)判定它是否能够向下拉动,如果无法向下拉动表示用户目前正在做下拉刷新操作。
    在实现了下拉操作的判定后只剩下如何实现在RecyclerView中添加头部视图的实现,参考ListView的源代码中实现添加头部和底部视图的实现,源码中会创建HeaderWrapperAdapter对象,它会包含用户添加的HeaderView,FooterView和用户设置的Adapter对象。

    // HeaderWrapperAdapter实现代码
    public class HeaderWrapperAdapter extends RecyclerView.Adapter<BaseRecyclerViewHolder> {
        private List<View> mHeaders; // HeaderView列表
        private List<View> mFooters; // FooterView列表
        private BaseRecyclerAdapter<BaseRecyclerViewHolder> mAdapter; // 用户Adapter对象
        private static final int HEADER_VIEW_TYPE = 0x8888; // HeaderView类型
        private static final int FOOTER_VIEW_TYPE = 0x9999; // FooterView类型
    
    	HeaderWrapperAdapter(List<View> headers, List<View> footers, 
    			BaseRecyclerAdapter adapter) {
            this.mHeaders = headers;
            this.mFooters = footers;
            this.mAdapter = adapter;
        }
    
        @Override
    	public BaseRecyclerViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
    		 int viewType) {
            int realType = getType(viewType),  position = getPosition(viewType);
            if (realType == HEADER_VIEW_TYPE) {
                return new HeaderViewHolder(mHeaders.get(position));
            } else if (realType == FOOTER_VIEW_TYPE) {
                return new HeaderViewHolder(mFooters.get(position – 
    mAdapter.getItemCount() - mHeaders.size()));
            } else {
                return mAdapter.onCreateViewHolder(viewGroup, realType);
            }
        }
    
        @Override // 绑定ViewHolder是只需要执行用户内容Adapter的绑定操作
    	public void onBindViewHolder(@NonNull BaseRecyclerViewHolder viewHolder, 
    		int position) {
            if (position >= mHeaders.size() && 
    position < mHeaders.size() + mAdapter.getItemCount()) {
                mAdapter.onBindViewHolder(viewHolder, position - mHeaders.size());
            }
        }
    
        @Override // 绑定ViewHolder是只需要执行用户内容Adapter的绑定操作
    	public void onBindViewHolder(@NonNull BaseRecyclerViewHolder viewHolder,
    		 int position, @NonNull List<Object> payloads) {
            if (position >= mHeaders.size() && 
    position < mHeaders.size() + mAdapter.getItemCount()) {
                mAdapter.onBindViewHolder(viewHolder, position - mHeaders.size());
            }
        }
    
        @Override
    	public int getItemCount() { 
            // RecyclerView内的元素个数,头视图、尾视图和用户视图总个数
    		return mHeaders.size() + mAdapter.getItemCount() + mFooters.size();
        }
    
        @Override
        public int getItemViewType(int position) { // 根据position决定视图类型
            if (position < mHeaders.size()) {
                return makeTypePos(HEADER_VIEW_TYPE, position);
            } else if (position < mHeaders.size() + mAdapter.getItemCount()) {
                return makeTypePos(mAdapter.getItemViewType(
    position - mHeaders.size()), position);
            } else {
                return makeTypePos(FOOTER_VIEW_TYPE, position);
            }
        }
    
        private int makeTypePos(int type, int pos) { // viewType和position绑定到int中
            return (type << 16) + pos;
        }
    
        private int getType(int typePos) { // 从int高16为获取viewType
            return typePos >>> 16;
        }
    
        private int getPosition(int typePos) { // 从int低16位获取position
            return typePos & 0xffff;
        }
    
        private static final class HeaderViewHolder extends BaseRecyclerViewHolder {
            HeaderViewHolder(@NonNull View itemView) {
                super(itemView);
            }
        }
    }
    

    代码中通过继承RecyclerView.Adapter创建了HeaderWrapperAdapter类型,该类型内部增加了HeaderView列表与FooterView列表,同时包含用户自己的RecyclerView.Adapter对象 ,在计算适配器内部数量长度包含了头部列表、尾部列表和用户适配器数据长度。当RecyclerView展示内部控件时先调用getItemViewType()根据position判定是头部视图、尾部视图还是用户内容视图,然后在将视图类型viewType和position绑定到int中传递到onCreateViewHolder()创建不同类型的ViewHolder对象。onCreateViewHolder()方法中只允许传递进来viewType类型数据,不过这里使用移位运算符实现在int数字中保存viewType和当前position两个数据,本节就使用如下的处理方式使用int的前两个字节保存viewType,后两个字节保存position。
    在这里插入图片描述
    接着创建自定义的BaseRecyclerView继承自RecyclerView,它内部使用HeaderWrapperAdapter管理内容视图,当用户调用setAdapter()内容适配器的时候就把它封装到HeaderWrapperAdapter内部,同时要添加addHeaderView()/removeHeaderView()等接口实现头部视图的添加移除工作。

    // 支持Header和Fooer的BaseRecyclerView实现代码
    public class BaseRecyclerView extends RecyclerView {
        private List<View> mHeaders = new ArrayList<>();
        private List<View> mFooters = new ArrayList<>();
        private HeaderWrapperAdapter mWrapperAdapter;
        private Adapter mAdapter;
    
        @Override
        public void setAdapter(@Nullable Adapter adapter) {
            if (adapter instanceof BaseRecyclerAdapter) {
                mAdapter = adapter;
                mWrapperAdapter = new HeaderWrapperAdapter(mHeaders, 
    mFooters, (BaseRecyclerAdapter) adapter);
                super.setAdapter(mWrapperAdapter);
            } else {
                super.setAdapter(adapter);
            }
        }
    
        public void addHeaderView(View headerView) {
            if (mWrapperAdapter != null) {
                mWrapperAdapter.notifyItemInserted(mHeaders.size() - 1);
            }
        }
    
        public void removeHeaderView(View headerView) {
            int index = mHeaders.indexOf(headerView);
            if (index < 0) {
                return;
            }
    
            mHeaders.remove(headerView);
            if (mWrapperAdapter != null) {
                mWrapperAdapter.notifyItemRemoved(index);
            }
        }
    // 省略底部视图添加、删除代码
    }
    

    代码中BaseRecyclerView继承自RecyclerView同时覆盖了setAdapter()方法,在设置适配器时会自动将BaseRecyclerAdapter封装到HeaderWrapperAdapter中,当用户调用addHeaderView()方法时实际上会把HeaderView添加到HeaderWrapperAdapter中,此时只要调用notifyItemInserted()就能够将添加的HeaderView展示出来。BaseRecyclerView通过addHeaderView() 添加下拉刷新的HeaderView后,在下拉刷新中使用canScrollVertical()判定顶部没有卷起内容,其他的用户事件处理与ScrollView基本相同,这样RecyclerView就实现了下拉刷新功能。

    下拉刷新组件Demo

    展开全文
  • 下拉刷新.zip

    2020-07-03 23:30:47
    可以实现web前端的上下拉刷新数据,比分页强多了,还有很多类,可以参考学习
  • 最近项目中需要用到ListView下拉刷新的功能,一开始想图省事,在网上直接找一个现成的,可是尝试了网上多个版本的下拉刷新之后发现效果都不怎么理想。有些是因为功能不完整或有Bug,有些是因为使用起来太复杂,...
  • html5+ 下拉刷新

    千次阅读 2018-10-10 15:06:01
    &amp;lt;!DOCTYPE HTML&amp;gt; &amp;lt;html&amp;gt; &amp;lt;head&amp;gt; &amp;lt;meta charset=&quot;utf-8&quot; /&amp;...initial-scale=1
  • Android几种强大的下拉刷新

    万次阅读 多人点赞 2017-03-31 10:57:10
    众多优秀的下拉刷新(除了我写的之外T_T) 说起下拉刷新,好像经历一段历史的洗礼。。。 (1)在我刚学android的时候,用的是XListView,在github上搜索有 MarkMjw/PullToRefresh ,根据Maxwin的XListView改造而来...
  • 有趣的下拉刷新

    千次阅读 2013-12-22 22:58:51
    5月22日,Twitter正式宣布获得了下拉刷新的技术专利,这项专利是在一年前提出的,虽然公司一再说明此项专利只用于保护自己,不会用来发起诉讼,但无疑这还是对行业来说是一个威胁。 下拉刷新这个操作最早由...
  • Android 下拉刷新框架实现

    万次阅读 多人点赞 2014-07-03 10:52:15
    一个通用的下拉刷新的框架介绍。 前段时间项目中用到了下拉刷新功能,之前在网上也找到过类似的demo,但这些demo的质量参差不齐,用户体验也不好,接口设计也不行。最张没办法,终于忍不了了,自己就写了一个下拉...
  • 微信小程序自定义下拉刷新

    万次阅读 热门讨论 2018-07-05 16:47:25
    哈喽,大家好, 微信小程序出来也有好几年了,大部分项目下拉刷新都用的系统自带的,用起来非常方便,有个缺点就是它的样式被微信锁定了,如果想在下拉不同状态显示不同文字就不怎么方便了,最近公司需求,写了一个...
  • Android下拉刷新上拉加载控件,对所有View通用!

    万次阅读 多人点赞 2014-12-12 11:35:32
    前面写过一篇关于下拉刷新控件的博客下拉刷新控件终结者:PullToRefreshLayout,后来看到好多人还有上拉加载更多的需求,于是就在前面下拉刷新控件的基础上进行了改进,加了上拉加载的功能。不仅如此,我已经把它...
  • ListView上拉加载和下拉刷新多种实现方式该篇为ListView下拉刷新和上拉加载实现的各种方法大合集。可能在具体的细节逻辑上处理不太到位,但基本上完成逻辑的实现。细节方面,个人可以根据自己的需求进行完善。该博客...
  • Android中ListView下拉刷新的实现

    万次阅读 多人点赞 2014-11-11 15:17:57
    ListView中的下拉刷新是非常常见的,也是经常使用的,看到有很多同学想要,那我就整理一下,供大家参考。那我就不解释,直接上代码了。 这里需要自己重写一下ListView,重写代码如下: package net.loonggg.listview; ...
  • 移动端html5 自动生成下拉刷新控件

    万次阅读 2015-04-16 16:37:31
    移动APP中很多页面可以下拉实现刷新,在此实现html5版本的下拉刷新控件。...2.写JS,在上面的容器上动态创建控件(下拉刷新控件,即手机APP中常见到的,下拉后有个加载圈在转,并提示下拉刷新。。) 3.给容器添加touch
  • 下拉刷新功能 下拉刷新功能是指在当前页面重新刷新页面,调取数据的过程。 uniapp 官网中,关于下拉刷新是有API的。此API有3个函数。分别如下介绍: 页面级的下拉刷新事件 onPullDownRefresh——监听用户下拉刷新...
  • 之前在做原生app开发的时候,下拉刷新和上拉加载更多是使用的比较多的一个功能了。现在在做微信小程序开发,小程序只提供了下拉刷新的接口。那么下拉刷新和上拉加载更多就需要我们换个思路实现了。关于scroll-view...
  • listView下拉刷新加载数据

    万次阅读 多人点赞 2014-04-03 20:23:41
    在2月27号的时候,完成listView滑动刷新代码,昨天的时候,完成listView下拉刷新(sina微博Android客户端效果)效果的解析,其实在三个例子当中,感觉最好的应该是listView下拉刷新(sina微博Android客户端效果)里面的...
  • 给RecyclerView最纯粹的下拉刷新和上拉加载更多

    万次阅读 热门讨论 2017-03-08 17:35:59
    RecyclerView 出现以后,Android 里的下拉刷新和加载更多实现起来就非常容易了。当然,现成的库也有很多,只是总会有不一样的需求,而且我们往往只需要最基本的下拉刷新和加载更多功能,而不需要其他多余的功能。我...
  • H5在微信公众号的下拉刷新实现

    万次阅读 2015-12-25 11:23:00
    由于项目需要在微信公众号做“健康资讯”相关的,所以会用到下拉刷新,可是使用的原生H5做页面,不过希望能够做到和app原生一样的下拉刷新效果,然后在网上找了找H5下拉刷新相关的资料,具体有:Iscroll,MUI,Hook....
1 2 3 4 5 ... 20
收藏数 71,545
精华内容 28,618
关键字:

下拉刷新