您的位置:首页 > 移动开发 > Android开发

Android下拉刷新代码完全解析,完全解读大神代码,解决两个小bug

2016-09-17 14:37 369 查看
本文转载自郭霖的博客转载地址:http://blog.csdn.net/guolin_blog/article/details/9255575,感谢大神的分享!

本文旨在解读代码,让喜欢IT,不断拼搏的程序员同学们能更快的学会大神分享的代码,若有不足之处,请多指教,大神勿喷啊!
不知道从什么时候开始,下拉和侧滑是做App必备的两项装B利器,在没有使用官方原生的侧滑和下拉之前,那都是无数大神用代码构造的啊,看见别人写的第三方,自己也拿来用,用久了,感觉不给力,出bug了不知道如何下手修复,后来就打算去网上找源码看看别人怎么写的,Google搜索排名第一的郭大神的下拉代码就被找出来了,写的原理确实不错啊,逻辑思维也很清晰啊,自己使用的同时也推荐给身边的同事,不过好多同事觉得注释太少看不懂,而且看的也是半懂,小弟怀着为天下苍生谋福利的想法,就把大神的代码全部注释翻译了一遍.

中间发现两个小问题,一个就是下拉刷新后,箭头图片无法隐藏,不过也解决了,就是一句代码的事,另外一个就是listview方法哦fragment时,下拉箭头初始化不会自动隐藏,不过还是一句代码的事,废话不多说,直接上代码,全面解读郭大神的下拉刷新:

差不多每一行都有注释,对于刚进入自绘控件的同学很有帮助.
首先是下拉头的xml文件:其中的箭头自己找一个吧,图片就不展示了

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="60dp">
<!--线性布局装载-->
<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/imagejiantou"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="@mipmap/pulltorefresh_down_arrow"
/>
<!--正在更新时使用的ProgressBar-->
<ProgressBar
android:id="@+id/progress_bar"
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/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/updated_at"
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:gravity="center_horizontal|top"
android:text="最后更新:暂未更新" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>

然后开始正题,下拉刷新的代码:
新建一个类继承LinearLayout:

