zoukankan      html  css  js  c++  java
  • Android之自定义控件-下拉刷新

    实现效果:

    图片素材:        

    --> 首先, 写先下拉刷新时的刷新布局 pull_to_refresh.xml:

     1 <resources>
     2     <string name="app_name">PullToRefreshTest</string>
     3     <string name="pull_to_refresh">下拉可以刷新</string>
     4     <string name="release_to_refresh">释放立即刷新</string>
     5     <string name="refreshing">正在刷新...</string>
     6     <string name="not_updated_yet">暂未更新过</string>
     7     <string name="updated_at">上次更新于%1$s前</string>
     8     <string name="updated_just_now">刚刚更新</string>
     9     <string name="time_error">时间有问题</string>
    10 </resources>
    strings
     1 <?xml version="1.0" encoding="utf-8"?>
     2 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     3     android:id="@+id/pull_to_refresh_head"
     4     android:layout_width="match_parent"
     5     android:layout_height="60dp">
     6 
     7     <LinearLayout
     8         android:layout_width="200dp"
     9         android:layout_height="60dp"
    10         android:layout_centerInParent="true"
    11         android:orientation="horizontal">
    12 
    13         <RelativeLayout
    14             android:layout_width="0dp"
    15             android:layout_height="60dp"
    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="@mipmap/indicator_arrow" />
    24 
    25             <ProgressBar
    26                 android:id="@+id/progress_bar"
    27                 android:layout_width="30dp"
    28                 android:layout_height="30dp"
    29                 android:layout_centerInParent="true"
    30                 android:visibility="gone" />
    31         </RelativeLayout>
    32 
    33         <LinearLayout
    34             android:layout_width="0dp"
    35             android:layout_height="60dp"
    36             android:layout_weight="12"
    37             android:orientation="vertical">
    38 
    39             <TextView
    40                 android:id="@+id/description"
    41                 android:layout_width="match_parent"
    42                 android:layout_height="0dp"
    43                 android:layout_weight="1"
    44                 android:gravity="center_horizontal|bottom"
    45                 android:text="@string/pull_to_refresh" />
    46 
    47             <TextView
    48                 android:id="@+id/updated_at"
    49                 android:layout_width="match_parent"
    50                 android:layout_height="0dp"
    51                 android:layout_weight="1"
    52                 android:gravity="center_horizontal|top"
    53                 android:text="@string/updated_at" />
    54         </LinearLayout>
    55     </LinearLayout>
    56 
    57 </RelativeLayout>
    pull_to_refresh

    --> 然后, 也是主要的, 自定义下拉刷新的 View (包含下拉刷新所有操作) RefreshView.java:

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

    --> 第三步, 写主布局:

     1 <?xml version="1.0" encoding="utf-8"?>
     2 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     3     xmlns:tools="http://schemas.android.com/tools"
     4     android:layout_width="match_parent"
     5     android:layout_height="match_parent"
     6     tools:context=".MainActivity" >
     7 
     8     <com.dragon.android.tofreshlayout.RefreshView
     9         android:id="@+id/refreshable_view"
    10         android:layout_width="match_parent"
    11         android:layout_height="match_parent" >
    12 
    13         <ListView
    14             android:id="@+id/list_view"
    15             android:layout_width="match_parent"
    16             android:layout_height="match_parent" >
    17         </ListView>
    18 
    19     </com.dragon.android.tofreshlayout.RefreshView>
    20 
    21 </RelativeLayout>
    activity_main

    --> 最后, Java 代码添加 ListView 的数据:

    package com.dragon.android.tofreshlayout;
    
    import android.os.Bundle;
    import android.os.SystemClock;
    import android.support.v7.app.AppCompatActivity;
    import android.webkit.WebView;
    import android.widget.ArrayAdapter;
    import android.widget.ListView;
    
    public class MainActivity extends AppCompatActivity {
    
        RefreshView refreshableView;
        ListView listView;
        ArrayAdapter<String> adapter;
        private WebView webView;
    
        private static int NUM = 30;
        String[] items = new String[NUM];
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            getSupportActionBar().hide();
    
            for (int i = 0; i < items.length; i++) {
                items[i] = "列表项" + i;
            }
    
            refreshableView = (RefreshView) findViewById(R.id.refreshable_view);
    
            listView = (ListView) findViewById(R.id.list_view);
            adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, items);
            listView.setAdapter(adapter);
    
            refreshableView.setOnRefreshListener(new RefreshView.PullToRefreshListener() {
                @Override
                public void onRefresh() {
                    SystemClock.sleep(3000);
                    refreshableView.finishRefreshing();
                }
            }, 0);
        }
    }
    View Code

    程序 Demo: 链接:http://pan.baidu.com/s/1ge6Llw3 密码:skna

    ***************其实还应该再封装的...*****************

  • 相关阅读:
    git使用小结
    关于vtordisp知多少?
    虚函数与虚继承寻踪
    最简git Server配置
    StarUML序
    CacheHelper对缓存的控制
    Web Service的一些经验和技巧总结
    月份信息二维坐标图绘制(绘制箭头算法)续
    dynamic与xml的相互转换
    如何将XML与OBJECT进行相互转换(泛型以及通用方法)
  • 原文地址:https://www.cnblogs.com/xmcx1995/p/5951772.html
Copyright © 2011-2022 走看看