android 下拉刷新源码分析

大家应该都用过微博空间之类的应用,这类应用现在都有个很普遍的特点,就是大部分都是采用ListView控件作为信息展示,而且现在大部分主流的应用很多都是具有下拉刷新的功能,具体请看示意图

开始下拉的时候


拉到一定位置的时候


拉到一定位置之后松开手后




在代码之前,先讲一下需要了解的知识点

onInterceptTouchEvent和onTouchEvent

onInterceptTouchEvent()是ViewGroup的一个方法,目的是在系统向该ViewGroup及其各个childView触发onTouchEvent()之前对相关事件进行一次拦截,Android这么设计的想法也很好理解,由于ViewGroup会包含若干childView,因此需要能够统一监控各种touch事件的机会,因此纯粹的不能包含子view的控件是没有这个方法的,如LinearLayout就有,TextView就没有。 

onInterceptTouchEvent()使用也很简单,如果在ViewGroup里覆写了该方法,那么就可以对各种touch事件加以拦截。但是如何拦截,是否所有的touch事件都需要拦截则是比较复杂的,touch事件在onInterceptTouchEvent()和onTouchEvent以及各个childView间的传递机制完全取决于onInterceptTouchEvent()和onTouchEvent()的返回值。并且,针对down事件处理的返回值直接影响到后续move和up事件的接收和传递。 

关于返回值的问题,基本规则很清楚,如果return true,那么表示该方法消费了此次事件,如果return false,那么表示该方法并未处理完全,该事件仍然需要以某种方式传递下去继续等待处理。

由于onInterceptTouchEvent()的机制比较复杂,总结一下,基本的规则是:

1.       down事件首先会传递到onInterceptTouchEvent()方法

2.       如果该ViewGroup的onInterceptTouchEvent()在接收到down事件处理完成之后return false,那么后续的move, up等事件将继续会先传递给该ViewGroup,之后才和down事件一样传递给最终的目标view的onTouchEvent()处理。

3.       如果该ViewGroup的onInterceptTouchEvent()在接收到down事件处理完成之后return true,那么后续的move, up等事件将不再传递给onInterceptTouchEvent(),而是和down事件一样传递给该ViewGroup的onTouchEvent()处理,注意,目标view将接收不到任何事件。

4.       如果最终需要处理事件的view的onTouchEvent()返回了false,那么该事件将被传递至其上一层次的view的onTouchEvent()处理。

5.       如果最终需要处理事件的view 的onTouchEvent()返回了true,那么后续事件将可以继续传递给该view的onTouchEvent()处理。


computeScroll() 
主要功能是计算拖动的位移量、更新背景、设置要显示的屏幕,这个函数一般要和Scroller类联合使用

然后上代码

package com.ishow.pjd2;


import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;


import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.Scroller;
import android.widget.TextView;


/**
 * 刷新控制view
 * 
 * @author panjidong
 * 
 */
public class RefreshableLinearLayout extends LinearLayout {


private static final String TAG = "LILITH";
private Scroller scroller;
private View refreshView;
private ImageView refreshIndicatorView;
private int refreshTargetTop = -60;
private ProgressBar bar;
private TextView downTextView;
private TextView timeTextView;
private LinearLayout refereshLinearLayout;


private RefreshListener refreshListener;


private String downTextString;
private String releaseTextString;


private Long refreshTime = null;
private int lastX;
private int lastY;
// 拉动标记
private boolean isDragging = false;
// 是否可刷新标记
private boolean isRefreshEnabled = true;
// 在刷新中标记
private boolean isRefreshing = false;


private Context mContext;


public RefreshableLinearLayout(Context context) {
super(context);
mContext = context;
}


public RefreshableLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
init();


}

private void init() {
// TODO Auto-generated method stub
// 滑动对象,
scroller = new Scroller(mContext);


// 刷新视图顶端的的view
refreshView = LayoutInflater.from(mContext).inflate(
R.layout.refresh_top_item, null);


/**
* 拉动时候的视图
*/
// 指示器view
refreshIndicatorView = (ImageView) refreshView
.findViewById(R.id.indicator);
// 下拉显示text
downTextView = (TextView) refreshView.findViewById(R.id.refresh_hint);


/**
* 刷新时候的视图
*/
// 根LinearLayout
refereshLinearLayout = (LinearLayout) refreshView
.findViewById(R.id.referesh_linearlayout);
// 刷新bar
bar = (ProgressBar) refreshView.findViewById(R.id.progress);
// 下来显示时间
timeTextView = (TextView) refreshView.findViewById(R.id.refresh_time);


LayoutParams lp = new LinearLayout.LayoutParams(
LayoutParams.FILL_PARENT, -refreshTargetTop);
lp.topMargin = refreshTargetTop;
lp.gravity = Gravity.CENTER;
addView(refreshView, lp);
downTextString = mContext.getResources().getString(
R.string.refresh_down_text);
releaseTextString = mContext.getResources().getString(
R.string.refresh_release_text);
}