public class PulltoRefresh extends LinearLayout implements View.OnTouchListener {
private SharedPreferences preferences;
//状态:正在刷新
private int STATE_FRESHING = 0;
//状态:下拉刷新
private int STATE_PULL = 1;
//状态:刷新完成
private int STATE_FINISH = 2;
//状态:释放刷新
private int STATE_RELEASE = 3;
//初始状态
private int STATE_CURRENT = STATE_FINISH;
//记录上次状态
private int STATE_LAST = STATE_CURRENT;
private View header;
private ProgressBar progressBar;
private ImageView imagejiantou;
private TextView description;
private TextView updateAt;
//在被判定为滚动之前用户手指可以移动的最大值。下面有解释
private int touchSlop;
//只能画一次头,下面有解释
private boolean loadOnce = false;
//高度
private int hideHeaderHeight;
private ViewGroup.MarginLayoutParams headerLayoutParams;
private ListView listview;
//是否允许下拉
private boolean ableToPull;
//listview滚动到顶部的时候,手指点击的坐标
private float yDown;
//刷新监听
private OnPullToRefreshListener onPullToRefreshListener;
//区分监听的ID
private int listenerId;

public PulltoRefresh(Context context, AttributeSet attrs) {
super(context, attrs);
//这个不用说吧
preferences = context.getSharedPreferences("VALUE", Context.MODE_PRIVATE);
//需要添加的头
header = LayoutInflater.from(context).inflate(R.layout.activity_pullrefresh, null);
//旋转的bar控件
progressBar = (ProgressBar) header.findViewById(R.id.progress_bar);
//下拉的图片箭头
imagejiantou = (ImageView) header.findViewById(R.id.imagejiantou);
//描述下拉可以刷新的控件
description = (TextView) header.findViewById(R.id.description);
//更新时间
updateAt = (TextView) header.findViewById(R.id.updated_at);
//getScaledTouchSlop是一个距离,表示滑动的时候,手的移动要大于这个距离才开始移动控件。
//如果小于这个距离就不触发移动控件,如viewpager就是用这个距离来判断用户是否翻页
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
//refreshUpdatedAtValue();
//设置方向
setOrientation(VERTICAL);
//将view添加到0的位置
addView(header, 0);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
//这里有很多疑问,查了很多资料,说一说本人的理解,
//当自绘控件没有发生位置移动时,changed会为false,此时不会绘制
//当自绘控件发生位置移动时,changed会为true,此时系统会调用该方法绘制
//但是我们只需要绘制一次,如果每次都绘制,那么下拉就拉不下来
//所以这里就在引入一个变量loadOnce,使其先为false,然后同时判断,让该方法第一次时开始绘制
//绘制完的时候将loadOnce改为true,这样后面系统就不能再次绘制该控件
if (changed && !loadOnce) {
//控件的隐藏高度
hideHeaderHeight = -header.getHeight();
//得到MarginLayoutParams参数
headerLayoutParams = (MarginLayoutParams) header.getLayoutParams();
//设置顶部的margin
headerLayoutParams.topMargin = hideHeaderHeight;
//此处如果listview实在MainActivity里面,可以不用设置
//如果是在fragment里面必须设置,笔者亲测...之前没有这句,后来加上后搞定
header.setLayoutParams(headerLayoutParams);
//得到listview,此处查看xml文件
listview = (ListView) getChildAt(1);
//设置listview的监听
listview.setOnTouchListener(this);
//改变变量,使系统只绘制一次
loadOnce = true;
}
}

//listview的监听
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 absolute = (int) (ymove - yDown);
//如果下拉的距离小于等于0,是上拉,上拉的时候肯定不刷新呗
//并且header的topmargin没有超过hideHeaderHeight,说明根本就没有下拉出来
//如果手指是下滑状态,并且下拉头是完全隐藏的,就屏蔽下拉事件
if (absolute <= 0 && headerLayoutParams.topMargin <= hideHeaderHeight) {
return false;
}
//距离小于touchSlop,也应该false
if (absolute < touchSlop) {
return false;
}
//如果当前状况不是正在刷新中
if (STATE_CURRENT != STATE_FRESHING) {
//判断topMargin>0,是释放状态
if (headerLayoutParams.topMargin > 0) {
STATE_CURRENT = STATE_RELEASE;
} else {
//否则就是下拉状态
STATE_CURRENT = STATE_PULL;
}
// 通过偏移下拉头的topMargin值,来实现下拉效果
//因为absolute的距离很大,所以除以2,在来与hideHeaderHeight搭配
headerLayoutParams.topMargin = absolute / 2 + hideHeaderHeight;
header.setLayoutParams(headerLayoutParams);
}
break;
case MotionEvent.ACTION_UP:
default:
//手指松开时,判断状态
//手指松开时如果是释放状态,立即调用刷新方法
if (STATE_CURRENT == STATE_RELEASE) {
//刷新任务
new PullRefreshing().execute();
//手指松开时如果是下拉状态,就调用下拉隐藏任务方法
} else if (STATE_CURRENT == STATE_PULL) {
// 隐藏下拉任务
new hideHeader().execute();
}
break;
}
//时刻记得更新下拉信息
if (STATE_CURRENT == STATE_PULL || STATE_CURRENT == STATE_RELEASE) {
//更新header任务
updateHeader();
// 当前正处于下拉或释放状态,要让ListView失去焦点,否则被点击的那一项会一直处于选中状态
listview.setPressed(false);
listview.setFocusable(false);
listview.setFocusableInTouchMode(false);
STATE_LAST = STATE_CURRENT;
//当前正处于下拉或释放状态,通过返回true屏蔽掉ListView的滚动事件
return true;
}

}
return false;
}

//刷新完成方法
public void finishRefresh() {
//记录状态
STATE_CURRENT = STATE_FINISH;
//将当前时间的毫秒数写入xml文件,方便下次更新是调用计算更新时间
//将传入监听的ListenerId也一起写进去,方便后续判断是哪个listview的更新时间
preferences.edit().putLong("TIME" + listenerId, System.currentTimeMillis()).commit();
//隐藏下拉头
new hideHeader().execute();
}

