android 自定义控件之下拉刷新源码详解

下拉刷新 是请求网络数据中经常会用的一种功能.

实现步骤如下:

1.新建项目   PullToRefreshDemo,定义下拉显示的头部布局pull_to_refresh_refresh.xml

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:tools="http://schemas.android.com/tools"

android:layout_width="match_parent"

android:id="@+id/pull_to_refresh_head"

android:layout_height="60dip"

>

<LinearLayout

android:layout_width="200dip"

android:layout_height="60dip"

android:layout_centerInParent="true"

android:orientation="horizontal"

>

<RelativeLayout

android:layout_width="0dip"

android:layout_height="60dip"

android:layout_weight="3"

>

<ImageView

android:id="@+id/iv_arrow"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_centerInParent="true"

android:src="@drawable/arrow"

/>

<ProgressBar

android:id="@+id/pb"

android:layout_width="30dip"

android:layout_height="30dip"

android:layout_centerInParent="true"

android:visibility="gone"

/>

</RelativeLayout>

<LinearLayout

android:layout_width="0dip"

android:layout_height="60dip"

android:layout_weight="12"

android:orientation="vertical"

>

<TextView

android:id="@+id/tv_description"

android:layout_width="fill_parent"

android:layout_height="0dip"

android:layout_weight="1"

android:gravity="center_horizontal|bottom"

android:text="下拉可以刷新"

/>

<TextView

android:id="@+id/tv_update"

android:layout_width="fill_parent"

android:layout_height="0dip"

android:layout_weight="1"

android:gravity="center_horizontal|top"

android:text="上次更新于%1$s前"

/>

</LinearLayout>

</LinearLayout>

</RelativeLayout>

2.新建一个RefreshView继承自LinearLayout.