/**
* 刷新

* @param time
*/
private void setRefreshDate(Date date) {
SimpleDateFormat format = new SimpleDateFormat("MM-dd hh:mm");
timeTextView.setText("更新与:" + format.format(date));
}


@Override
public boolean onTouchEvent(MotionEvent event) {
if (isRefreshing) {
return false;
}


int y = (int) event.getRawY();


switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 记录下y坐标
lastY = y;
break;


case MotionEvent.ACTION_MOVE:
Log.i(TAG, "ACTION_MOVE");
// y移动坐标
int m = y - lastY;
// if (((m > -1)) ) {
doMovement(m);
// }
// 记录下此刻y坐标
this.lastY = y;
break;


case MotionEvent.ACTION_UP:
Log.i(TAG, "ACTION_UP");


fling();


break;
}
return true;
}


/**
* up事件处理
*/
private void fling() {
// TODO Auto-generated method stub
if (isRefreshing) {
return;
}
LinearLayout.LayoutParams lp = (LayoutParams) refreshView
.getLayoutParams();
Log.i(TAG, "fling()" + lp.topMargin);
if (lp.topMargin > 0) {// 拉到了触发可刷新事件
refresh();
} else {
returnInitState();
}
}


private void returnInitState() {
// TODO Auto-generated method stub
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) this.refreshView
.getLayoutParams();
int i = lp.topMargin;
scroller.startScroll(0, i, 0, refreshTargetTop - i);
invalidate();
}


private void refresh() {
// TODO Auto-generated method stub
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) this.refreshView
.getLayoutParams();
int i = lp.topMargin;
refreshIndicatorView.setVisibility(View.GONE);
refereshLinearLayout.setVisibility(View.VISIBLE);
// bar.setVisibility(View.VISIBLE);
// timeTextView.setVisibility(View.VISIBLE);
downTextView.setVisibility(View.GONE);
scroller.startScroll(0, i, 0, 0 - i);
invalidate();
if (refreshListener != null) {
refreshListener.onRefresh(this);
isRefreshing = true;
}
}


/** 
     *  
     */
@Override
public void computeScroll() {
// TODO Auto-generated method stub
if (scroller.computeScrollOffset()) {
int i = this.scroller.getCurrY();
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) this.refreshView
.getLayoutParams();
// int k = Math.max(i, refreshTargetTop);
lp.topMargin = i;
this.refreshView.setLayoutParams(lp);
this.refreshView.invalidate();
invalidate();
}
}


/**
* 下拉move事件处理

* @param moveY
*/
private void doMovement(int moveY) {
// TODO Auto-generated method stub
LinearLayout.LayoutParams lp = (LayoutParams) refreshView
.getLayoutParams();
// if(moveY > 0){
// 获取view的上边距
float f1 = lp.topMargin;
float f2 = moveY * 0.3F;
int i = (int) (f1 + f2);
// 修改上边距
lp.topMargin = i;
// 修改后刷新
refreshView.setLayoutParams(lp);
refreshView.invalidate();
invalidate();
// }
if (refreshTime != null) {
setRefreshTime(refreshTime);
}
downTextView.setVisibility(View.VISIBLE);
refreshIndicatorView.setVisibility(View.VISIBLE);


refereshLinearLayout.setVisibility(View.GONE);
// timeTextView.setVisibility(View.GONE);
// bar.setVisibility(View.GONE);
if (lp.topMargin > 0) {
downTextView.setText(R.string.refresh_release_text);
refreshIndicatorView.setImageResource(R.drawable.refresh_arrow_up);
} else {
downTextView.setText(R.string.refresh_down_text);
refreshIndicatorView
.setImageResource(R.drawable.refresh_arrow_down);
}


}


public void setRefreshEnabled(boolean b) {
this.isRefreshEnabled = b;
}


public void setRefreshListener(RefreshListener listener) {
this.refreshListener = listener;
}


/**
* 刷新时间

* @param refreshTime2
*/
private void setRefreshTime(Long time) {
// TODO Auto-generated method stub


}