//判断能否下拉
private void setIsAbleToPull(MotionEvent event) {
//得到listview的第一个item
View firstchild = listview.getChildAt(0);
if (firstchild != null) {
//得到listview当前屏幕最上方的索引
int firstVisiblePosition = listview.getFirstVisiblePosition();
if (firstVisiblePosition == 0 && firstchild.getTop() == 0) {

if (!ableToPull) {
//记录坐标
yDown = event.getRawY();
}
// 如果首个元素的上边缘,距离父布局值为0,就说明ListView滚动到了最顶部,此时应该允许下拉刷新
ableToPull = true;
} else {
//如果listview没有滚动到顶部,判断header的topmargin,超出自身高度后
//设置回自身高度的margin
if (headerLayoutParams.topMargin != hideHeaderHeight) {
headerLayoutParams.topMargin = hideHeaderHeight;
header.setLayoutParams(headerLayoutParams);
}
//当然此时还是不能下拉
ableToPull = false;
}
} else {
//如果listview中没有内容,应该允许下拉
ableToPull = true;
}
}

//为了防止不同界面的下拉刷新在上次更新时间上互相有冲突, 请不同界面在注册下拉刷新监听器时一定要传入不同的id
public void setOnPullToRefreshListener(OnPullToRefreshListener onPullToRefreshListener, int id) {
this.onPullToRefreshListener = onPullToRefreshListener;
listenerId = id;
}

public interface OnPullToRefreshListener {
void onPullToRefreshListener();
}

//隐藏header任务
//隐藏下拉头的任务,当未进行下拉刷新或下拉刷新完成后,此任务将会使下拉头重新隐藏。
class hideHeader extends AsyncTask<Void, Integer, Integer> {
@Override
protected Integer doInBackground(Void... params) {
int topMargin = headerLayoutParams.topMargin;
int speed = -20;
while (true) {
topMargin = topMargin + speed;
if (topMargin <= hideHeaderHeight) {
topMargin = hideHeaderHeight;
break;
}
publishProgress(topMargin);
SystemClock.sleep(10);
}

return topMargin;
}

@Override
protected void onPostExecute(Integer integer) {
super.onPostExecute(integer);
headerLayoutParams.topMargin = integer;
header.setLayoutParams(headerLayoutParams);
STATE_CURRENT = STATE_FINISH;
}

@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
headerLayoutParams.topMargin = values[0];
header.setLayoutParams(headerLayoutParams);
}
}