public class RefreshView extends LinearLayout implements OnTouchListener {

//下拉状态

public static final int STATUS_PULL_TO_REFRESH=0;

//释放立即刷新状态

public static final int STATUS_RELEASE_TO_REFRESH=1;

//正在刷新状态

public static final int STATUS_REFRESHING=2;

//刷新完成或未刷新状态

public static final int STATUS_REFRESH_FINISH=3;

//下拉时头部回滚的速度

public static final int SCROLL_SPEED=-20;

//一分钟的毫秒值,判断上次的更新时间

public static final long ONE_MINUTE=60*1000;

//一小时的毫秒值,用于判断上次的更新时间

public static final long ONE_HOUR=60*ONE_MINUTE;

//一天的毫秒值

public static final long ONE_DAY=24*ONE_HOUR;

//一月的毫秒值

public static final long ONE_MONTH=30*ONE_DAY;

//一年的毫秒值

public static final long ONE_YEAR=12*ONE_MONTH;

//上次更新时间的字符串常量,用来做SharedPreference的键值

public static final String UPDATE_AT="update_at";

//存储上次更新时间

private SharedPreferences mShared;

//下拉时显示的View

private View header;

//下拉刷新的ListView

private ListView lv;

//刷新时显示的进度条

private ProgressBar mProgressBar;

//指示下拉和释放的箭头

private ImageView arrow;

//指示下拉和释放的文字描述

private TextView tv_des;

//上次更新时间的文字描述

private TextView tv_update;

//下拉头的布局参数

private MarginLayoutParams headerLayoutParams;

//上次更新时间的毫秒数

private long lastUpdateTime;

//为了防止不同界面的下拉刷新与上次更新时间互相有冲突,使用id来做区分

private int mId=-1;

//下拉头的高度

private int hideHeaderHeight;

//标志当前是什么状态

private int currentStatus=STATUS_REFRESH_FINISH;

//记录上次的状态是什么,避免进行重复操作

private int lastStatus=currentStatus;

//手指按下时 的屏幕纵坐标

private float yDown;

//在被判断为滚动之前用户手指可以移动的最大值

private int touchSlop;

//判断已加载过一次layout,这里的onLayout的初始化只需加载一次

private boolean loadOnce;

//当前是否可以下拉,只有ListView滚到头才允许下拉

private boolean ableToPull;

//下拉刷新的回调接口

private PullToRefreshListener mListener;

public RefreshView(Context context, AttributeSet attrs) {

super(context, attrs);

mShared=PreferenceManager.getDefaultSharedPreferences(context);

header=LayoutInflater.from(context).inflate(R.layout.pull_to_refresh,null,true);

mProgressBar=(ProgressBar) header.findViewById(R.id.pb);

arrow=(ImageView) header.findViewById(R.id.iv_arrow);

tv_des=(TextView) header.findViewById(R.id.tv_description);

tv_update=(TextView) header.findViewById(R.id.tv_update);

touchSlop=ViewConfiguration.get(context).getScaledTouchSlop()*3;   //得到  至少移动的距离

refreshUpdatedAtValue();  //更新文字描述

setOrientation(VERTICAL);  //设置摆放方向

addView(header, 0);

}

//进行一些关键的初始化操作,比如:将下拉头向上偏移进行隐藏,给ListView注册touch事件

@Override

protected void onLayout(boolean changed, int l, int t, int r, int b) {

super.onLayout(changed, l, t, r, b);

if(changed&&!loadOnce){   //只执行一次

hideHeaderHeight=-header.getHeight();     //设置成负值   刚好隐藏在页面的最上方

headerLayoutParams = (MarginLayoutParams) header.getLayoutParams();

headerLayoutParams.topMargin=hideHeaderHeight;    //设置布局的topMargin

header.setLayoutParams(headerLayoutParams);

lv=(ListView) getChildAt(1);   //找到 listView  因为第一个Child是上拉头   所以第二个才是 ListView.

lv.setOnTouchListener(this);

loadOnce=true;

}

}

//给下拉刷新控件注册一个监听器

public void setOnRefreshListener(PullToRefreshListener mListener,int id){

this.mListener=mListener;

mId=id;

}

//更新下拉头中的信息

private void updateHeaderView(){

if(lastStatus!=currentStatus){

if(currentStatus==STATUS_PULL_TO_REFRESH){

tv_des.setText("下拉刷新");

arrow.setVisibility(View.VISIBLE);

mProgressBar.setVisibility(View.GONE);

rotateArrow();

}

else if(currentStatus==STATUS_RELEASE_TO_REFRESH){

tv_des.setText("释放刷新");

arrow.setVisibility(View.VISIBLE);

mProgressBar.setVisibility(View.GONE);

rotateArrow();

}

else if(currentStatus==STATUS_REFRESHING){

tv_des.setText("正在刷新中");

mProgressBar.setVisibility(View.VISIBLE);

arrow.clearAnimation(); //清除动画效果

arrow.setVisibility(View.GONE);

}

refreshUpdatedAtValue();

}

}

//根据当前的状态来旋转箭头

private void rotateArrow(){

float pivoX=arrow.getWidth()/2f;

float pivoY=arrow.getHeight()/2f;

float fromDegress=0f;

float toDegress=0f;

if(currentStatus==STATUS_PULL_TO_REFRESH){

fromDegress=180f;

toDegress=360f;

}

else{

fromDegress=0f;

toDegress=180f;

}

RotateAnimation animation=new RotateAnimation(fromDegress,toDegress,pivoX,pivoY);

animation.setDuration(100);

animation.setFillAfter(true);

arrow.startAnimation(animation);

}

//根据当前listView的滚动状态来设定   ableToPull 的值

//每次都需要在onTouch中的一个执行,这样可以判断出当前滚动的是listView,还是应该进行下拉

private void setIsAbleToPull(MotionEvent event){

View firstView=lv.getChildAt(0);

if(firstView!=null){

int firstVisiblePos=lv.getFirstVisiblePosition();   //获得listView顶头项的是该列数据的第几个

if(firstVisiblePos==0&&firstView.getTop()==0){

if(!ableToPull){

yDown=event.getRawY();

}

//如果首个元素的上边缘,距离父布局值为0,就说明 listView滚到了最顶部,此时允许下拉刷新

ableToPull=true;

}

else{

if(headerLayoutParams.topMargin!=hideHeaderHeight){

headerLayoutParams.topMargin=hideHeaderHeight;

header.setLayoutParams(headerLayoutParams);

}

ableToPull=false;

}

}

}

//当所有刷新的逻辑执行完成后,停止刷新, 并记录

public void finishRefreshing(){

currentStatus=STATUS_REFRESH_FINISH;

mShared.edit().putLong(UPDATE_AT+mId, System.currentTimeMillis()).commit();

new HideHeaderTask().execute();

}

//更新下拉头中上次更新时间的文字描述

private void refreshUpdatedAtValue(){

lastUpdateTime=mShared.getLong(UPDATE_AT+mId,-1);  //从配置文件中取出上次更新的时间的毫秒数

long currentTime=System.currentTimeMillis();       //获得当前时间毫秒数

long timePassed=currentTime-lastUpdateTime;        //中间相差的毫秒数

long timeIntoFormat;

String updateAtValue;

if(lastUpdateTime==-1){

updateAtValue="暂未更新过";

}

else if(timePassed<0){

updateAtValue="时间故障";

}

else if(timePassed<ONE_MINUTE){

updateAtValue="刚刚更新";

}

else if(timePassed<ONE_HOUR){

timeIntoFormat=timePassed/ONE_HOUR;

String value=timeIntoFormat+"分钟";

updateAtValue=String.format("上次更新于%1$s前",value);

}

else if(timePassed<ONE_DAY){

timeIntoFormat=timePassed/ONE_HOUR;

String value=timeIntoFormat+"小时";

updateAtValue=String.format("上次更新于%1$s前",value);

}

else if(timePassed<ONE_MONTH){

timeIntoFormat=timePassed/ONE_DAY;

String value=timeIntoFormat+"天";

updateAtValue=String.format("上次更新于%1$s前",value);

}

else if(timePassed<ONE_YEAR){

timeIntoFormat=timePassed/ONE_MONTH;

String value=timeIntoFormat+"月";

updateAtValue=String.format("上次更新于%1$s前",value);

}

else{

timeIntoFormat=timePassed/ONE_YEAR;

String value=timeIntoFormat+"年";

updateAtValue=String.format("上次更新于%1$s前",value);

}

tv_update.setText(updateAtValue);

}

//当listView被触摸时调用,其中处理了各种下拉刷新的具体逻辑

@Override

public boolean onTouch(View v, MotionEvent event) {

setIsAbleToPull(event);

if(ableToPull){

switch (event.getAction()) {

case MotionEvent.ACTION_DOWN:

yDown=event.getRawY();

break;

case MotionEvent.ACTION_MOVE:

float yMove=event.getRawY();

int distance=(int)(yMove-yDown);

if(distance<=0&&headerLayoutParams.topMargin<=hideHeaderHeight){

return false;

}

if(distance<touchSlop){

return false;

}

if(currentStatus!=STATUS_REFRESHING){

if(headerLayoutParams.topMargin>0){

currentStatus=STATUS_RELEASE_TO_REFRESH;

}

else{

currentStatus=STATUS_PULL_TO_REFRESH;

}

headerLayoutParams.topMargin=(distance/2)+hideHeaderHeight;

header.setLayoutParams(headerLayoutParams);   //让  ListView可以弹动

}

break;

case MotionEvent.ACTION_UP:

default:

if(currentStatus==STATUS_RELEASE_TO_REFRESH){

//松开手 如果是释放立即刷新  ,则去调用刷新的任务

new RefreshingTask().execute();

}

else if(currentStatus==STATUS_PULL_TO_REFRESH){

//松开手 如果是下拉状态,则去隐藏下拉头的任务

new HideHeaderTask().execute();

}

break;

}

if(currentStatus==STATUS_PULL_TO_REFRESH||currentStatus==STATUS_RELEASE_TO_REFRESH){

updateHeaderView();

//当前处于 下拉或释放 状态,要让listView失去焦点,否则被点击的那一项会一直处于选中状态

lv.setPressed(false);

lv.setFocusable(false);

lv.setFocusableInTouchMode(false);

lastStatus=currentStatus;

return true;

}

}

return false;

}

//正在刷新的任务

class RefreshingTask extends AsyncTask<Void, Integer, Void>{

@Override

protected Void doInBackground(Void... params) {

int topMargin=headerLayoutParams.topMargin;

while(true){

topMargin=topMargin+SCROLL_SPEED;

if(topMargin<=0){

topMargin=0;

break;

}

publishProgress(topMargin);

sleep(10);

}

currentStatus=STATUS_REFRESHING;

publishProgress(0);

if(mListener!=null){

mListener.onRefresh();  //通知刷新

}

return null;

}

@Override

protected void onProgressUpdate(Integer... topMargin) {

updateHeaderView();

headerLayoutParams.topMargin=topMargin[0];

header.setLayoutParams(headerLayoutParams);

}

}

//隐藏下拉头的任务

class HideHeaderTask extends AsyncTask<Void, Integer, Integer>{

@Override

protected Integer doInBackground(Void... params) {

int topMargin=headerLayoutParams.topMargin;

while(true){

topMargin=topMargin+SCROLL_SPEED;   //慢慢往回收缩

if(topMargin<=hideHeaderHeight){    //判断是不是回到了原位

topMargin=hideHeaderHeight;

break;

}

publishProgress(topMargin);  //设置  收缩动作

sleep(10);

}

return topMargin;

}

@Override

protected void onProgressUpdate(Integer... values) {

headerLayoutParams.topMargin=values[0];

header.setLayoutParams(headerLayoutParams);

}

@Override

protected void onPostExecute(Integer result) {

headerLayoutParams.topMargin=result;

header.setLayoutParams(headerLayoutParams);

currentStatus=STATUS_REFRESH_FINISH;

}

}

/**

* 使当前线程睡眠指定的毫秒数。

*

* @param time

*            指定当前线程睡眠多久,以毫秒为单位

*/

private void sleep(int time) {

try {

Thread.sleep(time);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

//下拉刷新的监听器

public interface PullToRefreshListener{

void onRefresh();

}

}

首先,在构造函数中动态添加了pull_to_refresh这个布局作为下拉头,然后将onLayout方法中将下拉头向上偏移出了屏幕,再给ListView注册了Touch事件.

如果在ListView上进行滑动,onTouch就会执行,onTouch首先会用setIsAbleToPull方法判断ListView是否滚动到了最顶部,只有滚动到最顶部才会执行后面的代码,否则就是ListView的正常滚动,不作处理.当ListView滚动到最顶部,如果手指还在向下拖动,就会改变下拉头的偏移值,让下拉头显示出来,如果下拉的距离足够大,在松手后就会执行刷新操作,如果距离不够大,则会隐藏下拉头.

具体刷新方法操作在RefreshingTask中进行,其中在doInBackground方法中回调了PullToRefreshListener接口的onRefresh()方法.

具体使用方法如下:

3.在activity_main.xml中

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:tools="http://schemas.android.com/tools"

android:layout_width="match_parent"

android:layout_height="match_parent"

tools:context=".MainActivity" >

<com.cy.pulltorefreshDemo.RefreshView

android:id="@+id/refresh_view"

android:layout_width="fill_parent"

android:layout_height="fill_parent"

>

<ListView

android:id="@+id/lv"

android:layout_width="fill_parent"

android:layout_height="fill_parent"

android:cacheColorHint="@android:color/transparent"

></ListView>

</com.cy.pulltorefreshDemo.RefreshView>

</RelativeLayout>

只要将需要刷新的ListView包含在  RefreshView中.

4.MainActivity.java

public class MainActivity extends Activity {

RefreshView refreshView;

ListView lv;

ArrayAdapter<String> adapter;

List<String> items=new ArrayList<String>();

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

items.add("A");

items.add("B");

refreshView=(RefreshView) findViewById(R.id.refresh_view);

lv=(ListView) findViewById(R.id.lv);

adapter=new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,items);

lv.setAdapter(adapter);

refreshView.setOnRefreshListener(new PullToRefreshListener() {

@Override

public void onRefresh() {

try {

Thread.sleep(3000);

} catch (InterruptedException e) {

e.printStackTrace();

}

items.add("222");//自动添加 到 ListView中

refreshView.finishRefreshing();

}

}, 0);

}

}