/**
* 结束刷新事件
*/
public void finishRefresh() {
Log.i(TAG, "执行了=====finishRefresh");
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) this.refreshView
.getLayoutParams();
int i = lp.topMargin;
scroller.startScroll(0, i, 0, refreshTargetTop);
invalidate();
// refreshIndicatorView.setVisibility(View.VISIBLE);
// timeTextView.setVisibility(View.VISIBLE);
isRefreshing = false;
setRefreshDate(Calendar.getInstance().getTime());
}


/*
* 该方法一般和ontouchEvent 一起用 (non-Javadoc)

* @see
* Android.view.ViewGroup#onInterceptTouchEvent(android.view.MotionEvent)
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
// TODO Auto-generated method stub
int action = e.getAction();
int y = (int) e.getRawY();
switch (action) {
case MotionEvent.ACTION_DOWN:
lastY = y;
break;


case MotionEvent.ACTION_MOVE:
// y移动坐标
int m = y - lastY;


// 记录下此刻y坐标
this.lastY = y;
if (canScroll(m)) {
return true;
}
break;
case MotionEvent.ACTION_UP:


break;


case MotionEvent.ACTION_CANCEL:


break;
}
return false;
}



private boolean canScroll(int m) {
View childView;
if(m<0){
return false;
}
if (getChildCount() > 1) {
childView = this.getChildAt(1);
if (childView instanceof ListView) {
int top = ((ListView) childView).getChildAt(0).getTop();
int pad = ((ListView) childView).getListPaddingTop();
if ((Math.abs(top - pad)) < 3
&& ((ListView) childView).getFirstVisiblePosition() == 0) {
return true;
} else {
return false;
}
} else if (childView instanceof ScrollView) {
if (((ScrollView) childView).getScrollY() == 0) {
return true;
} else {
return false;
}
}
//可以多加个else if来支持GridView,自己思考吧!
}
return false;
}


/**
* 刷新监听接口

* @author Nono

*/
public interface RefreshListener {
public void onRefresh(RefreshableLinearLayout view);
}
}


最后我讲一下思路

网上大部分的刷新控件是继承ListView或者继承ViewGroup控件实现的,

我这里用的是后者,继承ViewGroup的子类---->LinearLayout


一、

在控件初始化的时候去inflate头部所用的layout文件add到控件里,用法就是用ListView之类的控件作为本控件的子控件就可以了。


二、

接下来就是具体的逻辑了,这里拿子控件是ListView为例

我们知道,ListView本身就是有滑动事件的,所以我们不能在ListView需要滑动的时候截取他的滑动事件。

当onInterceptTouchEvent(MotionEvent e)返回true的时候listView就获取不到滑动事件了,返回值为false时会获取到滑动事件,

这里的canScroll()方法的功能是判断本控件能否滑动,判断的逻辑是  用户正在向下拉 而且 listView的第一个控件完全显示。


三、

在onInterceptTouchEvent(MotionEvent e)返回true之后会调用本类的onTouchEvent(MotionEvent event)了,

主要的逻辑就是写在这个方法里面,当你手指按下的时候记录坐标,每次移动都计算移动的距离并手动刷新,

在本控件距离头部大于0的时候显示”松开即可刷新“(也就是初始化时add进来的控件完全显示时),小于0的时候显示"下拉即可刷新"(还没有完全显示时显示)

当用户手指放开屏幕时候判断是否调用刷新事件,分2种情况:

1、如果距离头部大于0也就是显示”松开即可刷新“,就去调用事件监听器RefreshListener执行刷新方法onRefresh(RefreshableLinearLayout view)并显示"正在刷新",刷新完成滑动到ListView第一条刚好显示的位置。

2、如果距离头部小于0也就是显示”下拉即可刷新“,就直接滑动到ListView第一条刚好显示的位置。


四、

最后就是界面的滑动过程了,我们知道,在用户手指拖动的时候我们和手指的运动轨迹一样就可以了,但是现在手指放开了,我们只知道最后要移动到这里,如果你直接设置移动,那会看起来很生硬,动作一点儿也不连续。这里用的是computeScroll()和Scroller类同时使用,先调用scroller.startScroll(0, i, 0, refreshTargetTop - i),然后记住调用本类的invalidate(),调用完了invalidate()之后它会去调用本类的computeScroll()方法(这个过程是极短的时间内完成的),这个方法里面判断scroller是否已经停止滑动,如果没有停止滑动,就根据scroller的值设置当前控件显示的位置,从而达到视觉上的滑动效果。


补充:以上所阐述有知识点不知道的朋友们建议看下我之前推荐的2篇博文,讲的比较详细。


最后的最后

欢迎转载


项目源码下载地址

andorid下拉刷新控件demo》








评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值