//设置旋转箭头方法
public void rotateImage() {
//根据状态分别定义旋转的起始角度
float fromDegrees = 0f;
float toDegrees = 0f;
if (STATE_CURRENT == STATE_PULL) {
fromDegrees = 180f;
toDegrees = 360f;
} else if (STATE_CURRENT == STATE_RELEASE) {
fromDegrees = 0f;
toDegrees = 180f;
}
//此处有改动
//旋转动画需要设置旋转的中心点,
//Animation.RELATIVE_TO_SELF意思是相对于自己
//两个0.5f,意思是相对于自己的x和y各一半,也就是控件自身的中心
RotateAnimation rotateAnimation = new RotateAnimation(fromDegrees, toDegrees, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
rotateAnimation.setDuration(100);
rotateAnimation.setFillAfter(true);
imagejiantou.startAnimation(rotateAnimation);
}

//设置更新头方法
public void updateHeader() {
if (STATE_LAST != STATE_CURRENT) {
if (STATE_CURRENT == STATE_PULL) {
description.setText("下拉刷新");
imagejiantou.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
rotateImage();
} else if (STATE_CURRENT == STATE_RELEASE) {
description.setText("释放刷新");
imagejiantou.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
rotateImage();
} else if (STATE_CURRENT == STATE_FRESHING) {
description.setText("正在刷新");
//此处代码必须加上,之前使用时,发现箭头图片不会隐藏
//后来才知道,图片设置过动画,而且设置了rotateAnimation.setFillAfter(true)方法后
//该图片就不会被隐藏掉,
//所以隐藏前必须先清除动画
imagejiantou.clearAnimation();
imagejiantou.setVisibility(View.GONE);
progressBar.setVisibility(View.VISIBLE);
}
//更新时间
//此处时间有争议,可以放到点击事件MotionEvent.ACTION_DOWN:
//总之看个人喜好,建议放到构造方法或者onLayout方法里面去
updateTime();
}
}

//更新时间的方法
public void updateTime() {
//获得上次的更新时间
long lastTime = preferences.getLong("TIME" + listenerId, -1);
//得到当前的更新时间
long currentTime = System.currentTimeMillis();
//计算更新时间的间隔
long updateSpace = currentTime - lastTime;
//新建一个文本变量
String timeText = null;
//当上次时间等于默认值时,说明还没更新
if (lastTime == -1) {
timeText = "最后更新:暂未更新";
//当时间间隔小于0时,显示更新时间错误
} else if (updateSpace < 0) {
timeText = "更新时间错误";
//当时间间隔小于2分钟时,显示刚刚更新
} else if (updateSpace < 60 * 1000 ) {
timeText = "最后更新:刚刚更新";
//当时间间隔小于1小时,显示最后更新:xx分钟前
} else if (updateSpace < 60 * 1000 * 60) {
long updateCount = updateSpace / (60 * 1000);
timeText = "最后更新:" + updateCount + "分钟前";
//当时间间隔小于24小时,显示最后更新:xx小时前
} else if (updateSpace < 60 * 1000 * 60 * 24) {
long updateCount = updateSpace / (60 * 1000 * 60);
timeText = "最后更新:" + updateCount + "小时前";
} else {
//最后时间太长了,直接很久以前吧
timeText = "最后更新:很久以前";
}
//最后设置给textView
updateAt.setText(timeText);

}

//设置更新任务
class PullRefreshing extends AsyncTask<Void, Integer, Void> {
protected Void doInBackground(Void... params) {
int topMargin = headerLayoutParams.topMargin;
int speed = -20;
while (true) {
topMargin = topMargin + speed;
if (topMargin <= 0) {
topMargin = 0;
break;
}
publishProgress(topMargin);
SystemClock.sleep(10);
}
STATE_CURRENT = STATE_FRESHING;
publishProgress(0);
if (onPullToRefreshListener != null) {
onPullToRefreshListener.onPullToRefreshListener();
}
return null;
}
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
updateHeader();
headerLayoutParams.topMargin = values[0];
header.setLayoutParams(headerLayoutParams);
}
}
}


代码注释很详细吧,这里就不啰嗦咯!看下面使用!

先是Xml的引用,直接把listview放进去:

<?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:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.example.pulltorefesh.MainActivity">
<!--此处就是自绘的下拉刷新-->
<com.example.pulltorefesh.PulltoRefresh
android:id="@+id/pulltorefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--此处listview-->
<ListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="#ff0000"
android:dividerHeight="1px"
android:entries="@array/city"></ListView>
</com.example.pulltorefesh.PulltoRefresh>

</RelativeLayout>
然后是我们的MainActivity:

public class MainActivity extends Activity {

private PulltoRefresh pulltorefresh;
private ListView listView;

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView = (ListView) findViewById(R.id.listview);
pulltorefresh = (PulltoRefresh) findViewById(R.id.pulltorefresh);
//别忘记设置监听
pulltorefresh.setOnPullToRefreshListener(new PulltoRefresh.OnPullToRefreshListener() {
@Override
public void onPullToRefreshListener() {
//在这里执行刷新任务
//我这里就不执行刷新任务了,让系统睡两秒
SystemClock.sleep(2000);
//执行完后调用finish,如果是是异步或者子线程中刷新,记得传回主线程在调用finish
pulltorefresh.finishRefresh();
//监听ID别忘记了
}
}, 0);
}
}


代码解读完毕,注释很多吧,慢慢看吧!中间的下拉箭头图片不能隐藏的解决代码,可别忘记哦!

最后向大神致敬!!!!!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息