就这样,一个完整的下拉刷新.

来自为知笔记(Wiz)

时间: 2024-11-05 11:26:31

android 自定义控件之下拉刷新源码详解的相关文章

Android View 事件分发机制源码详解(View篇)

前言 在Android View 事件分发机制源码详解(ViewGroup篇)一文中,主要对ViewGroup#dispatchTouchEvent的源码做了相应的解析,其中说到在ViewGroup把事件传递给子View的时候,会调用子View的dispatchTouchEvent,这时分两种情况,如果子View也是一个ViewGroup那么再执行同样的流程继续把事件分发下去,即调用ViewGroup#dispatchTouchEvent:如果子View只是单纯的一个View,那么调用的是Vie

Android PullToRefresh (ListView GridView 下拉刷新) 使用详解

Android PullToRefresh (ListView GridView 下拉刷新) 使用详解 标签: Android下拉刷新pullToRefreshListViewGridView 版权声明:本文为博主原创文章,未经博主允许不得转载. 目录(?)[+] 转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/38238749,本文出自:[张鸿洋的博客] 群里一哥们今天聊天偶然提到这个git hub上的控件:pull-to-r

Android研究之手PullToRefresh(ListView GridView 下拉刷新)使用详解

 群里一哥们今天聊天偶然提到这个git hub上的控件:pull-to-refresh ,有兴趣的看下,例子中的功能极其强大,支持很多控件.本篇博客详细给大家介绍下ListView和GridView利用pull-to-rerfesh 实现下拉刷新和上拉加载更多.对布局不清楚的可以看Android研究自定义ViewGroup实现FlowLayout 详解. 1.ListView下拉刷新快速入门 pull-to-refresh对ListView进行了封装,叫做:PullToRefreshList

