Android之自定义控件-下拉刷新

业无高卑志当坚,男儿有求安得闲?这篇文章主要讲述Android之自定义控件-下拉刷新相关的知识,希望能为你提供帮助。
实现效果:
【Android之自定义控件-下拉刷新】

Android之自定义控件-下拉刷新

文章图片

 
 
图片素材:  
Android之自定义控件-下拉刷新

文章图片
   
Android之自定义控件-下拉刷新

文章图片
   
Android之自定义控件-下拉刷新

文章图片

 
--> 首先, 写先下拉刷新时的刷新布局 pull_to_refresh.xml:
Android之自定义控件-下拉刷新

文章图片
Android之自定义控件-下拉刷新

文章图片
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
Android之自定义控件-下拉刷新

文章图片
Android之自定义控件-下拉刷新

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

 
--> 第三步, 写主布局:
Android之自定义控件-下拉刷新

文章图片
Android之自定义控件-下拉刷新

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

activity_main 
--> 最后, Java 代码添加 ListView 的数据:
Android之自定义控件-下拉刷新

文章图片
Android之自定义控件-下拉刷新

文章图片
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
***************其实还应该再封装的...*****************

    推荐阅读