Android编程之Fragment动画加载方法源码详解

上次谈到了Fragment动画加载的异常问题,今天再聊聊它的动画加载loadAnimation的实现源代码: Animation loadAnimation(Fragment fragment, int transit, boolean enter, int transitionStyle) { 接下来具体看一下里面的源码部分,我将一部分一部分的讲解,首先是: Animation animObj = fragment.onCreateAnimation(transit, enter, fragm

Android ArrayMap源码详解

尊重原创,转载请标明出处    http://blog.csdn.net/abcdef314159 分析源码之前先来介绍一下ArrayMap的存储结构,ArrayMap数据的存储不同于HashMap和SparseArray,在上一篇<Android SparseArray源码详解>中我们讲到SparseArray是以纯数组的形式存储的,一个数组存储的是key值一个数组存储的是value值,今天我们分析的ArrayMap和SparseArray有点类似,他也是以纯数组的形式存储,不过不同的是他的

Spring IOC源码详解之容器初始化

Spring IOC源码详解之容器初始化 上篇介绍了Spring IOC的大致体系类图,先来看一段简短的代码,使用IOC比较典型的代码 ClassPathResource res = new ClassPathResource("beans.xml"); DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDe

IntentService源码详解

IntentService可以做什么: 如果你有一个任务,分成n个子任务,需要它们按照顺序完成.如果需要放到一个服务中完成,那么IntentService就会使最好的选择. IntentService是什么: IntentService是一个Service(看起来像废话,但是我第一眼看到这个名字,首先注意的是Intent啊.),所以如果自定义一个IntentService的话,一定要在AndroidManifest.xml里面声明. 从上面的"可以做什么"我们大概可以猜测一下Inten

butterknife源码详解

butterknife源码详解 作为Android开发者,大家肯定都知道大名鼎鼎的butterknife.它大大的提高了开发效率,虽然在很早之前就开始使用它了,但是只知道是通过注解的方式实现的,却一直没有仔细的学习下大牛的代码.最近在学习运行时注解,决定今天来系统的分析下butterknife的实现原理. 如果你之前不了解Annotation,那强烈建议你先看注解使用. 废多看图: 从图中可以很直观的看出它的module结构,以及使用示例代码. 它的目录和我们在注解使用这篇文章中介绍的一样,大体

Guava Cache源码详解

目录 一.引子 二.使用方法 2.1 CacheBuilder有3种失效重载模式 2.2 测试验证 三.源码剖析 3.1 简介 3.2 源码剖析 四.总结 优点: 缺点: 正文 回到顶部 一.引子 缓存有很多种解决方案,常见的是: 1.存储在内存中 : 内存缓存顾名思义直接存储在JVM内存中,JVM宕机那么内存丢失,读写速度快,但受内存大小的限制,且有丢失数据风险. 2.存储在磁盘中: 即从内存落地并序列化写入磁盘的缓存,持久化在磁盘,读写需要IO效率低,但是安全. 3.内存+磁盘组合方式:这种